Просмотр исходного кода

Upgrade script from 14.0.0-A2 to 14.0.0-A3

Alex Trofimov 2 лет назад
Родитель
Сommit
b3a58cd9cb
58 измененных файлов с 40369 добавлено и 0 удалено
  1. 95 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/api.php
  2. 866 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolAcl.php
  3. 33 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolBan.php
  4. 959 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolConnection.php
  5. 339 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolContentFilter.php
  6. 211 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolGridConnections.php
  7. 282 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolModuleQuery.php
  8. 871 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolPage.php
  9. 393 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolPaginate.php
  10. 562 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolPayments.php
  11. 1299 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolSearch.php
  12. 1563 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolStorage.php
  13. 966 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolTranscoder.php
  14. 193 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/js/classes/BxDolMenuMoreAuto.js
  15. 1579 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/js/functions.js
  16. 2489 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/utils.inc.php
  17. 12 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/version.inc.php
  18. 111 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/general/classes/BxBaseModGeneralAlertsResponse.php
  19. 621 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/general/classes/BxBaseModGeneralMenuViewActions.php
  20. 3960 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/general/classes/BxBaseModGeneralModule.php
  21. 176 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/general/classes/BxBaseModGeneralPageEntry.php
  22. 226 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/groups/classes/BxBaseModGroupsConnectionFans.php
  23. 605 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/groups/classes/BxBaseModGroupsDb.php
  24. 86 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/groups/classes/BxBaseModGroupsGridBans.php
  25. 474 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/groups/classes/BxBaseModGroupsGridConnections.php
  26. 75 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/groups/classes/BxBaseModGroupsGridInvites.php
  27. 2239 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/groups/classes/BxBaseModGroupsModule.php
  28. 150 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/groups/classes/BxBaseModGroupsPageEntry.php
  29. 89 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/groups/classes/BxBaseModGroupsPrivacyPost.php
  30. 103 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/notifications/classes/BxBaseModNotificationsResponse.php
  31. 330 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/profile/classes/BxBaseModProfileGridAdministration.php
  32. 2326 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/profile/classes/BxBaseModProfileModule.php
  33. 159 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/profile/classes/BxBaseModProfilePageEntry.php
  34. 140 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/text/classes/BxBaseModTextConfig.php
  35. 498 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/text/classes/BxBaseModTextDb.php
  36. 1189 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/text/classes/BxBaseModTextModule.php
  37. 168 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/text/classes/BxBaseModTextPageEntry.php
  38. 1124 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/text/classes/BxBaseModTextTemplate.php
  39. 7 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/plugins/autoload.php
  40. 75 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/plugins/composer/autoload_real.php
  41. 226 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/plugins/composer/autoload_static.php
  42. 375 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/plugins/composer/installed.php
  43. 1326 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/plugins_public/tailwind/css/tailwind.css
  44. 0 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/plugins_public/tailwind/css/tailwind.min.css
  45. 96 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/template/css/cmts.css
  46. 687 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/template/css/forms.css
  47. 4 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/template/form_field_option_info.html
  48. 21 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/template/paginate.html
  49. 310 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBaseAccountForms.php
  50. 678 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBaseCmtsServices.php
  51. 2492 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBaseFormView.php
  52. 934 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBaseGrid.php
  53. 1208 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBasePage.php
  54. 471 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBaseSearchResult.php
  55. 797 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBaseServiceAccount.php
  56. 466 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBaseServiceConnections.php
  57. 1185 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBaseServiceProfiles.php
  58. 1450 0
      upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBaseServices.php

+ 95 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/api.php

@@ -0,0 +1,95 @@
+<?php
+
+define('BX_API', true);
+
+require_once('./inc/header.inc.php');
+require_once(BX_DIRECTORY_PATH_INC . "profiles.inc.php");
+require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+
+header('Content-Type: application/json');
+
+bx_api_check_access();
+
+// prepare params
+
+$aRequest = isset($_GET['r']) ? explode('/', $_GET['r']) : [];
+
+$a = ['sModule', 'sMethod', 'sClass'];
+foreach ($aRequest as $i => $v)
+    if (isset($a[$i]) && preg_match('/^[A-Za-z0-9_-]+$/', $v))
+        ${$a[$i]} = $v;
+
+if (!isset($sClass))
+    $sClass = 'Module';
+
+foreach ($a as $v) {
+    if (!isset($$v)) {
+        header('HTTP/1.0 404 Not Found');
+        header('Status: 404 Not Found');
+        echo json_encode(['status' => 404, 'error' => _t("_sys_request_page_not_found_cpt")]);
+        exit;
+    }
+}
+
+// check if service exists
+
+if (!BxDolRequest::serviceExists($sModule, $sMethod, $sClass)) {
+    header('HTTP/1.0 404 Not Found');
+    header('Status: 404 Not Found');
+    echo json_encode(['status' => 404, 'error' => _t("_sys_request_page_not_found_cpt")]);
+    exit;
+}
+
+// check if service is safe
+
+if (!getParam('sys_api_access_unsafe_services')) {
+    $b = BxDolRequest::serviceExists($sModule, 'is_safe_service', 'system' == $sModule ? 'TemplServices' : 'Module');
+    $bSafe = $b ? BxDolService::call($sModule, 'is_safe_service', array($sMethod), 'system' == $sModule ? 'TemplServices' : 'Module') : false;
+
+    $bPublic = false;
+    if (!$bSafe) {
+        $b = BxDolRequest::serviceExists($sModule, 'is_public_service', 'system' == $sModule ? 'TemplServices' : 'Module');
+        $bPublic = $b ? BxDolService::call($sModule, 'is_public_service', array($sMethod), 'system' == $sModule ? 'TemplServices' : 'Module') : false;
+    } 
+    if (!$bPublic && !$bSafe) {
+        header('HTTP/1.0 403 Forbidden');
+        header('Status: 403 Forbidden');
+        echo json_encode(['status' => 403, 'error' => _t("_Access denied")]);
+        exit;
+    }
+}
+
+// call service
+
+if (!($aParams = bx_get('params')))
+    $aParams = array();
+elseif (is_string($aParams) && preg_match('/^\[.*\]$/', $aParams))
+    $aParams = @json_decode($aParams);
+
+if (!is_array($aParams))
+    $aParams = array($aParams);
+
+$mixedRet = BxDolService::call($sModule, $sMethod, $aParams, $sClass);
+
+// check output and return JSON
+
+if (is_array($mixedRet) && isset($mixedRet['error'])) { 
+    header('HTTP/1.0 500 Internal Server Error');
+    header('Status: 500 Internal Server Error');
+    $a = [
+        'status' => isset($mixedRet['code']) ? $mixedRet['code'] : 500,
+        'error' => isset($mixedRet['desc']) ? $mixedRet['desc'] : $mixedRet['error'],
+    ];
+    if (isset($mixedRet['code']))
+        $a['code'] = $mixedRet['code'];
+    echo json_encode($a);
+    exit;
+}
+
+echo json_encode([
+    'status' => 200,
+    'module' => $sModule,
+    'method' => $sMethod,
+    'params' => $aParams,
+    'data' => $mixedRet,
+]);

+ 866 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolAcl.php

@@ -0,0 +1,866 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Message constants passed to _t_ext() function by checkAction()
+ *
+ * NOTE: checkAction() returns language dependent messages
+ */
+define('CHECK_ACTION_MESSAGE_NOT_ALLOWED',			"_sys_acl_action_not_allowed");
+define('CHECK_ACTION_MESSAGE_LIMIT_REACHED',		"_sys_acl_action_limit_reached");
+define('CHECK_ACTION_MESSAGE_MESSAGE_EVERY_PERIOD',	"_sys_acl_action_every_period");
+define('CHECK_ACTION_MESSAGE_NOT_ALLOWED_BEFORE',	"_sys_acl_action_not_allowed_before");
+define('CHECK_ACTION_MESSAGE_NOT_ALLOWED_AFTER',	"_sys_acl_action_not_allowed_after");
+
+define('CHECK_ACTION_MESSAGE_UNAUTHENTICATED',	    "_sys_acl_action_unauthenticated");
+define('CHECK_ACTION_MESSAGE_UNCONFIRMED',			"_sys_acl_action_unconfirmed");
+define('CHECK_ACTION_MESSAGE_PENDING',			    "_sys_acl_action_pending");
+define('CHECK_ACTION_MESSAGE_SUSPENDED',			"_sys_acl_action_suspended");
+
+/**
+ * Nodes of $args array that are passed to _t_ext() function by checkAction()
+ */
+define('CHECK_ACTION_LANG_FILE_ACTION', 1);
+define('CHECK_ACTION_LANG_FILE_MEMBERSHIP', 2);
+define('CHECK_ACTION_LANG_FILE_LIMIT', 3);
+define('CHECK_ACTION_LANG_FILE_PERIOD', 4);
+define('CHECK_ACTION_LANG_FILE_AFTER', 5);
+define('CHECK_ACTION_LANG_FILE_BEFORE', 6);
+define('CHECK_ACTION_LANG_FILE_SITE_EMAIL',	7);
+define('CHECK_ACTION_LANG_FILE_PERIOD_RESTART_AT', 8);
+
+/**
+ * Standard membership ID's
+ */
+define('MEMBERSHIP_ID_NON_MEMBER', 1);
+define('MEMBERSHIP_ID_ACCOUNT', 2);
+define('MEMBERSHIP_ID_STANDARD', 3);
+define('MEMBERSHIP_ID_UNCONFIRMED', 4);
+define('MEMBERSHIP_ID_PENDING', 5);
+define('MEMBERSHIP_ID_SUSPENDED', 6);
+define('MEMBERSHIP_ID_MODERATOR', 7);
+define('MEMBERSHIP_ID_ADMINISTRATOR', 8);
+
+/**
+ * Standard membership actions
+ */
+define('MEMBERSHIP_ACTION_EDIT_ANY_ENTRY', 'edit any entry');
+define('MEMBERSHIP_ACTION_DELETE_ANY_ENTRY', 'delete any entry');
+
+/**
+ * Indices for checkAction() result array
+ */
+define('CHECK_ACTION_RESULT', 0);
+define('CHECK_ACTION_MESSAGE', 1);
+define('CHECK_ACTION_PARAMETER', 3);
+
+/**
+ * CHECK_ACTION_RESULT node values
+ */
+define('CHECK_ACTION_RESULT_ALLOWED', 0);
+define('CHECK_ACTION_RESULT_NOT_ALLOWED', 1);
+define('CHECK_ACTION_RESULT_NOT_ACTIVE', 2);
+define('CHECK_ACTION_RESULT_LIMIT_REACHED', 3);
+define('CHECK_ACTION_RESULT_NOT_ALLOWED_BEFORE', 4);
+define('CHECK_ACTION_RESULT_NOT_ALLOWED_AFTER', 5);
+
+/**
+ * Standard period units
+ */
+define('MEMBERSHIP_PERIOD_UNIT_DAY', 'day');
+define('MEMBERSHIP_PERIOD_UNIT_WEEK', 'week');
+define('MEMBERSHIP_PERIOD_UNIT_MONTH', 'month');
+define('MEMBERSHIP_PERIOD_UNIT_YEAR', 'year');
+
+
+class BxDolAcl extends BxDolFactory implements iBxDolSingleton
+{
+    protected static $_aCacheData = array();
+
+    protected $oDb;
+
+    protected $_aStandardMemberships = array(
+        MEMBERSHIP_ID_NON_MEMBER => 1,
+        MEMBERSHIP_ID_ACCOUNT => 1,
+        MEMBERSHIP_ID_UNCONFIRMED => 1,
+        MEMBERSHIP_ID_PENDING => 1,
+        MEMBERSHIP_ID_SUSPENDED => 1,
+        MEMBERSHIP_ID_STANDARD => 1,
+    );
+
+    protected $_aProfileStatus2LevelMap = array (
+        BX_PROFILE_STATUS_SUSPENDED => MEMBERSHIP_ID_SUSPENDED,
+        BX_PROFILE_STATUS_PENDING => MEMBERSHIP_ID_PENDING,
+    );
+
+    protected $_aLevel2MessageMap = array (
+        MEMBERSHIP_ID_NON_MEMBER => '_sys_acl_action_unauthenticated',
+        MEMBERSHIP_ID_ACCOUNT => '_sys_acl_action_account',
+        MEMBERSHIP_ID_UNCONFIRMED => '_sys_acl_action_unconfirmed',
+        MEMBERSHIP_ID_PENDING => '_sys_acl_action_pending',
+        MEMBERSHIP_ID_SUSPENDED => '_sys_acl_action_suspended',
+    );
+
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+
+        $this->oDb = BxDolAclQuery::getInstance();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxTemplAcl();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+	/**
+     * Get necessary condition array to use membership level in search classes
+     * @param $sContentField content table field name
+     * @param $mixedLevelId level ID or array of level IDs
+     * @return array of conditions is returned
+     */
+    public function getContentByLevelAsCondition($sContentField, $mixedLevelId)
+    {
+        $iLevelId = !is_array($mixedLevelId) ? $mixedLevelId : 0;
+        if (!$iLevelId && is_array($mixedLevelId) && 1 == count($mixedLevelId)) {
+            $a = array_values($mixedLevelId);
+            $iLevelId = array_shift($a);
+        }
+
+        // unconfirmed
+        if (MEMBERSHIP_ID_UNCONFIRMED == $iLevelId) {
+            return array(
+                'restriction_sql' => ' AND `sys_accounts`.`email_confirmed` = 0 ',
+                'restriction' => array (),
+                'join' => array (),
+            );
+        }
+        // standard
+        elseif (MEMBERSHIP_ID_STANDARD == $iLevelId) {
+            return array(
+                'restriction_sql' => ' AND (`tlm`.DateStarts IS NULL OR `tlm`.DateStarts <= NOW()) AND (`tlm`.DateExpires IS NULL OR `tlm`.DateExpires > NOW()) AND `tlm`.`IDMember` IS NULL AND `sys_accounts`.`email_confirmed` != 0 ',
+                'restriction' => array (
+                ),
+                'join' => array (
+                    'acl_members' => array(
+                        'type' => 'LEFT',
+                        'table' => 'sys_acl_levels_members',
+                        'table_alias' => 'tlm',
+                        'mainField' => $sContentField,
+                        'onField' => 'IDMember',
+                        'joinFields' => array(),
+                    ),
+                ),
+            );
+        }
+        // other levels
+        else {
+            return array(
+                'restriction_sql' => ' AND (`tlm`.DateStarts IS NULL OR `tlm`.DateStarts <= NOW()) AND (`tlm`.DateExpires IS NULL OR `tlm`.DateExpires > NOW()) AND `sys_accounts`.`email_confirmed` != 0 ',
+                'restriction' => array (
+                    'acl_members' => array(
+                        'value' => $mixedLevelId,
+                        'field' => 'IDLevel',
+                        'operator' => is_array($mixedLevelId) ? 'in' : '=',
+                        'table' => 'tlm',
+                    ),
+                ),
+                'join' => array (
+                    'acl_members' => array(
+                        'type' => 'INNER',
+                        'table' => 'sys_acl_levels_members',
+                        'table_alias' => 'tlm',
+                        'mainField' => $sContentField,
+                        'onField' => 'IDMember',
+                        'joinFields' => array(),
+                    ),
+                ),
+            );
+        }
+    }
+
+	/**
+     * Get necessary parts of SQL query to use membership levels in other queries
+     * @param $sContentTable content table name
+     * @param $sContentField content table field name
+     * @param $mixedLevelId level ID or array of level IDs
+     * @return array of SQL string parts, for now 'where' part only is returned
+     */
+    public function getContentByLevelAsSQLPart($sContentTable, $sContentField, $mixedLevelId)
+    {
+        return $this->oDb->getContentByLevelAsSQLPart($sContentTable, $sContentField, $mixedLevelId);
+    }
+
+    /**
+     * Check if member has one of the provided membership levels
+     * @param $mixedPermissions - integer value (every bit is matched with some membership ID) or an array of membership IDs to check permissions for
+     * @param $iProfileId - profile to check, if it isn't provided or is false then currently logged in profile is used.
+     * @return true if member has privided membership levels, or false if member hasn't.
+     */
+    public function isMemberLevelInSet($mixedPermissions, $iProfileId = false)
+    {
+        $iPermissions = 0;
+        if(is_array($mixedPermissions))
+            foreach($mixedPermissions as $iPermissionId)
+                $iPermissions += pow(2, (int)$iPermissionId - 1);
+        else if(is_numeric($mixedPermissions))
+            $iPermissions = (int)$mixedPermissions;
+
+        if(!$iPermissions)
+            return false;
+
+        return ($iPermissions & $this->getMemberLevelBit($iProfileId));
+    }
+
+    /**
+     * Get user's membership level bit for bitwise operarions
+     */
+    public function getMemberLevelBit($iProfileId = 0)
+    {
+        if (!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+
+        $aACL = $this->getMemberMembershipInfo($iProfileId);
+        return pow(2, $aACL['id'] - 1);
+    }
+
+    /**
+     * Checks if a given action is allowed for a given profile and updates action information if the
+     * action is performed.
+     *
+     * @param  int     $iProfileId     ID of a profile that is going to perform an action
+     * @param  int     $iActionId      ID of the action itself
+     * @param  boolean $bPerformAction if true, then action information is updated, i.e. action is 'performed'
+     * @return array(
+     *                                CHECK_ACTION_RESULT => CHECK_ACTION_RESULT_ constant,
+     *                                CHECK_ACTION_MESSAGE => CHECK_ACTION_MESSAGE_ constant,
+     *                                CHECK_ACTION_PARAMETER => additional action parameter (string)
+     *                                )
+     *
+     * NOTES:
+     *
+     * $aResult[CHECK_ACTION_MESSAGE] contains a message with detailed information about the result,
+     * already processed by the language file
+     *
+     * if $aResult[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED then this node contains
+     * an empty string
+     *
+     * The error messages themselves are stored in the language file. Additional variables are
+     * passed to the languages.inc.php function _t_ext() as an array and can be used there in the form of
+     * {0}, {1}, {2} ...
+     *
+     * Additional variables passed to the lang. file on errors (can be used in error messages):
+     *
+     * 	For all errors:
+     *
+     * 		$arg0[CHECK_ACTION_LANG_FILE_ACTION]	= name of the action
+     * 		$arg0[CHECK_ACTION_LANG_FILE_MEMBERSHIP]= name of the current membership
+     *
+     * 	CHECK_ACTION_RESULT_LIMIT_REACHED:
+     *
+     * 		$arg0[CHECK_ACTION_LANG_FILE_LIMIT]		= limit on number of actions allowed for the profile
+     * 		$arg0[CHECK_ACTION_LANG_FILE_PERIOD]	= period that the limit is set for (in hours, 0 if unlimited)
+     * 		$arg0[CHECK_ACTION_LANG_FILE_PERIOD_RESTART_AT]	= time when new period begins, so counter will be reset
+     *
+     * 	CHECK_ACTION_RESULT_NOT_ALLOWED_BEFORE:
+     *
+     * 		$arg0[CHECK_ACTION_LANG_FILE_BEFORE]	= date/time since when the action is allowed
+     *
+     * 	CHECK_ACTION_RESULT_NOT_ALLOWED_AFTER:
+     *
+     * 		$arg0[CHECK_ACTION_LANG_FILE_AFTER]		= date/time since when the action is not allowed
+     *
+     * $aResult[CHECK_ACTION_PARAMETER] contains an additional parameter that can be considered
+     * when performing the action (like the number of profiles to show in search result)
+    */
+    function checkAction($iProfileId, $iActionId, $bPerformAction = false)
+    {
+        $aResult = array();
+        $aLangFileParams = array();
+
+        $iProfileId = (int)$iProfileId;
+        $iActionId = (int)$iActionId;
+        $bPerformAction = $bPerformAction ? true : false;
+
+        $aMembership = $this->getMemberMembershipInfo($iProfileId); // get current profile's membership information
+
+        $aLangFileParams[CHECK_ACTION_LANG_FILE_MEMBERSHIP] = _t($aMembership['name']);
+        $aLangFileParams[CHECK_ACTION_LANG_FILE_SITE_EMAIL] = getParam('site_email');
+
+        $aAction = $this->oDb->getAction($aMembership['id'], $iActionId);
+        if (!$aAction)
+            bx_trigger_error('Unknown action ID: ' . $iActionId, 2);
+
+        $aResult[CHECK_ACTION_PARAMETER] = $aAction['additional_param_value'];
+        $aLangFileParams[CHECK_ACTION_LANG_FILE_ACTION] = !empty($aAction['title']) ? _t($aAction['title']) : $aAction['name'];
+
+        /* Check membership action with ability to overwrite params - begin */
+        $bContinue = true;
+        bx_alert('membership', 'check_action', $iActionId, $iProfileId, array(
+            'action' => &$aAction,
+            'perform' => &$bPerformAction,
+            'result' => &$aResult,
+            'lang' => &$aLangFileParams,
+            'continue' => &$bContinue
+        ));
+
+        if (!$bContinue)
+            return $aResult;
+        /* Check membership action with ability to overwrite params - end */
+
+        /**
+         * Action is not allowed for the current membership
+         */
+        if (is_null($aAction['id'])) {
+
+            $sLangKey = CHECK_ACTION_MESSAGE_NOT_ALLOWED;
+            if (isset($this->_aLevel2MessageMap[$aMembership['id']]))
+                $sLangKey = $this->_aLevel2MessageMap[$aMembership['id']];
+
+            $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_NOT_ALLOWED;
+            $aResult[CHECK_ACTION_MESSAGE] = _t_ext($sLangKey, $aLangFileParams);
+            return $aResult;
+        }
+
+        /**
+         * Check fixed period limitations if present (also for non-members)
+         */
+        if($aAction['allowed_period_start'] && time() < $aAction['allowed_period_start']) {
+            $aLangFileParams[CHECK_ACTION_LANG_FILE_BEFORE] = bx_time_js($aAction['allowed_period_start'], BX_FORMAT_DATE_TIME);
+
+            $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_NOT_ALLOWED_BEFORE;
+            $aResult[CHECK_ACTION_MESSAGE] = _t_ext(CHECK_ACTION_MESSAGE_NOT_ALLOWED_BEFORE, $aLangFileParams);
+
+            return $aResult;
+        }
+
+        if($aAction['allowed_period_end'] && time() > $aAction['allowed_period_end']) {
+            $aLangFileParams[CHECK_ACTION_LANG_FILE_AFTER] = bx_time_js($aAction['allowed_period_end'], BX_FORMAT_DATE_TIME);
+
+            $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_NOT_ALLOWED_AFTER;
+            $aResult[CHECK_ACTION_MESSAGE] = _t_ext(CHECK_ACTION_MESSAGE_NOT_ALLOWED_AFTER, $aLangFileParams);
+
+            return $aResult;
+        }
+
+        /**
+         * if non-member, allow action without performing further checks
+         */
+        if ($aMembership['id'] == MEMBERSHIP_ID_NON_MEMBER) {
+            $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_ALLOWED;
+            return $aResult;
+        }
+
+        /**
+         * Check other limitations (for members only)
+         */
+        $iAllowedCnt = (int)$aAction['allowed_count']; ///< Number of allowed actions. Unlimited if not specified or 0
+        $iPeriodLen = (int)$aAction['allowed_period_len']; ///< Period for AllowedCount in hours. If not specified, AllowedCount is treated as total number of actions permitted.
+
+        if($iAllowedCnt > 0) {
+            $aActionTrack = $this->oDb->getActionTrack($iActionId, $iProfileId);
+
+            $iActionsLeft = $bPerformAction ? $iAllowedCnt - 1 : $iAllowedCnt;
+            $iValidSince = time();
+
+            /**
+             * Member is requesting/performing this action for the first time,
+             * and there is no corresponding record in sys_acl_actions_track table.
+             */
+            if(!$aActionTrack) {
+                $this->oDb->insertActionTarck($iActionId, $iProfileId, $iActionsLeft, $iValidSince);
+
+                $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_ALLOWED;
+                return $aResult;
+            }
+
+            /**
+             * Action has been requested/performed at least once at this point and there is a corresponding record in sys_acl_actions_track table
+             *
+             * Action record in sys_acl_actions_track table is out of date.
+             */
+            $iPeriodEnd = (int)$aActionTrack['valid_since'] + $iPeriodLen * 3600; //ValidSince is in seconds, PeriodLen is in hours
+            if($iPeriodLen > 0 && $iPeriodEnd < time()) {
+                $this->oDb->updateActionTrack($iActionId, $iProfileId, $iActionsLeft, $iValidSince);
+
+                $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_ALLOWED;
+                return $aResult;
+            }
+
+            $iActionsLeft = (int)$aActionTrack['actions_left']; ///< Action record is up to date
+
+            /**
+             * Action limit reached for now
+             */
+            if($iActionsLeft <= 0){
+                $aLangFileParams[CHECK_ACTION_LANG_FILE_LIMIT] = $iAllowedCnt;
+                $aLangFileParams[CHECK_ACTION_LANG_FILE_PERIOD] = $iPeriodLen;
+                $aLangFileParams[CHECK_ACTION_LANG_FILE_PERIOD_RESTART_AT] = bx_time_js($iPeriodEnd);
+
+                $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_LIMIT_REACHED;
+                $aResult[CHECK_ACTION_MESSAGE] = '<div class="bx-acl-err-msg">' . _t_ext(CHECK_ACTION_MESSAGE_LIMIT_REACHED, $aLangFileParams) . ($iPeriodLen > 0 ? _t_ext(CHECK_ACTION_MESSAGE_MESSAGE_EVERY_PERIOD, $aLangFileParams) : '') . '.</div>';
+
+                return $aResult;
+            }
+
+            if($bPerformAction) {
+                $iActionsLeft--;
+                $this->oDb->updateActionTrack($iActionId, $iProfileId, $iActionsLeft);
+            }
+        }
+
+        $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_ALLOWED;
+        return $aResult;
+    }
+    
+    /**
+     * Get the number of allowed action
+     *
+     * @param  int     $iProfileId     ID of a profile that is going to perform an action
+     * @param  int     $iActionId      ID of the action itself
+     * @param  boolean $bPerformAction if true, then action information is updated, i.e. action is 'performed'
+     * @return int if the action is countable, or true if it's not countable
+     */
+    function getActionNumberLeft($iProfileId, $iActionId)
+    {
+        $aMembership = $this->getMemberMembershipInfo($iProfileId); // get current profile's membership information
+
+        $aAction = $this->oDb->getAction($aMembership['id'], $iActionId);
+        
+        $iAllowedCnt = (int)$aAction['allowed_count']; ///< Number of allowed actions. Unlimited if not specified or 0
+        
+        if($iAllowedCnt > 0) {
+            $aActionTrack = $this->oDb->getActionTrack($iActionId, $iProfileId);
+        
+            if(!$aActionTrack) 
+                return $iAllowedCnt;
+            
+           return (int)$aActionTrack['actions_left']; 
+        }
+        
+        return true;
+    }
+
+    /**
+     * Get the list of existing memberships
+     *
+     * @param  bool   $bPurchasableOnly if true, fetches only purchasable memberships; 'purchasable' here means that:
+     *                                  - MemLevels.Purchasable = 'yes'
+     *                                  - MemLevels.Active = 'yes'
+     *                                  - there is at least one pricing option for the membership
+     * @return array( membershipID_1 => membershipName_1,  membershipID_2 => membershipName_2, ...) if no such memberships, then just array()
+     */
+    function getMemberships($bPurchasableOnly = false, $bActiveOnly = false, $isTranslate = true, $bFilterOutSystemAutomaticLevels = false)
+    {
+        $sType = 'all_pair';
+        if($bPurchasableOnly)
+            $sType = 'all_active_purchasble_pair';
+        else if($bActiveOnly)
+            $sType = 'all_active_pair';
+
+        $aLevels = array();
+        $this->oDb->getLevels(array('type' => $sType), $aLevels, false);
+        if ($isTranslate)
+            foreach ($aLevels as $k => $s)
+                $aLevels[$k] = _t($s);
+
+        if ($bFilterOutSystemAutomaticLevels) {
+            unset($aLevels[MEMBERSHIP_ID_NON_MEMBER]);
+            unset($aLevels[MEMBERSHIP_ID_ACCOUNT]);
+            unset($aLevels[MEMBERSHIP_ID_UNCONFIRMED]);
+            unset($aLevels[MEMBERSHIP_ID_PENDING]);
+            unset($aLevels[MEMBERSHIP_ID_SUSPENDED]);
+        }
+
+        return $aLevels;
+    }
+
+    function getMembershipsBy($aParams)
+    {
+        $aLevels = array();
+        $this->oDb->getLevels($aParams, $aLevels, false);
+
+        return $aLevels;
+    }
+
+    /**
+     * Get info about a given membership
+     *
+     * @param  int    $iLevelId membership to get info about
+     * @return array(
+     *                         'id'					=>	ID,
+     *                         'name'					=>	name,
+     *                         'icon'					=>	icon,
+     *                         'description'			=>	description,
+     *                         'active'				=>	active,
+     *                         'purchasable'			=>	purchasable,
+     *                         'removable'				=>	removable
+     *                         'quota_size'			=>	quota size,
+     *                         'quota_number'			=>	quota number,
+     *                         'quota_max_file_size'	=>	quota max file size
+     *                         )
+     */
+    function getMembershipInfo($iLevelId)
+    {
+        $aLevel = array();
+        $this->oDb->getLevels(array('type' => 'by_id', 'value' => $iLevelId), $aLevel, false);
+        return $aLevel;
+    }
+
+    /**
+     * Retrieves information about membership for a given profile at a given moment.
+     *
+     * If there are no memberships purchased/assigned to the
+     * given profile or all of them have expired at the given point,
+     * the profile is assumed to be a standard profile, and the function
+     * returns	information about the Standard membership. This will
+     * also happen if a profile wasnt actually registered in the database
+     * at that point - the function will still return info about Standard
+     * membership, not the Non-member one.
+     *
+     * If there is no profile with the given $iProfileId,
+     * the function returns information about the Non-member or Authenticated
+     * predefined membership.
+     *
+     * The Standard, Authenticated and Non-member memberships have their
+     * DateStarts and DateExpires attributes set to NULL.
+     *
+     * @param  int    $iProfileId ID of a profile to get info about
+     * @param  int    $time       specifies the time to use when determining membership; if not specified, the function takes the current time
+     * @return array(
+     *                           'id'			=> membership id,
+     *                           'name'			=> membership name,
+     *                           'date_starts'	=> (UNIX timestamp) date/time purchased,
+     *                           'date_expires'	=> (UNIX timestamp) date/time expires
+     *                           )
+     */
+    function getMemberMembershipInfo($iProfileId, $iTime = 0, $bClearCache = 0)
+    {
+        $aMembershipCurrent = $this->getMemberMembershipInfoCurrent($iProfileId, $iTime, $bClearCache);
+        if (isset($this->_aStandardMemberships[$aMembershipCurrent['id']]))
+            return $aMembershipCurrent;
+
+        $aMembership = $aMembershipCurrent;
+        do {
+            $iDateStarts = $aMembership['date_starts'];
+            $aMembership = $this->getMemberMembershipInfoCurrent($iProfileId, ((int)$iDateStarts < 1 ? 0 : $iDateStarts - 1), $bClearCache);
+        }
+        while($aMembership['id'] == $aMembershipCurrent['id'] && (int)$aMembership['date_starts']);
+
+        $aMembership = $aMembershipCurrent;
+        do {
+            $iDateExpires = $aMembership['date_expires'];
+            $aMembership = $this->getMemberMembershipInfoCurrent($iProfileId, $iDateExpires, $bClearCache);
+        } while($aMembership['id'] == $aMembershipCurrent['id'] && (int)$aMembership['date_expires']);
+
+        $aMembershipCurrent['date_starts'] = $iDateStarts;
+        $aMembershipCurrent['date_expires'] = $iDateExpires;
+
+        return $aMembershipCurrent;
+    }
+
+    /**
+     * Set a membership for a profile
+     *
+     * @param  int     $iProfileId profile that is going to get the membership
+     * @param  int     $iLevelId   membership that is going to be assigned to the profile
+     *                             if $iLevelId == MEMBERSHIP_ID_STANDARD then $days and $bStartsNow parameters are not used,
+     *                             so Standard membership is always set immediately and `forever`
+     * @param  mixed   $mixedPeriod  number of Days to set membership for or an array with 'period'-'period unit' pair. If number or 'period' in pair equal 0, then the membership is set forever
+     * @param  boolean $bStartsNow if true, the membership will start immediately if false, the membership will start after the current membership expires
+     * @return boolean true in case of success, false in case of failure
+     */
+    function setMembership($iProfileId, $iLevelId, $mixedPeriod = 0, $bStartsNow = false, $sTransactionId = '')
+    {
+        $iProfileId = (int)$iProfileId;
+        $iLevelId = (int)$iLevelId;
+        $bStartsNow = $bStartsNow ? true : false;
+
+        if (!$iProfileId)
+            $iProfileId = -1;
+
+        if (empty($sTransactionId))
+            $sTransactionId = 'NULL';
+
+        // check if profile exists
+        if(($sProfileEmail = BxDolProfileQuery::getInstance()->getEmailById($iProfileId)) === false)
+            return false;
+
+        // check if membership exists
+        $aLevel = array();
+        $this->oDb->getLevels(array('type' => 'by_id', 'value' => $iLevelId), $aLevel, false);
+        if(empty($aLevel) || !is_array($aLevel))
+            return false;
+
+        if($iLevelId == MEMBERSHIP_ID_NON_MEMBER)
+            return false;
+
+        $aMembershipCurrent = $this->getMemberMembershipInfo($iProfileId);
+        $aMembershipLatest = $this->getMemberMembershipInfoLatest($iProfileId);
+
+        // setting Standard membership level
+        if ($iLevelId == MEMBERSHIP_ID_STANDARD) {
+            if ($aMembershipCurrent['id'] == MEMBERSHIP_ID_STANDARD)
+                return true;
+
+            // delete present and future memberships
+            $bResult = $this->oDb->deleteLevelByProfileId($iProfileId);
+            if($bResult) {
+                $this->oDb->cleanMemory('BxDolAclQuery::getLevelCurrent' . $iProfileId . time());
+                unset(self::$_aCacheData[$iProfileId . '_0']);
+            }
+
+            return $bResult;
+        }
+
+        if ((is_numeric($mixedPeriod) && (int)$mixedPeriod < 0) || (is_array($mixedPeriod) && (!isset($mixedPeriod['period']) || $mixedPeriod['period'] < 0)))
+            return false;
+
+        /*
+         * Make the membership starts after the latest membership expires or starts immediately 
+         * if latest membership is lifetime membership or immediate start was requested.
+         */
+        $iDateStarts = time();
+        if ($bStartsNow || empty($aMembershipLatest['date_expires'])) {
+            // Delete any profile's membership level and actions traces
+            $this->oDb->deleteLevelByProfileId($iProfileId, true); 
+            $this->oDb->clearActionsTracksForMember($iProfileId);
+            $this->oDb->cleanMemory('BxDolAclQuery::getLevelCurrent' . $iProfileId . time());
+            unset(self::$_aCacheData[$iProfileId . '_0']);
+        }
+        else
+            $iDateStarts = $aMembershipLatest['date_expires'];
+
+        // set lifetime membership if 0 days is used.
+        if(is_numeric($mixedPeriod))
+            $mixedPeriod = array('period' => (int)$mixedPeriod, 'period_unit' => MEMBERSHIP_PERIOD_UNIT_DAY);
+
+        if(!$this->oDb->insertLevelByProfileId($iProfileId, $iLevelId, $iDateStarts, $mixedPeriod, $sTransactionId))
+           return false;
+
+        $bProlong = $iLevelId == $aMembershipCurrent['id'];
+
+        // raise membership alert
+        bx_alert('profile', ($bProlong ? 'prolong' : 'set') . '_membership', '', $iProfileId, [
+            'mlevel'=> $iLevelId, 
+            'period' => $mixedPeriod['period'], 
+            'period_unit' => $mixedPeriod['period_unit'], 
+            'starts_now' => $bStartsNow, 
+            'txn_id' => $sTransactionId
+        ]);
+
+        // audit
+        $aDataForAudit = array();
+        if (!empty($aMembershipCurrent))
+            $aDataForAudit = array('new_membership_level' => _t($aLevel['name']), 'old_membership_level' => _t($aMembershipCurrent['name']));
+        BxDolProfile::getInstance($iProfileId)->doAudit('_sys_audit_action_set_membership', $aDataForAudit);
+
+        $oEmailTemplates = BxDolEmailTemplates::getInstance();
+        // Check if there is a special email template for this membership level
+        $sTemplateName = 't_Mem' . ($bProlong ? 'Prolonged' : 'Changed') . '_' . $aLevel['id'];
+        if ($oEmailTemplates->getTemplate($sTemplateName) === FALSE)
+            $sTemplateName = 't_Mem' . ($bProlong ? 'Prolonged' : 'Changed');
+
+        // Send notification
+        $aTemplate = $oEmailTemplates->parseTemplate($sTemplateName, array('membership_level' => _t($aLevel['name'])), 0, $iProfileId);
+        if ($aTemplate)
+            sendMail($sProfileEmail, $aTemplate['Subject'], $aTemplate['Body']);
+
+        return true;
+    }
+
+    function unsetMembership($iProfileId, $iLevelId, $sTransactionId)
+    {
+    	return $this->oDb->deleteLevelBy(array(
+    		'IDMember' => $iProfileId,
+    		'IDLevel' => $iLevelId,
+    		'TransactionID' => $sTransactionId
+    	));
+    }
+
+    /**
+     * get action id by module and name
+     * @param $sAction action name
+     * @param $sModule module name
+     * @param $aActions array of actions from sys_acl_actions table, with default array keys (starting from 0) and text values
+     */
+    function getMembershipActionId($sAction, $sModule)
+    {
+        $this->oDb->getActions(array('type' => 'by_names_and_module', 'value' => $sAction, 'module' => $sModule), $aActions, false);
+        if (count($aActions) > 1)
+            trigger_error('Duplicate action - name:' . $sAction . ', module:' . $sModule, E_USER_ERROR);
+        $aAction = array_pop($aActions);
+        return $aAction['id'];
+    }
+
+    function getExpirationLetter($iProfileId, $sLevelName, $iLevelExpireDays )
+    {
+        $iProfileId = (int)$iProfileId;
+
+        if(!$iProfileId)
+            return false;
+
+        $oProfileQuery = BxDolProfileQuery::getInstance();
+        $sProfileEmail = $oProfileQuery->getEmailById($iProfileId);
+
+        $aPlus = array(
+            'membership_name' => _t($sLevelName),
+            'expire_days' => $iLevelExpireDays,
+            'page_url' => BxDolRequest::serviceExists('bx_acl', 'get_view_url') ? BxDolService::call('bx_acl', 'get_view_url') : '#'
+        );
+
+        $aTemplate = BxDolEmailTemplates::getInstance()->parseTemplate('t_MemExpiration', $aPlus, 0, $iProfileId);
+
+        $iResult = $aTemplate && sendMail($sProfileEmail, $aTemplate['Subject'], $aTemplate['Body'], $iProfileId, $aPlus);
+        return !empty($iResult);
+    }
+
+    /**
+     * clear expired membership levels
+     */
+    public function maintenance()
+    {
+        return $this->oDb->maintenance();
+    }
+
+    protected function getMemberMembershipInfoCurrent($iProfileId, $iTime = 0, $bClearCache = 0)
+    {
+        $sKey = $iProfileId . '_' . $iTime;
+        if ($bClearCache && isset(self::$_aCacheData[$sKey]))
+            unset(self::$_aCacheData[$sKey]);
+        elseif (array_key_exists($sKey, self::$_aCacheData) && !defined('BX_DOL_INSTALL') && !defined('BX_DOL_CRON_EXECUTE'))
+            return self::$_aCacheData[$sKey];
+
+        $aMemLevel = false;
+
+        do {
+            // get profile status
+            $oProfile = BxDolProfile::getInstance($iProfileId);
+            $aProfileInfo = $oProfile ? $oProfile->getInfo() : false;
+            $sProfileStatus = $aProfileInfo ? $aProfileInfo['status'] : false;
+            $sProfileType = $aProfileInfo ? $aProfileInfo['type'] : false;
+
+            // account profile
+            if($sProfileType == 'system') {
+                $aMemLevel = $this->oDb->getLevelByIdCached(MEMBERSHIP_ID_ACCOUNT);
+                if (!$aMemLevel)
+                    trigger_error ('Standard member level is missing: MEMBERSHIP_ID_ACCOUNT', E_USER_ERROR);
+                break;
+            }
+
+            // check if account is unconfirmed, every account's profile is unconfirmed if account is unconfirmed
+            $oAccount = $aProfileInfo ? BxDolAccount::getInstance($aProfileInfo['account_id']) : false;
+            if ($oAccount && !$oAccount->isConfirmed()) {
+                $aMemLevel = $this->oDb->getLevelByIdCached(MEMBERSHIP_ID_UNCONFIRMED);
+                if (!$aMemLevel)
+                    trigger_error ('Standard member level is missing: MEMBERSHIP_ID_UNCONFIRMED', E_USER_ERROR);
+                break;
+            }
+            
+            // profile is not active, so return standard memberships according to profile status        
+            if (false === $sProfileStatus || BX_PROFILE_STATUS_ACTIVE != $sProfileStatus) {                
+                if (!isset($this->_aProfileStatus2LevelMap[$sProfileStatus]))
+                    $iLevelId = MEMBERSHIP_ID_NON_MEMBER; // if there is no profile status - then it isn't member
+                else
+                    $iLevelId = $this->_aProfileStatus2LevelMap[$sProfileStatus]; // get member level id which associated with every non-active status
+
+                $aMemLevel = $this->oDb->getLevelByIdCached($iLevelId);
+
+                if (!$aMemLevel)
+                    trigger_error ('Standard member level is missing: ' . $iLevelId, E_USER_ERROR);
+                break;
+            }
+
+            // profile is active get memebr level from profile
+            $aMemLevel = $this->oDb->getLevelCurrent((int)$iProfileId, $iTime);
+
+            // There are no purchased/assigned memberships for the profile or all of them have expired.
+            // In this case the profile is assumed to have Standard membership.
+            if (!$aMemLevel || is_null($aMemLevel['id'])) {
+                $aMemLevel = $this->oDb->getLevelByIdCached(MEMBERSHIP_ID_STANDARD);
+                if (!$aMemLevel)
+                    trigger_error ('Standard member level is missing: ' . MEMBERSHIP_ID_NON_MEMBER, E_USER_ERROR);
+                break;
+            }
+        } 
+        while (0);
+
+        return (self::$_aCacheData[$sKey] = $aMemLevel);
+    }
+
+    protected function getMemberMembershipInfoLatest($iProfileId, $iTime = 0, $bClearCache = 0)
+    {
+        $aMembershipCurrent = $this->getMemberMembershipInfoCurrent($iProfileId, $iTime, $bClearCache);
+        if (isset($this->_aStandardMemberships[$aMembershipCurrent['id']]))
+            return $aMembershipCurrent;
+
+        $aMembership = $aMembershipCurrent;
+        while ($aMembership['id'] != MEMBERSHIP_ID_STANDARD) {
+            $aMembershipLast = $aMembership;
+            if(!isset($aMembership['date_expires']) || (int)$aMembership['date_expires'] == 0)
+                break;
+
+            $aMembership = $this->getMemberMembershipInfoCurrent($iProfileId, $aMembership['date_expires'], $bClearCache);
+        }
+
+        return $aMembershipLast;
+    }
+
+    public function onProfileDelete ($iProfileId)
+    {
+        return $this->oDb->deleteLevelByProfileId($iProfileId, true);
+    }
+}
+
+function checkAction($iProfileId, $iActionId, $bPerformAction = false)
+{
+    return BxDolAcl::getInstance()->checkAction($iProfileId, $iActionId, $bPerformAction);
+}
+
+function checkActionModule($iProfileId, $sActionName, $sModuleName, $bPerformAction = false)
+{
+    $oACL = BxDolAcl::getInstance();
+
+    $iActionId = $oACL->getMembershipActionId($sActionName, $sModuleName);
+    if (!$iActionId)
+        bx_trigger_error("Unknown action: '$sActionName' in module '$sModuleName'", 1);
+
+    return $oACL->checkAction($iProfileId, $iActionId, $bPerformAction);
+}
+
+function getActionNumberLeftModule($iProfileId, $sActionName, $sModuleName)
+{
+    $oACL = BxDolAcl::getInstance();
+
+    $iActionId = $oACL->getMembershipActionId($sActionName, $sModuleName);
+    if (!$iActionId)
+        bx_trigger_error("Unknown action: '$sActionName' in module '$sModuleName'", 1);
+
+    return $oACL->getActionNumberLeft($iProfileId, $iActionId);
+}
+
+/** @} */

+ 33 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolBan.php

@@ -0,0 +1,33 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolBan extends BxDolConnection
+{
+    protected function __construct($aObject)
+    {
+        parent::__construct($aObject);
+    }  
+
+    /**
+     * Add new connection.
+     * @param $iInitiator initiator of the connection, in most cases some profile id
+     * @param $iContent content to make connection to, in most cases some content id, or other profile id in case of friends
+     * @return true - if connection was added, false - if connection already exists or error occured
+     */
+    public function addConnection($iInitiator, $iContent, $aParams = [])
+    {
+        $bResult = parent::addConnection($iInitiator, $iContent, $aParams);
+        if($bResult)
+            $this->_oQuery->updateConnection($iInitiator, $iContent, [
+                'module' => !empty($aParams['module']) ? $aParams['module'] : BxDolProfile::getInstance($iInitiator)->getModule(),
+            ]);
+
+        return $bResult;
+    }
+}

+ 959 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolConnection.php

@@ -0,0 +1,959 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Default limit for connections list in Counter.
+ */
+define('BX_CONNECTIONS_LIST_COUNTER', 5);
+
+/**
+ * Default limit for connections lists
+ */
+define('BX_CONNECTIONS_LIST_LIMIT', 1000);
+
+/**
+ * No limit for connections lists. 
+ * Is needed for Total Number calculation.
+ */
+define('BX_CONNECTIONS_LIST_NO_LIMIT', -1);
+
+/**
+ * Connections order: no order
+ */
+define('BX_CONNECTIONS_ORDER_NONE', 0);
+
+/**
+ * Connections order: by addded time, asceding
+ */
+define('BX_CONNECTIONS_ORDER_ADDED_ASC', 1);
+
+/**
+ * Connections order: by addded time, desceding
+ */
+define('BX_CONNECTIONS_ORDER_ADDED_DESC', 2);
+
+/**
+ * Connection type: one-way
+ */
+define('BX_CONNECTIONS_TYPE_ONE_WAY', 'one-way');
+
+/**
+ * Connection type: mutual
+ */
+define('BX_CONNECTIONS_TYPE_MUTUAL', 'mutual');
+
+/**
+ * Connections content type: content
+ */
+define('BX_CONNECTIONS_CONTENT_TYPE_CONTENT', 'content');
+
+/**
+ * Connections content type: initiators
+ */
+define('BX_CONNECTIONS_CONTENT_TYPE_INITIATORS', 'initiators');
+
+/**
+ * Connections content type: common
+ */
+define('BX_CONNECTIONS_CONTENT_TYPE_COMMON', 'common');
+
+/**
+ * Connection is usefull when you need to organize some sorts of connections between different content,
+ * for example: friends, contacts, favorites, block lists, subscriptions, etc.
+ *
+ * Two types of connections are supported one way connections (block list, favourites) and mutual (friends).
+ *
+ * For automatic handling of connections (like, add/remove connection in frontend) refer to JS function: @see bx_conn_action()
+ *
+ * @section connection_create Creating the Connection object:
+ *
+ * Step 1:
+ * Add record to 'sys_objects_connection' table:
+ * - object: name of the connection object, in the format: vendor prefix, underscore, module prefix, underscore, internal identifier or nothing;
+ *           for example: bx_blogs_favorites - favorite blogs for users in blogs module.
+ * - table: table name with connections, see step 2
+ * - type: 'one-way' or 'mutual'
+ * - override_class_name: user defined class name which is derived from one of base classes.
+ * - override_class_file: the location of the user defined class, leave it empty if class is located in system folders.
+ *
+ * Step 2:
+ * Create table for connections:
+ * @code
+ * CREATE TABLE `my_sample_connections` (
+ *   `id` int(11) NOT NULL AUTO_INCREMENT,
+ *   `initiator` int(11) NOT NULL,
+ *   `content` int(11) NOT NULL,
+ *   `mutual` tinyint(4) NOT NULL, -- can re removed for one-way connections
+ *   `added` int(10) unsigned NOT NULL,
+ *   PRIMARY KEY (`id`),
+ *   UNIQUE KEY `initiator` (`initiator`,`content`),
+ *   KEY `content` (`content`)
+ * )
+ * @endcode
+ *
+ *
+ * @section example Example of usage
+ *
+ * Check if two profiles are friends:
+ * @code
+ *   $oConnectionFriends = BxDolConnection::getObjectInstance('bx_profiles_friends'); // get friends connections object
+ *   if ($oConnectionFriends) // check if connections is available for using
+ *      echo $oConnectionFriends->isConnected (100, 200, true) ? "100 and 200 are friends" : "100 and 200 aren't friends"; // check if profiles with IDs 100 and 200 have mutual connections
+ * @endcode
+ *
+ * Get mutual content IDs (friends IDs)
+ * @code
+ *   $oConnectionFriends = BxDolConnection::getObjectInstance('bx_profiles_friends'); // get friends connections object
+ *   if ($oConnectionFriends) // check if connections is available for using
+ *       print_r($oConnection->getConnectedContent(100, 1)); // print array of friends IDs of 100's profile
+ * @endcode
+ *
+ */
+class BxDolConnection extends BxDolFactory implements iBxDolFactoryObject
+{
+    protected $_sObject;
+    protected $_aObject;
+    protected $_oQuery;
+    protected $_sType;
+
+    /**
+     * Constructor
+     * @param $aObject array of connection options
+     */
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+
+        $this->_sObject = $aObject['object'];
+        $this->_aObject = $aObject;
+        $this->_aObject['per_page_default'] = 20;
+
+        $this->_sType = $aObject['type'];
+
+        $this->_oQuery = new BxDolConnectionQuery($aObject);
+    }
+
+    /**
+     * Get connection object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    static public function getObjectInstance($sObject)
+    {
+        if (!$sObject)
+            return false;
+
+        if (isset($GLOBALS['bxDolClasses']['BxTemplConnection!'.$sObject]))
+            return $GLOBALS['bxDolClasses']['BxTemplConnection!'.$sObject];
+
+        $aObject = BxDolConnectionQuery::getConnectionObject($sObject);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        $sClass = empty($aObject['override_class_name']) ? 'BxTemplConnection' : $aObject['override_class_name'];
+        if (!empty($aObject['override_class_file']))
+            require_once(BX_DIRECTORY_PATH_ROOT . $aObject['override_class_file']);
+
+        $o = new $sClass($aObject);
+        $o->init();
+
+        return ($GLOBALS['bxDolClasses']['BxTemplConnection!'.$sObject] = $o);
+    }
+
+    /**
+     * Init something here if it's needed.
+     */
+    public function init()
+    {
+    }
+
+    /**
+     * Get connection type.
+     * return BX_CONNECTIONS_TYPE_ONE_WAY or BX_CONNECTIONS_TYPE_MUTUAL
+     */ 
+    public function getType()
+    {
+        return $this->_sType;
+    }
+
+    /**
+     * Get connection table.
+     * return string with table name.
+     */ 
+    public function getTable()
+    {
+        return $this->_aObject['table'];
+    }
+
+    /**
+     * Checks whether connection's Initiator is profile or not.
+     * return boolean.
+     */
+    public function isProfileInitiator()
+    {
+        return (int)$this->_aObject['profile_initiator'] != 0;
+    }
+
+    /**
+     * Checks whether connection's Content is profile or not.
+     * return boolean.
+     */
+    public function isProfileContent()
+    {
+        return (int)$this->_aObject['profile_content'] != 0;
+    }
+
+    /**
+     * Check whether connection between Initiator and Content can be established.
+     */
+    public function checkAllowedConnect ($iInitiator, $iContent, $isPerformAction = false, $isMutual = false, $isInvertResult = false, $isSwap = false, $isCheckExists = true)
+    {
+        if(!$iInitiator || !$iContent || $iInitiator == $iContent)
+            return _t('_sys_txt_access_denied');
+
+        $oInitiator = BxDolProfile::getInstance($iInitiator);
+        $oContent = BxDolProfile::getInstance($iContent);
+        if(!$oInitiator || !$oContent)
+            return _t('_sys_txt_access_denied');
+
+        // check ACL
+        if(($mixedResult = $this->_checkAllowedConnectInitiator($oInitiator, $isPerformAction)) !== CHECK_ACTION_RESULT_ALLOWED)
+            return $mixedResult;
+
+        // check content's visibility
+        if(!$this->isConnected($iContent, $iInitiator) && ($mixedResult = $this->_checkAllowedConnectContent($oContent)) !== CHECK_ACTION_RESULT_ALLOWED)
+            return $mixedResult;
+
+        if(!$isCheckExists)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        if($isSwap)
+            $isConnected = $this->isConnected($iContent, $iInitiator, $isMutual);
+        else
+            $isConnected = $this->isConnected($iInitiator, $iContent, $isMutual);
+
+        if($isInvertResult)
+            $isConnected = !$isConnected;
+
+        return $isConnected ? _t('_sys_txt_access_denied') : CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    protected function _checkAllowedConnectInitiator ($oInitiator, $isPerformAction = false)
+    {
+        $aCheck = checkActionModule($oInitiator->id(), 'connect', 'system', $isPerformAction);
+        if($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    protected function _checkAllowedConnectContent ($oContent)
+    {
+        return $oContent->checkAllowedProfileView();
+    }
+
+    /**
+     * Add new connection.
+     * @param $iContent content to make connection to, in most cases some content id, or other profile id in case of friends
+     * @return array
+     */
+    public function actionAdd ($iContent = 0, $iInitiator = false)
+    {
+        if (!$iContent)
+            $iContent = bx_process_input($_POST['id'], BX_DATA_INT);
+
+        return $this->_action ($iInitiator ? $iInitiator : bx_get_logged_profile_id(), $iContent, 'addConnection', '_sys_conn_err_connection_already_exists', true);
+    }
+
+    /**
+     * Remove connection. This method is wrapper for @see removeConnection to be called from @see conn.php upon AJAX request to this file.
+     * @param $iContent content to make connection to, in most cases some content id, or other profile id in case of friends
+     * @return array
+     */
+    public function actionRemove ($iContent = 0, $iInitiator = false)
+    {
+        if (!$iContent)
+            $iContent = bx_process_input($_POST['id'], BX_DATA_INT);
+
+        if ($iContent != bx_get_logged_profile_id() && BX_CONNECTIONS_TYPE_MUTUAL == $this->_aObject['type']) {
+            $a = $this->actionReject($iContent, $iInitiator);
+            if (false == $a['err'])
+                return $a;
+        }
+
+        return $this->_action ($iInitiator ? $iInitiator : bx_get_logged_profile_id(), $iContent, 'removeConnection', '_sys_conn_err_connection_does_not_exists', false, true);
+    }
+
+    /**
+     * Reject connection request. This method is wrapper for @see removeConnection to be called from @see conn.php upon AJAX request to this file.
+     * @param $iContent content to make connection to, in most cases some content id, or other profile id in case of friends
+     * @return array
+     */
+    public function actionReject ($iContent = 0, $iInitiator = false)
+    {
+        if (!$iContent)
+            $iContent = bx_process_input($_POST['id'], BX_DATA_INT);
+
+        return $this->_action($iContent, $iInitiator ? $iInitiator : bx_get_logged_profile_id(), 'removeConnection', '_sys_conn_err_connection_does_not_exists', false, true);
+    }
+
+    protected function _action ($iInitiator, $iContent, $sMethod, $sErrorKey, $isMutual = false, $isInvert = false)
+    {
+        bx_import('BxDolLanguages');
+
+        if(!$iContent || !$iInitiator)
+            return ['err' => true, 'msg' => _t('_sys_conn_err_input_data_is_not_defined')];
+
+        $sMethodCheck = 'checkAllowed' . bx_gen_method_name($sMethod);
+        if(($mixedResult = $this->{method_exists($this, $sMethodCheck) ? $sMethodCheck : 'checkAllowedConnect'}($iInitiator, $iContent, false, false, $isInvert)) !== CHECK_ACTION_RESULT_ALLOWED)
+            return ['err' => true, 'msg' => $mixedResult];
+
+        if (!$this->$sMethod((int)$iInitiator, (int)$iContent)) {
+            if ($isMutual && BX_CONNECTIONS_TYPE_MUTUAL == $this->_sType && $this->isConnected((int)$iInitiator, (int)$iContent, false) && !$this->isConnected((int)$iInitiator, (int)$iContent, true))
+                return ['err' => true, 'msg' => _t('_sys_conn_err_connection_is_awaiting_confirmation')];
+
+            return ['err' => true, 'msg' => _t($sErrorKey)];
+        }
+
+        return ['err' => false, 'msg' => _t('_sys_conn_msg_success')];
+    }
+
+    public function outputActionResult ($mixed, $sFormat = 'json')
+    {
+        switch ($sFormat) {
+            case 'html':
+                echo $mixed;
+                break;
+                
+            case 'json':
+            default:
+                header('Content-Type: application/json; charset=utf-8');
+                echo json_encode($mixed);     
+        }
+        exit;
+    }
+
+    /**
+     * Add new connection.
+     * @param $iInitiator initiator of the connection, in most cases some profile id
+     * @param $iContent content to make connection to, in most cases some content id, or other profile id in case of friends
+     * @return true - if connection was added, false - if connection already exists or error occured
+     */
+    public function addConnection ($iInitiator, $iContent, $aParams = array())
+    {
+        $iMutual = 0;
+        $iInitiator = (int)$iInitiator;
+        $iContent = (int)$iContent;
+
+        $aAlertExtras = array(
+            'initiator' => &$iInitiator,
+            'content' => &$iContent,
+            'mutual' => &$iMutual,
+            'object' => $this,
+        );
+        if(!empty($aParams['alert_extras']) && is_array($aParams['alert_extras']))
+            $aAlertExtras = array_merge($aAlertExtras, $aParams['alert_extras']);
+        
+        bx_alert($this->_sObject, 'connection_before_add', 0, bx_get_logged_profile_id(), $aAlertExtras);
+        
+        if (!$this->_oQuery->addConnection((int)$iInitiator, (int)$iContent, $iMutual))
+            return false;
+        
+        bx_alert($this->_sObject, 'connection_added', 0, bx_get_logged_profile_id(), $aAlertExtras);
+
+        $this->onAdded($iInitiator, $iContent, $iMutual);
+
+        return true;
+    }
+
+    public function onAdded($iInitiator, $iContent, $iMutual)
+    {
+        $this->checkAllowedConnect ($iInitiator, $iContent, true, $iMutual, false);
+
+        /**
+         * Update recommendations.
+         */
+        $bMutual = false;
+        if($this->_aObject['type'] == BX_CONNECTIONS_TYPE_ONE_WAY || ($bMutual = ($this->_aObject['type'] == BX_CONNECTIONS_TYPE_MUTUAL && $iMutual))) {
+            $oProfileQuery = BxDolProfileQuery::getInstance();
+
+            if($this->_aObject['profile_initiator']) {
+                $aInitiator = $oProfileQuery->getInfoById($iInitiator);
+                if(bx_srv($aInitiator['type'], 'act_as_profile'))
+                    BxDolRecommendation::updateData($iInitiator);
+            }
+
+            if($bMutual && $this->_aObject['profile_content']) {
+                $aContent = $oProfileQuery->getInfoById($iContent);
+                if(bx_srv($aContent['type'], 'act_as_profile'))
+                    BxDolRecommendation::updateData($iContent);
+            }
+        }
+    }
+
+    /**
+     * Remove connection.
+     * @param $iInitiator initiator of the connection
+     * @param $iContent connected content or other profile id in case of friends
+     * @return true - if connection was removed, false - if connection isn't exist or error occured
+     */
+    public function removeConnection ($iInitiator, $iContent)
+    {
+        if (!($aConnection = $this->_oQuery->getConnection((int)$iInitiator, (int)$iContent))) // connection doesn't exist
+            return false;
+
+        if (!$this->_oQuery->removeConnection((int)$iInitiator, (int)$iContent))
+            return false;
+
+        bx_alert($this->_sObject, 'connection_removed', 0, bx_get_logged_profile_id(), array(
+            'initiator' => (int)$iInitiator,
+            'content' => (int)$iContent,
+            'mutual' => isset($aConnection['mutual']) ? $aConnection['mutual'] : 0,
+            'object' => $this,
+        ));
+
+        $this->onRemoved($iInitiator, $iContent);
+
+        return true;
+    }
+
+    public function onRemoved($iInitiator, $iContent) {}
+
+    /**
+     * Compound function, which calls getCommonContent, getConnectedContent or getConnectedInitiators depending on $sContentType
+     * @param $sContentType content type to get BX_CONNECTIONS_CONTENT_TYPE_CONTENT, BX_CONNECTIONS_CONTENT_TYPE_INITIATORS or BX_CONNECTIONS_CONTENT_TYPE_COMMON
+     * @param $iId1 one content or initiator
+     * @param $iId2 second content or initiator only in case of BX_CONNECTIONS_CONTENT_TYPE_COMMON content type
+     * @param $isMutual get mutual connections only
+     * @return array of available connections
+     */
+    public function getConnectionsAsArray ($sContentType, $iId1, $iId2, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        if (BX_CONNECTIONS_CONTENT_TYPE_COMMON == $sContentType)
+            return $this->getCommonContent($iId1, $iId2, $isMutual, $iStart, $iLimit, $iOrder);
+
+        if (BX_CONNECTIONS_CONTENT_TYPE_INITIATORS == $sContentType)
+            $sMethod = 'getConnectedInitiators';
+        else
+            $sMethod = 'getConnectedContent';
+
+        return $this->$sMethod($iId1, $isMutual, $iStart, $iLimit, $iOrder);
+    }
+
+    /**
+     * Get common content IDs between two initiators
+     * @param $iInitiator1 one initiator
+     * @param $iInitiator2 second initiator
+     * @param $isMutual get mutual connections only
+     * @return array of available connections
+     */
+    public function getCommonContent ($iInitiator1, $iInitiator2, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        return $this->_oQuery->getCommonContent($iInitiator1, $iInitiator2, $isMutual, $iStart, $iLimit, $iOrder);
+    }
+    
+    /**
+     * Get common content count between two initiators
+     * @param $iInitiator1 one initiator
+     * @param $iInitiator2 second initiator
+     * @param $isMutual get mutual connections only
+     * @return number of connections
+     */
+    public function getCommonContentCount ($iInitiator1, $iInitiator2, $isMutual = false)
+    {
+        return $this->_oQuery->getCommonContentCount($iInitiator1, $iInitiator2, $isMutual);
+    }
+
+    /**
+     * Get connected content count
+     * @param $iInitiator initiator of the connection
+     * @param $isMutual get mutual connections only
+     * @return number of connections
+     */
+    public function getConnectedContentCount ($iInitiator, $isMutual = false, $iFromDate = 0)
+    {
+        return $this->_oQuery->getConnectedContentCount($iInitiator, $isMutual, $iFromDate);
+    }
+
+    /**
+     * Get connected initiators count
+     * @param $iContent content of the connection
+     * @param $isMutual get mutual connections only
+     * @return number of connections
+     */
+    public function getConnectedInitiatorsCount ($iContent, $isMutual = false)
+    {
+        return $this->_oQuery->getConnectedInitiatorsCount($iContent, $isMutual);
+    }
+
+    /**
+     * Get connected content IDs
+     * @param $iInitiator initiator of the connection
+     * @param $isMutual get mutual connections only
+     * @return array of available connections
+     */
+    public function getConnectedContent ($iInitiator, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        return $this->_oQuery->getConnectedContent($iInitiator, $isMutual, $iStart, $iLimit, $iOrder);
+    }
+    
+    /**
+     * Get connected content IDs for specified type
+     * @param $iInitiator initiator of the connection
+     * @param $mixedType type of content or an array of types
+     * @param $isMutual get mutual connections only
+     * @return array of available connections
+     */
+    public function getConnectedContentByType ($iInitiator, $mixedType, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        return $this->_oQuery->getConnectedContentByType($iInitiator, $mixedType, $isMutual, $iStart, $iLimit, $iOrder);
+    }
+
+    /**
+     * Get connected initiators IDs
+     * @param $iContent content of the connection
+     * @param $isMutual get mutual connections only
+     * @return array of available connections
+     */
+    public function getConnectedInitiators ($iContent, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        return $this->_oQuery->getConnectedInitiators($iContent, $isMutual, $iStart, $iLimit, $iOrder);
+    }
+
+    /**
+     * Get connected initiators IDs
+     * @param $iContent content of the connection
+     * @param $mixedType type of content or an array of types
+     * @param $isMutual get mutual connections only
+     * @return array of available connections
+     */
+    public function getConnectedInitiatorsByType ($iContent, $mixedType, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        return $this->_oQuery->getConnectedInitiatorsByType($iContent, $mixedType, $isMutual, $iStart, $iLimit, $iOrder);
+    }
+
+    /**
+     * Similar to getConnectionsAsArray, but for getCommonContentAsSQLParts, getConnectedContentAsSQLParts or getConnectedInitiatorsAsSQLParts methods
+     * @see getConnectionsAsArray
+     */
+    public function getConnectionsAsSQLParts ($sContentType, $sContentTable, $sContentField, $iId1, $iId2, $isMutual = false)
+    {
+        if (BX_CONNECTIONS_CONTENT_TYPE_COMMON == $sContentType)
+            return $this->getCommonContentAsSQLParts($sContentTable, $sContentField, $iId1, $iId2, $isMutual);
+
+        if (BX_CONNECTIONS_CONTENT_TYPE_INITIATORS == $sContentType)
+            $sMethod = 'getConnectedInitiatorsAsSQLParts';
+        else
+            $sMethod = 'getConnectedContentAsSQLParts';
+
+        return $this->$sMethod($sContentTable, $sContentField, $iId1, $isMutual);
+    }
+
+    /**
+     * Get necessary parts of SQL query to use connections in other queries
+     * @param $sContentTable content table or alias
+     * @param $sContentField content table field or field alias
+     * @param $iInitiator initiator of the connection
+     * @param $isMutual get mutual connections only
+     * @return array of SQL string parts, for now 'join' part only is returned
+     */
+    public function getCommonContentAsSQLParts ($sContentTable, $sContentField, $iInitiator1, $iInitiator2, $isMutual = false)
+    {
+        return $this->_oQuery->getCommonContentSQLParts($sContentTable, $sContentField, $iInitiator1, $iInitiator2, $isMutual);
+    }
+
+    /**
+     * Get necessary parts of SQL query to use connections in other queries
+     * @param $sContentTable content table or alias
+     * @param $sContentField content table field or field alias
+     * @param $iInitiator initiator of the connection
+     * @param $isMutual get mutual connections only
+     * @return array of SQL string parts, for now 'join' part only is returned
+     */
+    public function getConnectedContentAsSQLParts ($sContentTable, $sContentField, $iInitiator, $isMutual = false)
+    {
+        return $this->_oQuery->getConnectedContentSQLParts($sContentTable, $sContentField, $iInitiator, $isMutual);
+    }
+    
+    public function getConnectedContentAsSQLPartsExt ($sContentTable, $sContentField, $iInitiator, $isMutual = false)
+    {
+        return $this->_oQuery->getConnectedContentSQLPartsExt($sContentTable, $sContentField, $iInitiator, $isMutual);
+    }
+
+	/**
+     * Get necessary parts of SQL query to use connections in other queries
+     * @param $sContentTable content table or alias
+     * @param $sContentField content table field or field alias
+     * @param $sInitiatorTable initiator table or alias
+     * @param $sInitiatorField initiator table field or field alias
+     * @param $iInitiator initiator of the connection
+     * @param $isMutual get mutual connections only
+     * @return array of SQL string parts, for now 'join' part only is returned
+     */
+    public function getConnectedContentAsSQLPartsMultiple ($sContentTable, $sContentField, $sInitiatorTable, $sInitiatorField, $isMutual = false)
+    {
+        return $this->_oQuery->getConnectedContentSQLPartsMultiple($sContentTable, $sContentField, $sInitiatorTable, $sInitiatorField, $isMutual);
+    }
+
+    /**
+     * Get necessary parts of SQL query to use connections in other queries
+     * @param $sInitiatorTable initiator table or alias
+     * @param $sInitiatorField initiator table field or field alias
+     * @param $iContent content of the connection
+     * @param $isMutual get mutual connections only
+     * @return array of SQL string parts, for now 'join' part only is returned
+     */
+    public function getConnectedInitiatorsAsSQLParts ($sInitiatorTable, $sInitiatorField, $iContent, $isMutual = false)
+    {
+        return $this->_oQuery->getConnectedInitiatorsSQLParts($sInitiatorTable, $sInitiatorField, $iContent, $isMutual);
+    }
+
+	/**
+     * Get necessary parts of SQL query to use connections in other queries
+     * @param $sInitiatorTable initiator table or alias
+     * @param $sInitiatorField initiator table field or field alias
+     * @param $sContentTable content table or alias
+     * @param $sContentField content table field or field alias
+     * @param $isMutual get mutual connections only
+     * @return array of SQL string parts, for now 'join' part only is returned
+     */
+    public function getConnectedInitiatorsAsSQLPartsMultiple ($sInitiatorTable, $sInitiatorField, $sContentTable, $sContentField, $isMutual = false)
+    {
+        return $this->_oQuery->getConnectedInitiatorsSQLPartsMultiple ($sInitiatorTable, $sInitiatorField, $sContentTable, $sContentField, $isMutual);
+    }
+
+    /**
+     * Similar to getConnectionsAsArray, but for getCommonContentAsCondition, getConnectedContentAsCondition or getConnectedInitiatorsAsCondition methods
+     * @see getConnectionsAsArray
+     */
+    public function getConnectionsAsCondition ($sContentType, $sContentField, $iId1, $iId2, $isMutual = false)
+    {
+        if (BX_CONNECTIONS_CONTENT_TYPE_COMMON == $sContentType)
+            return $this->getCommonContentAsCondition($sContentField, $iId1, $iId2, $isMutual);
+
+        if (BX_CONNECTIONS_CONTENT_TYPE_INITIATORS == $sContentType)
+            $sMethod = 'getConnectedInitiatorsAsCondition';
+        else
+            $sMethod = 'getConnectedContentAsCondition';
+
+        return $this->$sMethod($sContentField, $iId1, $isMutual);
+    }
+
+    /**
+     * Get necessary condition array to use connections in search classes
+     * @param $sContentField content table field name
+     * @param $iInitiator initiator of the connection
+     * @param $iMutual get mutual connections only
+     * @return array of conditions, for now with 'restriction' and 'join' parts
+     */
+    public function getCommonContentAsCondition ($sContentField, $iInitiator1, $iInitiator2, $iMutual = false)
+    {
+        return array(
+
+            'restriction' => array (
+                'connections_' . $this->_sObject => array(
+                    'value' => $iInitiator1,
+                    'field' => 'initiator',
+                    'operator' => '=',
+                    'table' => 'c',
+                ),
+                'connections_mutual_' . $this->_sObject => array(
+                    'value' => $iMutual,
+                    'field' => 'mutual',
+                    'operator' => '=',
+                    'table' => 'c',
+                ),
+                'connections2_' . $this->_sObject => array(
+                    'value' => $iInitiator2,
+                    'field' => 'initiator',
+                    'operator' => '=',
+                    'table' => 'c2',
+                ),
+                'connections2_mutual_' . $this->_sObject => array(
+                    'value' => $iMutual,
+                    'field' => 'mutual',
+                    'operator' => '=',
+                    'table' => 'c2',
+                ),
+            ),
+
+            'join' => array (
+                'connections_' . $this->_sObject => array(
+                    'type' => 'INNER',
+                    'table' => $this->_aObject['table'],
+                    'table_alias' => 'c',
+                    'mainField' => $sContentField,
+                    'onField' => 'content',
+                    'joinFields' => array(),
+                ),
+                'connections2_' . $this->_sObject => array(
+                    'type' => 'INNER',
+                    'table' => $this->_aObject['table'],
+                    'table_alias' => 'c2',
+                    'mainTable' => 'c',
+                    'mainField' => 'content',
+                    'onField' => 'content',
+                    'joinFields' => array(),
+                ),
+            ),
+
+        );
+    }
+
+    /**
+     * Get necessary condition array to use connections in search classes
+     * @param $sContentField content table field name
+     * @param $iInitiator initiator of the connection
+     * @param $iMutual get mutual connections only
+     * @return array of conditions, for now with 'restriction' and 'join' parts
+     */
+    public function getConnectedContentAsCondition ($sContentField, $iInitiator, $iMutual = false)
+    {
+        $sOperation = '=';
+        if(is_array($iInitiator))
+            $sOperation = 'in';
+
+        return array(
+
+            'restriction' => array (
+                'connections_' . $this->_sObject => array(
+                    'value' => $iInitiator,
+                    'field' => 'initiator',
+                    'operator' => $sOperation,
+                    'table' => $this->_aObject['table'],
+                ),
+                'connections_mutual_' . $this->_sObject => array(
+                    'value' => $iMutual,
+                    'field' => 'mutual',
+                    'operator' => '=',
+                    'table' => $this->_aObject['table'],
+                ),
+            ),
+
+            'join' => array (
+                'connections_' . $this->_sObject => array(
+                    'type' => 'INNER',
+                    'table' => $this->_aObject['table'],
+                    'mainField' => $sContentField,
+                    'onField' => 'content',
+                    'joinFields' => array(),//'initiator'),
+                ),
+            ),
+
+        );
+    }
+
+    /**
+     * Get necessary condition array to use connections in search classes
+     * @param $sContentField content table field name
+     * @param $iInitiator initiator of the connection
+     * @param $iMutual get mutual connections only
+     * @return array of conditions, for now with 'restriction' and 'join' parts
+     */
+    public function getConnectedInitiatorsAsCondition ($sContentField, $iContent, $iMutual = false)
+    {
+        $sOperation = '=';
+        if(is_array($iContent))
+            $sOperation = 'in';
+
+        return array(
+
+            'restriction' => array (
+                'connections_' . $this->_sObject => array(
+                    'value' => $iContent,
+                    'field' => 'content',
+                    'operator' => $sOperation,
+                    'table' => $this->_aObject['table'],
+                ),
+                'connections_mutual_' . $this->_sObject => array(
+                    'value' => $iMutual,
+                    'field' => 'mutual',
+                    'operator' => '=',
+                    'table' => $this->_aObject['table'],
+                ),
+            ),
+
+            'join' => array (
+                'connections_' . $this->_sObject => array(
+                    'type' => 'INNER',
+                    'table' => $this->_aObject['table'],
+                    'mainField' => $sContentField,
+                    'onField' => 'initiator',
+                    'joinFields' => array(),//'initiator'),
+                ),
+            ),
+
+        );
+    }
+
+    /**
+     * Check if initiator and content are connected.
+     * In case if friends this function in conjunction with isMutual parameter can be used to check pending friend requests.
+     * @param $iInitiator initiator of the connection
+     * @param $iContent connected content or other profile id in case of friends
+     * @return true - if content and initiator are connected or false - in all other cases
+     */
+    public function isConnected ($iInitiator, $iContent, $isMutual = false)
+    {
+        $oConnection = $this->_oQuery->getConnection ($iInitiator, $iContent);
+        if (!$oConnection)
+            return false;
+        return false === $isMutual ? true : (isset($oConnection['mutual']) ? $oConnection['mutual'] : false);
+    }
+
+    /**
+     * Check if initiator and content are connected but connetion is not mutual, for checking pending connection requests.
+     * This method makes sense only when type of connection is mutual.
+     * @param $iInitiator initiator of the connection
+     * @param $iContent connected content or other profile id in case of friends
+     * @return true - if content and initiator are connected but connection is not mutual or false in all other cases
+     */
+    public function isConnectedNotMutual ($iInitiator, $iContent)
+    {
+        $oConnection = $this->_oQuery->getConnection ($iInitiator, $iContent);
+        if (!$oConnection)
+            return false;
+        return $oConnection['mutual'] ? false : true;
+    }
+
+    public function getConnection ($iInitiator, $iContent)
+    {
+        return $this->_oQuery->getConnection($iInitiator, $iContent);
+    }
+
+    public function getConnectionById ($iId)
+    {
+        return $this->_oQuery->getConnectionById($iId);
+    }
+
+    /**
+     * Must be called when some content is deleted which can have connections as 'content' or as 'initiator', to delete any associated data
+     * @param $iId which can be as conetnt ot initiator
+     * @return true if some connections were deleted
+     */
+    public function onDeleteInitiatorAndContent ($iId)
+    {
+        $b = $this->onDeleteInitiator ($iId);
+        $b = $this->onDeleteContent ($iId) || $b;
+        return $b;
+    }
+
+    /**
+     * Must be called when some content is deleted which can have connections as 'initiator', to delete any associated data
+     * @param $iIdInitiator initiator id
+     * @return true if some connections were deleted
+     */
+    public function onDeleteInitiator ($iIdInitiator)
+    {
+        if(!$this->_oQuery->onDelete ($iIdInitiator, 'initiator'))
+            return false;
+
+        bx_alert($this->_sObject, 'connection_removed_all', 0, bx_get_logged_profile_id(), array(
+            'initiator' => (int)$iIdInitiator,
+            'object' => $this,
+        ));
+
+        return true;
+    }
+
+    /**
+     * Must be called when some content is deleted which can have connections as 'content', to delete any associated data
+     * @param $iIdInitiator initiator id
+     * @return true if some connections were deleted
+     */
+    public function onDeleteContent ($iIdContent)
+    {
+        if(!$this->_oQuery->onDelete ($iIdContent, 'content'))
+            return false;
+
+        bx_alert($this->_sObject, 'connection_removed_all', 0, bx_get_logged_profile_id(), array(
+            'content' => (int)$iIdContent,
+            'object' => $this,
+        ));
+
+        return true;
+    }
+
+
+    /**
+     * Must be called when module (which can have connections as 'content' or as 'initiator') is deleted, to delete any associated data.
+     * This method call may be automated via @see BxBaseModGeneralInstaller::_aConnections property.
+     * @param $sTable table name with data which have assiciations with the connection
+     * @param $sFieldId id field name which is associated with the connection
+     * @return number of deleted connections
+     */
+    public function onModuleDeleteInitiatorAndContent ($sTable, $sFieldId)
+    {
+        $iAffected = $this->onModuleDeleteInitiator ($sTable, $sFieldId);
+        $iAffected += $this->onModuleDeleteContent ($sTable, $sFieldId);
+        return $iAffected;
+    }
+
+    /**
+     * Must be called when module (which can have connections as 'initiator') is deleted, to delete any associated data.
+     * This method call may be automated via @see BxBaseModGeneralInstaller::_aConnections property.
+     * @param $sTable table name with data which have assiciations with the connection
+     * @param $sFieldId id field name which is associated with the connection
+     * @return number of deleted connections
+     */
+    public function onModuleDeleteInitiator ($sTable, $sFieldId)
+    {
+        return $this->_oQuery->onModuleDelete ($sTable, $sFieldId, 'initiator');
+    }
+
+    /**
+     * Must be called when module (which can have connections as 'content') is deleted, to delete any associated data.
+     * This method call may be automated via @see BxBaseModGeneralInstaller::_aConnections property.
+     * @param $sTable table name with data which have assiciations with the connection
+     * @param $sFieldId id field name which is associated with the connection
+     * @return number of deleted connections
+     */
+    public function onModuleDeleteContent ($sTable, $sFieldId)
+    {
+        return $this->_oQuery->onModuleDelete ($sTable, $sFieldId, 'content');
+    }
+
+
+    /**
+     * Must be called when module (which can have connections as 'content' or as 'initiator' with 'sys_profiles' table) is deleted, to delete any associated data.
+     * This method call may be automated via @see BxBaseModGeneralInstaller::_aConnections property.
+     * @param $sModuleName module name to delete connections for
+     * @return number of deleted connections
+     */
+    public function onModuleProfileDeleteInitiatorAndContent ($sModuleName)
+    {
+        $iAffected = $this->onModuleProfileDeleteInitiator ($sModuleName);
+        $iAffected += $this->onModuleProfileDeleteContent ($sModuleName);
+        return $iAffected;
+    }
+
+    /**
+     * Must be called when module (which can have connections as 'initiator' with 'sys_profiles' table) is deleted, to delete any associated data.
+     * This method call may be automated via @see BxBaseModGeneralInstaller::_aConnections property.
+     * @param $sModuleName module name to delete connections for
+     * @return number of deleted connections
+     */
+    public function onModuleProfileDeleteInitiator ($sModuleName)
+    {
+        return $this->_oQuery->onModuleProfileDelete ($sModuleName, 'initiator');
+    }
+
+    /**
+     * Must be called when module (which can have connections as 'content' with 'sys_profiles' table) is deleted, to delete any associated data.
+     * This method call may be automated via @see BxBaseModGeneralInstaller::_aConnections property.
+     * @param $sModuleName module name to delete connections for
+     * @return number of deleted connections
+     */
+    public function onModuleProfileDeleteContent ($sModuleName)
+    {
+        return $this->_oQuery->onModuleProfileDelete ($sModuleName, 'content');
+    }
+}
+
+/** @} */

+ 339 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolContentFilter.php

@@ -0,0 +1,339 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolContentFilter extends BxDolFactory implements iBxDolSingleton
+{
+    protected $_oDb;
+
+    protected $_sDataList;
+    protected $_iDefaultValue;
+
+    protected $_iViewerId;
+
+    protected function __construct()
+    {
+        parent::__construct();
+
+        $this->_oDb = BxDolDb::getInstance();
+
+        $this->_sDataList = 'sys_content_filter';
+        $this->_iDefaultValue = 1; //--- Means G (ID = 1) - content available to everybody
+
+        $this->_iViewerId = bx_get_logged_profile_id();
+    }
+
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    public static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolContentFilter();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    public function isEnabled()
+    {
+        return getParam('sys_cf_enable') == 'on';
+    }
+
+    public function isEnabledForComments()
+    {
+        return getParam('sys_cf_enable_comments') == 'on';
+    }
+
+    public function getProhibited()
+    {
+        $sValues = getParam('sys_cf_prohibited');
+        if(!$sValues)
+            return [];
+
+        $aValues = explode(',', $sValues);
+        if(!$aValues)
+            return [];
+
+        return $aValues;
+    }
+    
+    public function getUnauthenticated()
+    {
+        $sValues = getParam('sys_cf_unauthenticated');
+        if(!$sValues)
+            return [];
+
+        $aValues = explode(',', $sValues);
+        if(!$aValues)
+            return [];
+
+        return $aValues;
+    }
+
+    public function getDefaultValue()
+    {
+        return $this->_iDefaultValue;
+    }
+    
+    public function getDefaultValueUnauthenticated()
+    {
+        $aFilters = $this->getUnauthenticated();
+
+        $iCfwValue = 0;
+        foreach($aFilters as $iFilter)
+            $iCfwValue |= (1 << ($iFilter - 1));
+
+        return $iCfwValue;
+    }
+
+    public function getValues()
+    {
+        return BxDolFormQuery::getDataItems($this->_sDataList);
+    }
+
+    public function getInput($aInput, $iProfileId = 0)
+    {
+        return $this->_getInput('content', $aInput, $iProfileId);
+    }
+
+    public function getInputForComments($aInput, $iProfileId = 0)
+    {
+        return $this->_getInput('comments', $aInput, $iProfileId);
+    }
+
+    protected function _getInput($sType, $aInput, $iProfileId = 0)
+    {
+        if(!$this->{'isEnabled' . ($sType == 'comments' ? 'ForComments' : '')}())
+            return array_merge($aInput, [
+                'type' => 'hidden',
+                'value' => 1
+            ]);
+
+        if(!$aInput['values'])
+            $aInput['values'] = BxDolFormQuery::getDataItems($this->_sDataList);
+
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+
+        $aProfileInfo = BxDolProfileQuery::getInstance()->getInfoById($iProfileId);
+        if(empty($aProfileInfo) || !isset($aProfileInfo['cfu_items']))
+            return $aInput;
+
+        $aCfuValues = [];
+        foreach($aInput['values'] as $iValue => $sTitle)
+            if((1 << ($iValue - 1)) & (int)$aProfileInfo['cfu_items'])
+                $aCfuValues[$iValue] = $sTitle;
+
+        $aInput['values'] = $aCfuValues;
+        return $aInput;
+    }
+
+    public function isAllowed($iValue, $iViewerId = 0)
+    {
+        if(!$this->isAllowedBySetting($iValue))
+            return false;
+
+        if(!$this->isAllowedByViewer($iValue, $iViewerId))
+            return false;
+
+        return true;
+    }
+
+    public function isAllowedBySetting($iValue)
+    {
+        $aValues = $this->getProhibited();
+        if(!$aValues)
+            return true;
+
+        return !in_array($iValue, $aValues);
+    }
+
+    public function isAllowedByViewer($iValue, $iViewerId = 0)
+    {
+        $iCfDefault = $this->getDefaultValue();
+        if(!$iValue)
+            $iValue = $iCfDefault;
+
+        if(!$iViewerId)
+            $iViewerId = $this->_iViewerId;
+
+        $iCfwValue = $iCfDefault;
+        $aViewerInfo = BxDolProfileQuery::getInstance()->getInfoById($iViewerId);
+        if(is_array($aViewerInfo) && isset($aViewerInfo['cfw_value']))
+            $iCfwValue = $aViewerInfo['cfw_value'];
+        else
+            $iCfwValue = $this->getDefaultValueUnauthenticated();
+
+        return (1 << ($iValue - 1)) & $iCfwValue;
+    }
+
+    public function getSQLParts($sContentTable, $sContentField, $iViewerId = 0)
+    {
+        $sResult = '';
+
+        $aSQLParts = $this->getSettingSQLParts($sContentTable, $sContentField);
+        if(!empty($aSQLParts['where']))
+            $sResult .= $aSQLParts['where'];
+
+        $aSQLParts = $this->getViewerSQLParts($sContentTable, $sContentField, $iViewerId);
+        if(!empty($aSQLParts['where']))
+            $sResult .= $aSQLParts['where'];
+
+        return $sResult;
+    }
+
+    public function getSettingSQLParts($sContentTable, $sContentField)
+    {
+        $aValues = $this->getProhibited();
+        if(!$aValues)
+            return [];
+
+        return [
+            'where' => " AND `" . $sContentTable . "`.`" . $sContentField . "` NOT IN (" . $this->_oDb->implode_escape($aValues) . ")"
+        ];
+    }
+
+    public function getViewerSQLParts($sContentTable, $sContentField, $iViewerId = 0)
+    {
+        if(!$iViewerId)
+            $iViewerId = $this->_iViewerId;
+
+        $aViewerInfo = BxDolProfileQuery::getInstance()->getInfoById($iViewerId);
+        if(is_array($aViewerInfo) && isset($aViewerInfo['cfw_value']))
+            $iViewerCfwValue = $aViewerInfo['cfw_value'];
+        else
+            $iViewerCfwValue = $this->getDefaultValueUnauthenticated();
+
+        return [
+            'where' => " AND 1 << (`" . $sContentTable . "`.`" . $sContentField . "` - 1) & " . $iViewerCfwValue
+        ];
+    }
+
+    public function getConditions($sContentTable, $sContentField, $iViewerId = 0)
+    {
+        $aResult = [];
+
+        $aConditions = $this->getSettingConditions($sContentTable, $sContentField);
+        if(!empty($aConditions) && is_array($aConditions))
+            $aResult['cf_setting'] = $aConditions;
+
+        $aConditions = $this->getViewerConditions($sContentTable, $sContentField, $iViewerId);
+        if(!empty($aConditions) && is_array($aConditions))
+            $aResult['cf_viewer'] = $aConditions;
+
+        return $aResult;
+    }
+
+    public function getSettingConditions($sContentTable, $sContentField)
+    {
+        $aValues = $this->getProhibited();
+        if(!$aValues)
+            return [];
+
+        return [
+            'value' => $aValues,
+            'field' => $sContentField,
+            'operator' => 'not in',
+            'table' => $sContentTable
+        ];
+    }
+
+    public function getViewerConditions($sContentTable, $sContentField, $iViewerId = 0)
+    {
+        if(!$iViewerId)
+            $iViewerId = $this->_iViewerId;
+
+        $aViewerInfo = BxDolProfileQuery::getInstance()->getInfoById($iViewerId);
+        if(is_array($aViewerInfo) && isset($aViewerInfo['cfw_value']))
+            $iViewerCfwValue = $aViewerInfo['cfw_value'];
+        else
+            $iViewerCfwValue = $this->getDefaultValueUnauthenticated();
+    
+        return [
+            'value' => $iViewerCfwValue,
+            'field' => $sContentField,
+            'operator' => 'in_set',
+            'table' => $sContentTable
+        ];
+    }
+
+    public function updateValuesByProfile($aProfile)
+    {
+        if(!is_array($aProfile))
+            $aProfile = BxDolProfileQuery::getInstance()->getProfiles(['type' => 'id', 'id' => (int)$aProfile]);
+
+        if(!BxDolRequest::serviceExists($aProfile['type'], 'get_info'))
+            return;
+
+        $aFilters = BxDolFormQuery::getDataItems($this->_sDataList, true, BX_DATA_VALUES_ALL);
+        $aProfileInfo = bx_srv($aProfile['type'], 'get_info', [$aProfile['content_id'], false]);
+
+        $iCfwValue = (int)$aProfile['cfw_value'];
+        $bUpdateCfwValue = false;
+
+        $iCfwItems = $iCfuItems = 0;
+        $bUpdateCfwItems = $bUpdateCfuItems = false;
+        foreach($aFilters as $aFilter) {
+            $iFilter = (int)$aFilter['Value'];
+
+            if(!empty($aFilter['Data'])) {
+                $aData = unserialize($aFilter['Data']);
+                if(empty($aData) || !is_array($aData))
+                    continue;
+            }
+            else
+                $aData = [
+                    'is_allowed_watch' => ['module' => 'system', 'method' => 'is_allowed_cfilter', 'params' => ['watch'], 'class' => 'BaseServiceProfiles'],
+                    'is_allowed_use' => ['module' => 'system', 'method' => 'is_allowed_cfilter', 'params' => ['use'], 'class' => 'BaseServiceProfiles'],
+                ];
+
+            if(!empty($aData['is_allowed_watch'])) {
+                $aData['is_allowed_watch']['params'] = array_merge($aData['is_allowed_watch']['params'], [$iFilter, $aProfileInfo]);
+
+                $iWatch = call_user_func_array('bx_srv', array_values($aData['is_allowed_watch']));
+                if($iWatch === false)
+                    continue;
+
+                if($iWatch == 0) {
+                    $iCfwValue &= ~ (1 << ($iFilter - 1));
+                    $bUpdateCfwValue = true;
+                }
+
+                $iCfwItems |= $iWatch;
+                $bUpdateCfwItems = true;
+            }
+
+            if(!empty($aData['is_allowed_use']) && (int)$aProfile['cfu_locked'] == 0) {
+                $aData['is_allowed_use']['params'] = array_merge($aData['is_allowed_use']['params'], [$aFilter['Value'], $aProfileInfo]);
+
+                $iUse = call_user_func_array('bx_srv', array_values($aData['is_allowed_use']));
+                if($iUse === false)
+                    continue;
+
+                $iCfuItems |= $iUse;
+                $bUpdateCfuItems = true;
+            }
+        }
+
+        $oProfileQuery = BxDolProfileQuery::getInstance();
+
+        if($bUpdateCfwValue) 
+            $oProfileQuery->changeCfwValue($aProfile['id'], $iCfwValue);
+
+        if($bUpdateCfwItems)
+            $oProfileQuery->changeCfwItems($aProfile['id'], $iCfwItems);
+
+        if($bUpdateCfuItems)
+            $oProfileQuery->changeCfuItems($aProfile['id'], $iCfuItems);
+    }
+}
+
+/** @} */

+ 211 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolGridConnections.php

@@ -0,0 +1,211 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+
+class BxDolGridConnections extends BxTemplGrid
+{
+    protected $_bInit = false;
+    protected $_bOwner = false;
+    protected $_sObjectConnections = 'sys_profiles_friends';
+    protected $_oProfile;
+    protected $_oConnection;
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        parent::__construct ($aOptions, $oTemplate);
+        $this->_sDefaultSortingOrder = 'DESC';
+
+        $this->_aQueryAppendExclude[] = 'join_connections';
+
+        if(($iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT)) !== false)
+            $this->setProfile($iProfileId);
+    }
+
+    public function init($bForceInit = false)
+    {
+        if($this->_bInit && !$bForceInit)
+            return $this->_bInit;
+
+        if(!$this->_oProfile)
+            return false;
+
+        if($this->_oProfile->id() == bx_get_logged_profile_id())
+            $this->_bOwner = true;
+
+        $this->_oConnection = BxDolConnection::getObjectInstance($this->_sObjectConnections);
+        if(!$this->_oConnection)
+            return false;
+
+        $aSQLParts = $this->_oConnection->getConnectedInitiatorsAsSQLParts('p', 'id', $this->_oProfile->id(), $this->_bOwner ? false : true);
+
+        $this->addMarkers(array(
+            'profile_id' => $this->_oProfile->id(),
+            'join_connections' => $aSQLParts['join']
+        ));
+
+        return true;
+    }
+
+    public function setProfile($iProfileId)
+    {
+        $this->_oProfile = BxDolProfile::getInstance((int)$iProfileId);
+
+        $this->_bInit = $this->init(true);
+    }
+
+    public function getCode ($isDisplayHeader = true)
+    {
+        if(!$this->_bInit)
+            return '';
+
+        return parent::getCode($isDisplayHeader);        
+    }
+
+    /**
+     * 'accept' action handler
+     */
+    public function performActionAccept()
+    {
+        list ($iId, $iViewedId) = $this->_prepareIds();
+        if(!$iId)
+            return $this->_bIsApi ? [] : echoJson(['msg' => _t('_sys_txt_error_occured')]);
+
+        $a = $this->_oConnection->actionAdd($iId, $iViewedId);
+        if (isset($a['err']) && $a['err'])
+            $aResult = ['msg' => $a['msg']];
+        else
+            $aResult = !$this->_bIsApi ? ['grid' => $this->getCode(false), 'blink' => $iId] : [];
+
+        return $this->_bIsApi ? $aResult : echoJson($aResult);
+    }
+
+    /**
+     * 'add friend' action handler
+     */
+    public function performActionAddFriend()
+    {
+        return $this->performActionAccept();
+    }
+
+    protected function _delete ($mixedId)
+    {
+        list ($iId, $iViewedId) = $this->_prepareIds();
+
+        if ($this->_oConnection->isConnected($iViewedId, $iId, true))
+            $a = $this->_oConnection->actionRemove($iViewedId, $iId);
+        else
+            $a = $this->_oConnection->actionReject($iId, $iViewedId);
+
+        if (isset($a['err']) && $a['err'])
+            return false;
+
+        return true;
+    }
+
+    protected function _getCellName ($mixedValue, $sKey, $aField, $aRow)
+    {
+        if($this->_bIsApi)
+            return ['type' => 'profile', 'data' => BxDolProfile::getData($aRow['id'])];
+
+        $oProfile = BxDolProfile::getInstance($aRow['id']);
+        if (!$oProfile)
+            return _t('_sys_txt_error_occured');
+
+        $sBadges = bx_srv($oProfile->getModule(), 'get_badges', array($oProfile->getContentId()));
+        return parent::_getCellDefault($oProfile->getUnit(0, array('template' => array('vars' => array('addon' => $sBadges)))), $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellInfo ($mixedValue, $sKey, $aField, $aRow)
+    {
+        $s = '';
+
+        // for friend requests display mutual friends
+        if ($this->_bOwner && !$aRow['mutual']) {
+            $a = $this->_oConnection->getCommonContent($aRow['id'], bx_get_logged_profile_id(), true);
+            $i = count($a);
+            if (1 == $i) {
+                $iProfileId = array_pop($a);
+                $oProfile = BxDolProfile::getInstance($iProfileId);
+                $s = _t('_sys_txt_one_mutual_friend', $oProfile->getUrl(), $oProfile->getDisplayName());
+            } elseif ($i) {
+                $s = _t('_sys_txt_n_mutual_friends', $i);
+            }
+        }
+
+        // display friends number if no other info is available
+        if (!$s) {
+            $a = $this->_oConnection->getConnectedContent($aRow['id'], true);
+            $i = count($a);
+            $s = _t('_sys_txt_n_friends', $i);
+        }
+
+        return parent::_getCellDefault ($s, $sKey, $aField, $aRow);
+    }
+
+    protected function _getActionDelete ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if (!$this->_bOwner)
+            return $this->_bIsApi ? [] : '';
+
+        return parent::_getActionDelete($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getActionAccept ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if (!$this->_bOwner || $aRow['mutual'])
+            return $this->_bIsApi ? [] : '';
+
+        if($this->_bIsApi)
+            return array_merge($a, ['name' => $sKey, 'type' => 'callback', 'on_callback' => 'hide']);
+
+        return parent::_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getActionAddFriend ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if (!isLogged() || $this->_bOwner || $aRow['id'] == bx_get_logged_profile_id())
+            return $this->_bIsApi ? [] : '';
+
+        if ($this->_oConnection->isConnected($aRow['id'], bx_get_logged_profile_id()) || $this->_oConnection->isConnected(bx_get_logged_profile_id(), $aRow['id']))
+            return $this->_bIsApi ? [] : '';
+
+        if($this->_bIsApi)
+            return array_merge($a, ['name' => $sKey, 'type' => 'callback', 'on_callback' => 'hide']);
+
+        return parent::_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _prepareIds ()
+    {
+        $aIds = bx_get('ids');
+        if($aIds && is_array($aIds))
+            $mixedId = array_pop($aIds);
+
+        return $this->__prepareIds($mixedId);
+    }
+
+    protected function __prepareIds ($mixedId)
+    {
+        $iId = 0;
+        $iViewedId = false;
+
+        if(strpos($mixedId, ':') !== false) {
+            list ($iId, $iViewedId) = explode (':', $mixedId);
+            $iId = (int)$iId;
+            $iViewedId = (int)$iViewedId;
+        }
+        else
+            $iId = (int)$mixedId;
+
+        return [$iId, $iViewedId];
+    }
+}
+
+/** @} */

+ 282 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolModuleQuery.php

@@ -0,0 +1,282 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolModuleQuery extends BxDolDb implements iBxDolSingleton
+{
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+    }
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance()
+    {
+        $sClass = __CLASS__;
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new $sClass();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    function getModuleById($iId, $bFromCache = true)
+    {
+        $sSql = $this->prepare("SELECT * FROM `sys_modules` WHERE `id`=? LIMIT 1", $iId);
+        return $bFromCache ? $this->fromMemory('sys_modules_' . $iId, 'getRow', $sSql) : $this->getRow($sSql);
+    }
+    function getModuleByName($sName, $bFromCache = true)
+    {
+        $sSql = $this->prepare("SELECT * FROM `sys_modules` WHERE `name`=? LIMIT 1", $sName);
+        return $bFromCache ? $this->fromMemory('sys_modules_' . $sName, 'getRow', $sSql) : $this->getRow($sSql);
+    }
+    function getModuleByUri($sUri, $bFromCache = true)
+    {
+        $sSql = $this->prepare("SELECT * FROM `sys_modules` WHERE `uri`=? LIMIT 1", $sUri);
+        return $bFromCache ? $this->fromMemory('sys_modules_' . $sUri, 'getRow', $sSql) : $this->getRow($sSql);
+    }
+    function enableModuleByUri($sUri)
+    {
+        $sSql = $this->prepare("UPDATE `sys_modules` SET `enabled`='1' WHERE `uri`=? LIMIT 1", $sUri);
+        return (int)$this->query($sSql) > 0;
+    }
+    function disableModuleByUri($sUri)
+    {
+        $sSql = $this->prepare("UPDATE `sys_modules` SET `enabled`='0' WHERE `uri`=? LIMIT 1", $sUri);
+        return (int)$this->query($sSql) > 0;
+    }
+    function setModulePendingUninstall($sUri, $bPendingUninstall)
+    {
+        $sSql = $this->prepare("UPDATE `sys_modules` SET `pending_uninstall` = ? WHERE `uri` = ? LIMIT 1", $bPendingUninstall ? 1 : 0, $sUri);
+        return $this->query($sSql);
+    }
+    function isModule($sUri)
+    {
+        $sSql = $this->prepare("SELECT `id` FROM `sys_modules` WHERE `uri`=? LIMIT 1", $sUri);
+        return (int)$this->fromMemory('sys_module_' . $sUri, 'getOne', $sSql) > 0;
+    }
+    function isModuleByName($sName)
+    {
+        $sSql = $this->prepare("SELECT `id` FROM `sys_modules` WHERE `name`=? LIMIT 1", $sName);
+        return (int)$this->fromMemory('sys_module_' . $sName, 'getOne', $sSql) > 0;
+    }
+    function isModuleParamsUsed($sName, $sUri, $sPath, $sPrefixDb, $sPrefixClass)
+    {
+        $sSql = "SELECT `id` FROM `sys_modules` WHERE `name`=:name || `uri`=:uri || `path`=:path || `db_prefix`=:db_prefix || `class_prefix`=:class_prefix LIMIT 1";
+        return (int)$this->getOne($sSql, [
+            'name' => $sName,
+            'uri' => $sUri,
+            'path' => $sPath,
+            'db_prefix' => $sPrefixDb,
+            'class_prefix' => $sPrefixClass
+        ]) > 0;
+    }
+    function isEnabled($sUri)
+    {
+        $sSql = $this->prepare("SELECT `id` FROM `sys_modules` WHERE `uri`=? AND `enabled`='1' LIMIT 1", $sUri);
+        return (int)$this->fromMemory('sys_module_enabled_' . $sUri, 'getOne', $sSql) > 0;
+    }
+    function isEnabledByName($sName)
+    {
+        $sSql = $this->prepare("SELECT `id` FROM `sys_modules` WHERE `name`=? AND `enabled`='1' LIMIT 1", $sName);
+        return (int)$this->fromMemory('sys_module_enabled_' . $sName, 'getOne', $sSql) > 0;
+    }
+    function getModules()
+    {
+        $sSql = "SELECT * FROM `sys_modules` ORDER BY `title`";
+        return $this->fromMemory('sys_modules', 'getAll', $sSql);
+    }
+    function getModulesBy($aParams = array(), $bFromCache = true)
+    {
+    	$aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
+        $sPostfix = $sWhereClause = $sOrderByClause = "";
+        $sSelectClause = "`id`, `type`, `name`, `title`, `vendor`, `version`, `help_url`, `path`, `uri`, `class_prefix`, `db_prefix`, `lang_category`, `date`, `enabled`";
+        $aBindings = [];
+
+        switch($aParams['type']) {
+            case 'type':
+                if(!is_array($aParams['value']))
+                    $aParams['value'] = array($aParams['value']);
+
+                $sPostfix .= '_type_' . implode('_', $aParams['value']);
+                $sWhereClause .= " AND `type` IN (" . $this->implode_escape($aParams['value']) . ")";
+                break;
+
+            case 'modules':
+                $sPostfix .= '_modules';
+                $aBindings['type'] = BX_DOL_MODULE_TYPE_MODULE;
+
+                $sWhereClause .= " AND `type`=:type";
+                break;
+
+            case 'modules_subtypes':
+                if(isset($aParams['id_as_key']) && $aParams['id_as_key'] === true) {
+                    $aMethod['name'] = 'getAllWithKey';
+                    $aMethod['params'][1] = 'id';
+                }
+
+                if(isset($aParams['name_as_key']) && $aParams['name_as_key'] === true) {
+                    $aMethod['name'] = 'getAllWithKey';
+                    $aMethod['params'][1] = 'name';
+                }
+
+                if(!is_array($aParams['value']))
+                    $aParams['value'] = [$aParams['value']];
+
+                $sPostfix .= '_modules_subtypes_' . implode('_', $aParams['value']);
+                $aBindings['type'] = BX_DOL_MODULE_TYPE_MODULE;
+
+                $iSubtypes = 0;
+                foreach($aParams['value'] as $iSubtype) 
+                    $iSubtypes |= pow(2, $iSubtype);
+
+                $aBindings['subtypes'] = $iSubtypes;
+
+                $sWhereClause .= " AND `type`=:type AND `subtypes` & :subtypes";
+                break;
+
+            case 'is_module_subtype':
+                $sPostfix .= '_is_module_' . $aParams['subtype'] . '_' . $aParams['name'];
+                $aMethod['name'] = 'getOne';
+                $aBindings = [
+                    'type' => BX_DOL_MODULE_TYPE_MODULE,
+                    'subtypes' => pow(2, (int)$aParams['subtype']),
+                    'name' => $aParams['name']
+                ];
+                $sSelectClause = "`id`";
+                $sWhereClause .= " AND `type`=:type AND `subtypes` & :subtypes AND `name`=:name";
+                break;
+
+            case 'languages':
+                $sPostfix .= '_languages';
+                $aBindings['type'] = BX_DOL_MODULE_TYPE_LANGUAGE;
+
+                $sWhereClause .= " AND `type`=:type";
+                break;
+
+            case 'templates':
+                $sPostfix .= '_templates';
+                $aBindings['type'] = BX_DOL_MODULE_TYPE_TEMPLATE;
+
+                $sWhereClause .= " AND `type`=:type";
+                break;
+
+            case 'path_and_uri':
+                $sPostfix .= '_path_and_uri_' . $aParams['path'] . '_' . $aParams['uri'];
+            	$aMethod['name'] = 'getRow';
+            	$aBindings = array_merge($aBindings, array(
+                    'path' => $aParams['path'],
+                    'uri' => $aParams['uri']
+                ));
+
+            	$sWhereClause .= " AND `path`=:path AND `uri`=:uri";
+            	break;
+
+            case 'all_pairs_name_uri':
+                $sPostfix .= 'all_pairs_name_uri';
+            	$aMethod['name'] = 'getPairs';
+            	$aMethod['params'][1] = 'name';
+            	$aMethod['params'][2] = 'uri';
+            	break;
+
+            case 'all_key_name':
+                $sPostfix .= 'all_key_name';
+            	$aMethod['name'] = 'getAllWithKey';
+            	$aMethod['params'][1] = 'name';
+            	break;
+
+            case 'all':
+            	break;
+        }
+
+        if(isset($aParams['active'])) {
+            $sPostfix .= "_active";
+            $aBindings['enabled'] = (int)$aParams['active'];
+
+            $sWhereClause .= " AND `enabled`=:enabled";
+        }
+
+        $sOrderByClause = " ORDER BY " . (isset($aParams['order_by']) ? $aParams['order_by'] : '`title`');
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . "
+            FROM `sys_modules`
+            WHERE 1 " . $sWhereClause . $sOrderByClause;
+        $aMethod['params'][] = $aBindings;
+
+        if(!$bFromCache || empty($sPostfix))
+            return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+
+        return call_user_func_array(array($this, 'fromMemory'), array_merge(array('sys_modules' . $sPostfix, $aMethod['name']), $aMethod['params']));
+    }
+    
+    function isModuleContent($sName)
+    {
+        return (int)$this->getModulesBy(['type' => 'is_module_subtype', 'subtype' => BX_DOL_MODULE_SUBTYPE_TEXT, 'name' => $sName]) != 0;
+    }
+    
+    function isModuleContext($sName)
+    {
+        return (int)$this->getModulesBy(['type' => 'is_module_subtype', 'subtype' => BX_DOL_MODULE_SUBTYPE_CONTEXT, 'name' => $sName]) != 0;
+    }
+    
+    function isModuleProfile($sName)
+    {
+        return (int)$this->getModulesBy(['type' => 'is_module_subtype', 'subtype' => BX_DOL_MODULE_SUBTYPE_PROFILE, 'name' => $sName]) != 0;
+    }
+
+    function getModulesUri()
+    {
+        return $this->fromMemory('sys_modules_uri', 'getColumn', 'SELECT `uri` FROM `sys_modules` ORDER BY `uri`');
+    }
+
+    function getDependent($sName, $sUri)
+    {
+        $aResults = array();
+
+        $aModules = $this->getAll("SELECT `id`, `title`, `dependencies`, `enabled` FROM `sys_modules` WHERE (`dependencies` LIKE " . $this->escape('%' . $sName . '%') . " OR `dependencies` LIKE " . $this->escape('%' . $sUri . '%') . ") AND `enabled`='1'");
+        foreach($aModules as $aModule) {
+            $aDependencies = explode(',', $aModule['dependencies']);
+            if(in_array($sName, $aDependencies) || in_array($sUri, $aDependencies))
+                $aResults[] = $aModule;
+        }
+
+        return $aResults;
+    }
+
+    public function updateModule($aParamsSet, $aParamsWhere = array())
+    {
+        if(empty($aParamsSet))
+            return false;
+
+        $sWhereClause = !empty($aParamsWhere) ? $this->arrayToSQL($aParamsWhere, " AND ") : "1";
+
+        $sSql = "UPDATE `sys_modules` SET " . $this->arrayToSQL($aParamsSet) . " WHERE " . $sWhereClause;
+        return $this->query($sSql);
+    }
+    
+    public function checkModulesSubtypes()
+    {
+        return (int)$this->getOne("SELECT COUNT(`id`) FROM `sys_modules` WHERE `type` = :type AND `subtypes` <> 0", [
+            'type' => BX_DOL_MODULE_TYPE_MODULE
+        ]) > 0;
+    }
+}
+
+/** @} */

+ 871 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolPage.php

@@ -0,0 +1,871 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Pages.
+ *
+ * It allows to display pages which are built from studio.
+ *
+ * The new system has the following page features:
+ * - Layouts: page can have any structure, not just columns.
+ * - SEO: page can have own SEO options, like meta tags and meta keywords, as well as instructions for search bots.
+ * - Cache: page can be cached on the server.
+ * - Access control: page access can be controlled using member levels.
+ *
+ *
+ *
+ * @section page_create Creating the Page object:
+ *
+ * 1. add record to 'sys_objects_page' table:
+ *
+ * - object: name of the page object, in the format: vendor prefix, underscore, module prefix, underscore, internal identifier or nothing; for example: bx_profiles_view - profile view page.
+ * - title: name to display as page title.
+ * - module: the module this page belongs to.
+ * - layout_id: page layout to use, this is id of the record from 'sys_pages_layouts' table.
+ * - visible_for_levels: bit field with set of member level ids. To use member level id in bit field - the level id minus 1 as power of 2 is used, for example:
+ *      - user level id = 1 -> 2^(1-1) = 1
+ *      - user level id = 2 -> 2^(2-1) = 2
+ *      - user level id = 3 -> 2^(3-1) = 4
+ *      - user level id = 4 -> 2^(4-1) = 8
+ * - visible_for_levels_editable: it determines if 'visible_for_levels' field is editable from page builder, visibility options can be overriden by custom class and shouldn't be editable in this case.
+ * - url: the page url, if it is static page.
+ * - content_info: content info object name related to the content on the page,
+ *                 if different from module name
+ * - meta_description: meta description of the page.
+ * - meta_keywords: meta keywords of the page.
+ * - meta_robots: instructions for search bots.
+ * - cache_lifetime: number of seconds to store cache for.
+ * - cache_editable: it determines if cache can be edited from page builder.
+ * - deletable: it determines if page can be deleted from page builder.
+ * - override_class_name: user defined class name which is derived from BxTemplPage.
+ * - override_class_file: the location of the user defined class, leave it empty if class is located in system folders.
+ *
+ * Page can select appropriate menu automatically if 'module' and 'object' fields in 'sys_objects_page' table are matched with 'module' and 'name' fields in 'sys_menu_items' table.
+ *
+ *
+ * Page layout are stored in 'sys_pages_layouts' table:
+ * - name: inner unique layout name.
+ * - icon: layout icon to display in page builder, it should represent basic view of the layout to help studio operator determine the layout structure.
+ * - title: layout name to display in page builder.
+ * - template: template name to use to display page with certain layout.
+ * - cells_number: number of areas in the layout, page blocks can be places into this areas(cells).
+ * To define areas in the layout they should be named as '__cell_N__', where N is cell number, starting from 1.
+ *
+ *
+ * 2. Add page blocks to 'sys_pages_blocks' table:
+ * - object: page object name this block belongs to.
+ * - cell_id: cell number in page layout to place block to.
+ * - module: module name this block belongs to.
+ * - title: block title.
+ * - designbox_id: design box to use to diplay page block, it is id of the record from 'sys_pages_design_boxes' table.
+ * - visible_for_levels: bit field with set of member level ids. To use member level id in bit field - the level id minus 1 as power of 2 is used, for example:
+ *      - user level id = 1 -> 2^(1-1) = 1
+ *      - user level id = 2 -> 2^(2-1) = 2
+ *      - user level id = 3 -> 2^(3-1) = 4
+ *      - user level id = 4 -> 2^(4-1) = 8
+ * - type: block type
+ *      - raw: HTML block, displayed in page builder as HTML textarea.
+ *      - html: HTML block, displayed in page builder as visual editor, like TinyMCE.
+ *      - lang: translatable language string, displayed in page builder as editable language key.
+ *      - image: just an image, displayed in page builder as HTML upload form.
+ *      - rss: RSS block, displayed in page builder as editable URL to RSS resource, along with number of displayed items.
+ *      - menu: menu block, displayed as menu selector.
+ *      - service: to display block content, the provided service method is used.
+ * - content: depending on 'type' field:
+ *      - raw: HTML string.
+ *      - html: HTML string.
+ *      - lang: language key.
+ *      - image: image id in the storage and alignment (left, center, right) for example: 36\#center
+ *      - rss: URL to RSS with number of displayed items, for example: http://www.example.com/rss#4
+ *      - menu: menu object name.
+ *      - service: serialized array of service call parameters: module - module name, method - service method name, params - array of parameters.
+ * - deletable: is block deletable from page builder
+ * - copyable: is block can be copied to any other page from page builder.
+ * - order: block order in particular cell.
+ *
+ * Block design boxes are stored in 'sys_pages_design_boxes' table:
+ * - id: consistent id, there are the following defines can be used in the code for each system block style:
+ *      - 0 - BX_DB_CONTENT_ONLY: design box with content only - no borders, no background, no caption.
+ *      - 1 - BX_DB_DEF: default design box with content, borders and caption.
+ *      - 2 - BX_DB_EMPTY: just empty design box, without anything.
+ *      - 3 - BX_DB_NO_CAPTION: design box with content, like BX_DB_DEF but without caption.
+ *      - 10 - BX_DB_PADDING_CONTENT_ONLY: design box with content only wrapped with default padding - no borders, no background, no caption; it can be used to just wrap content with default padding.
+ *      - 11 - BX_DB_PADDING_DEF: default design box with content wrapped with default padding, borders and caption.
+ *      - 13 - BX_DB_PADDING_NO_CAPTION: design box with content wrapped with default padding, like BX_DB_DEF but without caption.
+ * - title: block name which is displayed in studio, describes block styles.
+ * - template: template name to use to display page block.
+ *
+ *
+ * 3. Display Page.
+ * Use the following sample code to display page:
+ * @code
+ *     $oPage = BxDolPage::getObjectInstance('sample'); // 'sample' is 'object' field from 'sys_objects_page' table, it automatically creates instance of default or custom class by object name
+ *     if ($oPage)
+ *         echo $oPage->getCode(); // print page
+ * @endcode
+ *
+ */
+class BxDolPage extends BxDolFactory implements iBxDolFactoryObject, iBxDolReplaceable
+{
+    protected $_sObject;
+    protected $_aObject;
+    protected $_oQuery;
+    protected $_aMarkers = array ();
+
+    /**
+     * Constructor
+     * @param $aObject array of page options
+     */
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+
+        $this->_sObject = $aObject['object'];
+        $this->_aObject = $aObject;
+        $this->_oQuery = new BxDolPageQuery($this->_aObject);
+    }
+
+    /**
+     * Get page object instance by Module name and page URI
+     * @param $sModule module name
+     * @param $sURI unique page URI
+     * @return object instance or false on error
+     */
+    static public function getObjectInstanceByModuleAndURI($sModule, $sURI = '', $oTemplate = false)
+    {
+    	if(empty($sURI) && bx_get('i') !== false)
+    		$sURI = bx_process_input(bx_get('i'));
+
+    	if(empty($sURI))
+			return false;
+
+        $sObject = BxDolPageQuery::getPageObjectNameByURI($sURI, $sModule);
+        return $sObject ? self::getObjectInstance($sObject, $oTemplate) : false;
+    }
+
+    /**
+     * Get page object instance by page URI
+     * @param $sURI unique page URI
+     * @return object instance or false on error
+     */
+    static public function getObjectInstanceByURI($sURI = '', $oTemplate = false, $bRedirectCheck = false)
+    {
+    	if(empty($sURI) && bx_get('i') !== false)
+    		$sURI = bx_process_input(bx_get('i'));
+
+    	if(empty($sURI))
+			return false;
+
+        $sObject = BxDolPageQuery::getPageObjectNameByURI($sURI);
+        if ($bRedirectCheck && !$sObject && '/' == substr($sURI, -1)) {
+            header("HTTP/1.1 301 Moved Permanently");
+            unset($_GET['i']);
+            header ('Location:' . bx_append_url_params(bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . trim($sURI, '/'))), $_GET));
+            exit;
+        }
+
+        return $sObject ? self::getObjectInstance($sObject, $oTemplate) : false;
+    }
+
+    /**
+     * Get page object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    static public function getObjectInstance($sObject, $oTemplate = false)
+    {
+        if (isset($GLOBALS['bxDolClasses']['BxDolPage!'.$sObject]))
+            return $GLOBALS['bxDolClasses']['BxDolPage!'.$sObject];
+
+        $aObject = BxDolPageQuery::getPageObject($sObject);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        $sClass = 'BxTemplPage';
+        if (!empty($aObject['override_class_name'])) {
+            $sClass = $aObject['override_class_name'];
+            if (!empty($aObject['override_class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aObject['override_class_file']);
+        }
+
+        $o = new $sClass($aObject, $oTemplate);
+
+        return ($GLOBALS['bxDolClasses']['BxDolPage!'.$sObject] = $o);
+    }
+
+	/**
+     * Process page triggers.
+     * Page triggers allow to automatically add page blocks to modules with no different if dependant module was install before or after the module page block belongs to.
+     * For example module "Notes" adds page blocks to all profiles modules (Persons, Organizations, etc)
+     * with no difference if persons module was installed before or after "Notes" module was installed.
+     * @param $sPageTriggerName trigger name to process, usually specified in module installer class - @see BxBaseModGeneralInstaller
+     * @return always true, always success
+     */
+    static public function processPageTrigger ($sPageTriggerName)
+    {
+        // get list of active modules
+        $aModules = BxDolModuleQuery::getInstance()->getModulesBy(array(
+            'type' => 'modules',
+            'active' => 1,
+        ));
+
+        // get list of page block triggers
+        $aPageBlocks = BxDolPageQuery::getPageTriggers($sPageTriggerName);
+
+        // check each page block trigger for all modules
+        foreach ($aPageBlocks as $aPageBlock) {
+            foreach ($aModules as $aModule) {
+                if (!BxDolRequest::serviceExists($aModule['name'], 'get_page_object_for_page_trigger'))
+                    continue;
+
+				$mixedPageObject = BxDolService::call($aModule['name'], 'get_page_object_for_page_trigger', array($sPageTriggerName));
+                if (!$mixedPageObject)
+                    continue;
+
+                $aPageBlockRow = $aPageBlock;
+
+                if (is_array($mixedPageObject))
+                    $aPageBlockRow = array_merge($aPageBlockRow, $mixedPageObject);
+                else
+                    $aPageBlockRow['object'] = $mixedPageObject;
+                
+                BxDolPageQuery::addPageBlockToPage($aPageBlockRow);
+            }
+        }
+
+        return true;
+    }
+
+    static public function multisiteLinkCheck ($sLink, $sPageUri = '', $sPageModule = '', $aQueryParams = [])
+    {
+        $sMultisiteDomain = self::getMultisite($sLink, $sPageUri, $sPageModule, $aQueryParams);
+        if ($sMultisiteDomain) {
+            if (parse_url($sMultisiteDomain, PHP_URL_HOST) !== $_SERVER['HTTP_HOST'])
+                return $sMultisiteDomain . $sLink;
+        }
+
+        if (!$sMultisiteDomain && parse_url(BX_MULTISITE_URL_MAIN, PHP_URL_HOST) !== $_SERVER['HTTP_HOST']) {
+            return BX_MULTISITE_URL_MAIN . $sLink;
+        }
+
+        return false;
+
+    }
+
+	/**
+     * Get multisite link
+     * @param $sLink link to check
+     * @return return multisite link if provided link is multisite, or false is other case
+     */
+    static public function getMultisite ($sLink, $sPageUri = '', $sPageModule = '', $aQueryParams = [])
+    {
+        $sMultisiteDomain = '';
+        if (!$sPageModule || !$aQueryParams) {
+
+            $sQuery = parse_url($sLink, PHP_URL_QUERY);
+            if ($sQuery) {
+
+                $aQueryParams = bx_parse_str($sQuery);
+                $sPageUri = !empty($aQueryParams['i']) ? $aQueryParams['i'] : false;
+                if ($sPageUri) {
+                    $sPageName = BxDolPageQuery::getPageObjectNameByURI($sPageUri);
+                    $aPage = $sPageName ? BxDolPageQuery::getPageObject($sPageName) : false;
+                    $sPageModule = $aPage ? $aPage['module'] : '';
+                }
+            }
+        }
+
+        $sMultisiteDomain = '';
+        if (!empty($aQueryParams['profile_id'])) {
+            $oProfile = BxDolProfile::getInstance($aQueryParams['profile_id']);
+            if (BX_MULTISITE_MODULE == $oProfile->getModule())
+                $sMultisiteDomain = $oProfile->getDisplayName();
+        }
+        elseif (!empty($aQueryParams['id']) && $sPageModule) {
+            $sContentInfo = !empty($aPage['content_info']) ? $aPage['content_info'] : $sPageModule;
+            $oContentInfo = BxDolContentInfo::getObjectInstance($sContentInfo);
+            if ($oContentInfo && BX_MULTISITE_MODULE == $sPageModule) {
+                $sMultisiteDomain = $oContentInfo->getContentTitle($aQueryParams['id']);
+            }
+            elseif ($oContentInfo && BX_MULTISITE_MODULE != $sPageModule) {
+                $iPrivacy = $oContentInfo->getContentPrivacy($aQueryParams['id']);
+                if (0 > $iPrivacy) {
+                    $oProfile = BxDolProfile::getInstance(abs($iPrivacy));
+                    if ($oProfile && BX_MULTISITE_MODULE == $oProfile->getModule())
+                        $sMultisiteDomain = $oProfile->getDisplayName();
+                }
+            }
+        }
+
+        if ($sMultisiteDomain)
+            return str_replace('{domain}', mb_strtolower($sMultisiteDomain), BX_MULTISITE_URL_PATTERN);
+
+        return false;
+    }
+
+	/**
+     * Delete SEO link
+     * @param $sModule module name
+     * @param $sContentInfoObject content info object
+     * @param $sId content id
+     * @return number of affected rows
+     */
+    static public function deleteSeoLink ($sModule, $sContentInfoObject, $sId)
+    {
+        return BxDolPageQuery::deleteSeoLink($sModule, $sContentInfoObject, $sId);
+    }
+
+	/**
+     * Delete SEO links by param
+     * @param $sParamName GET param name
+     * @param $sId content id
+     * @return number of affected rows
+     */
+    static public function deleteSeoLinkByParam ($sParamName, $sId)
+    {
+        return BxDolPageQuery::deleteSeoLinkByParam($sParamName, $sId);
+    }
+
+	/**
+     * Delete SEO links by module
+     * @param $sModule module name
+     * @return number of affected rows
+     */
+    static public function deleteSeoLinkByModule ($sModule)
+    {
+        return BxDolPageQuery::deleteSeoLinkByModule($sModule);
+    }
+
+	/**
+     * Get page object by SEO link. It takes request part from SEO link and returns page object
+     * @param $sRequest request URI with SEO link
+     * @return false - if page wasn't found,  
+     *         page object - on success,  
+     *         string URL - if permanent redirect is needed
+     */
+    static public function getPageBySeoLink ($sRequest)
+    {
+        if (!$sRequest || '/' === $sRequest || !getParam('permalinks_seo_links'))
+            return false;
+
+        // redirect to the correct URL
+        if ('/' === $sRequest[-1] || $sRequest != mb_strtolower($sRequest)) {
+            unset($_GET['_q']);
+            $sUrl = BX_DOL_URL_ROOT . bx_append_url_params(mb_strtolower(trim($sRequest, '/')), $_GET);
+            return $sUrl;    
+        }
+
+        // parse URL
+        $a = explode('/', $sRequest);
+        if (!$a || empty($a[0]))
+            return false;
+
+        // check page URI rewrite redirect
+        $aSeoUriRewrites = BxDolPageQuery::getSeoUriRewrites();
+        if (isset($aSeoUriRewrites[$a[0]])) {
+            unset($_GET['_q']);
+            $a[0] = $aSeoUriRewrites[$a[0]];
+            $sUrl = BX_DOL_URL_ROOT . bx_append_url_params(mb_strtolower(implode('/', $a)), $_GET);
+            return $sUrl;
+        }
+
+        // check page URI rewrite
+        $aSeoUriRewritesInv = array_flip($aSeoUriRewrites);
+        if (isset($aSeoUriRewritesInv[$a[0]]))
+            $a[0] = $aSeoUriRewritesInv[$a[0]];
+
+        // get page
+        $sPageName = BxDolPageQuery::getPageObjectNameByURI($a[0]);
+        $aPage = $sPageName ? BxDolPageQuery::getPageObject($sPageName) : false;
+        if (!$aPage)
+            return false;
+
+        // page with params
+        if (!empty($a[1])) { 
+            $r = BxDolPageQuery::getSeoLink($aPage['module'], $a[0], ['uri' => $a[1]]);
+            if ($r)
+                $_GET[$r['param_name']] = $_REQUEST[$r['param_name']] = $r['param_value'];
+        }
+
+        // multisite redirect
+        if (defined('BX_MULTISITE_MODULE')) {
+            list($sPageLink, $aPageParams) = bx_get_base_url_inline();
+            unset($aPageParams['_q']);
+            $sLink = bx_append_url_params($sPageLink, $aPageParams);
+            if (0 === strpos($sLink, BX_DOL_URL_ROOT))
+                $sLink = substr($sLink, strlen(BX_DOL_URL_ROOT));
+
+            $s = BxDolPage::multisiteLinkCheck ('', $a[0], $aPage['module'], $_GET);
+            if (false !== $s)
+                return $s . $sLink;
+        }
+
+        // display page
+        $_REQUEST['i'] = $_GET['i'] = $a[0];
+        $oPage = BxDolPage::getObjectInstanceByURI($a[0], false, true);
+        return $oPage;
+    }
+    
+	/**
+     * Process SEO links. It takes request part from SEO link and process it 
+     * to make it work as regular page link
+     * @param $sRequest request URI with SEO link
+     * @return true - if page was found and processed correctly, false - if page wasn't found
+     */
+    static public function processSeoLink ($sRequest)
+    {
+        $mixed = self::getPageBySeoLink($sRequest);
+        if (($sUrl = $mixed) && is_string($sUrl)) {
+            header('Location:' . $sUrl, true, 301);
+            exit;
+        }
+        elseif (($oPage = $mixed) && is_object($oPage)) {
+            $oPage->displayPage();
+            return true;
+        }
+        else {
+            return false;
+        }
+    }
+
+	/**
+     * Transform regular link to SEO link. It takes regular list as param and return SEO link.
+     * @param $sLink regular link
+     * @param $sPrefix prefix to add to the final URL, usually BX_DOL_URL_ROOT
+     * @param $aParams additional GET params
+     * @return SEO link string on success, false if transform failed or not necessary
+     */
+    static public function transformSeoLink ($sLink, $sPrefix, $aParams = array())
+    {
+        if (!getParam('permalinks_seo_links'))
+            return false;
+
+        if (0 !== strncmp('page.php', $sLink, 8)) // only page.php links are supported
+            return false;
+
+        $sQuery = parse_url($sLink, PHP_URL_QUERY);
+        if (false === $sQuery)
+            return false;
+
+        if (function_exists('bx_parse_str'))
+            $aQueryParams = bx_parse_str($sQuery);
+        else
+            parse_str($sQuery, $aQueryParams);
+
+        $sPageUri = !empty($aQueryParams['i']) ? $aQueryParams['i'] : false;
+        unset($aQueryParams['i']);
+
+        if (!$sPageUri) // page URI wasn't found
+            return false;
+
+        $sPageModule = '';
+        $aSeoParams = array('id', 'profile_id'); // supported SEO params which are transformed to SEO strings
+        $sSeoParamName = '';
+        $sSeoParamValue = '';
+        foreach ($aSeoParams as $k) {
+            if (empty($aQueryParams[$k]) && empty($aParams[$k]))
+                continue;
+            $sSeoParamName = $k;
+            $sSeoParamValue = !empty($aParams[$k]) ? $aParams[$k] : $aQueryParams[$k];
+            unset($aQueryParams[$k]);
+            unset($aParams[$k]);
+            break; // only 1 SEO param will be transformed
+        }
+
+        if (!empty($sSeoParamValue) && 0 === strpos($sSeoParamValue, '{'))
+            return false;
+
+        $sSeoPageUri = $sPageUri;
+        $aSeoUriRewrites = BxDolPageQuery::getSeoUriRewrites();
+        if (isset($aSeoUriRewrites[$sSeoPageUri]))
+            $sSeoPageUri = $aSeoUriRewrites[$sSeoPageUri];
+
+        if ($sSeoParamName && $sSeoParamValue) { // process page with SEO param
+            $sPageName = BxDolPageQuery::getPageObjectNameByURI($sPageUri);
+            $aPage = $sPageName ? BxDolPageQuery::getPageObject($sPageName) : false;
+            if ($aPage) {
+                $sPageModule = $aPage['module'];
+                $sContentInfo = !empty($aPage['content_info']) ? $aPage['content_info'] : $aPage['module'];
+                if ('id' == $sSeoParamName) {
+                    $oContentInfo = BxDolContentInfo::getObjectInstance($sContentInfo);
+                    $sSeoTitle = $oContentInfo ? $oContentInfo->getContentTitle($sSeoParamValue) : '';
+                    if (!$sSeoTitle) 
+                        $sSeoTitle = self::getSeoHash($sSeoParamValue);
+                }
+                elseif ('profile_id' == $sSeoParamName) {
+                    $oProfile = BxDolProfile::getInstance($sSeoParamValue);
+                    $sSeoTitle = $oProfile ? $oProfile->getDisplayName() : self::getSeoHash($sSeoParamValue);
+                }
+                
+                $r = BxDolPageQuery::getSeoLink($sPageModule, $sPageUri, ['param_value' => $sSeoParamValue]);
+                if (!$r) {
+                    $sSeoTitleLimited = BxTemplFunctions::getInstance()->getStringWithLimitedLength($sSeoTitle, 45);
+                    $sUri = uriGenerate ($sSeoTitleLimited, 'sys_seo_links', 'uri', ['cond' => ['module' => $sPageModule, 'page_uri' => $sPageUri]]);
+
+                    bx_alert('system', 'uri_generate', 0, false, [
+                        'module' => $sPageModule,
+                        'page_uri' =>$sPageUri,
+                        'param_name' => $sSeoParamName,
+                        'param_value' => $sSeoParamValue,
+                        'title' => $sSeoTitle,
+                        'uri' => &$sUri,
+                    ]);
+
+                    BxDolPageQuery::insertSeoLink($sPageModule, $sPageUri, $sSeoParamName, $sSeoParamValue, $sUri);
+                }
+                elseif ($r['param_name'] == $sSeoParamName) {
+                    $sUri = $r['uri'];
+                } 
+                else {
+                    $sUri = false;
+                }
+
+                if ($sUri)
+                    $sSeoPageUri .= '/' . $sUri;
+                else
+                    $sSeoPageUri = false;
+            }
+        }
+
+        if (!$sSeoPageUri)
+            return false;
+
+        $sLink = $sPrefix . bx_append_url_params($sSeoPageUri, array_merge($aQueryParams, $aParams), false);
+        if (defined('BX_MULTISITE_MODULE')) {
+
+            $s = BxDolPage::multisiteLinkCheck ($sLink, $sPageUri, $sPageModule, [$sSeoParamName => $sSeoParamValue]);
+            if (false !== $s) 
+                return $s;
+        }
+        return $sLink;
+    }
+
+	/**
+     * Transform SEO regular link into regular link with permalinks off.
+     * @param $sSeoLink SEO link
+     * @param $sPrefix prefix to add to the final URL, usually BX_DOL_URL_ROOT
+     * @return unSEO link string on success, false if no transform is needed
+     */
+    static public function untransformSeoLink ($sSeoLink, $sPrefix)
+    {
+        // check for standard links first
+        if (preg_match('/^(page\/|s\/|m\/|modules\/|page\.php|storage\.php)/', $sSeoLink))
+            return false;
+
+        // parse link
+        $aParts = parse_url($sSeoLink);
+        if (!$aParts || empty($aParts['path']))
+            return false;
+
+        $aUris = explode('/', trim($aParts['path'], '/'));
+
+        // check if link starts with page URI and page with this URI exists 
+        if (!$aUris || empty($aUris[0]) || !($sPageName = BxDolPageQuery::getPageObjectNameByURI($aUris[0], false, true)))
+            return false;
+
+        $aPage = BxDolPageQuery::getPageObject ($sPageName);
+
+        // make final URL
+        $s = 'page.php?i=' . $aPage['uri'];
+
+        // add params
+        if (!empty($aUris[1])) {
+            if ($aPage) {
+                $r = BxDolPageQuery::getSeoLink($aPage['module'], $aPage['uri'], ['uri' => urldecode($aUris[1])]);     
+                if (!$r)
+                    return false;
+                $s .= '&' . $r['param_name'] . '=' .  $r['param_value'];
+            }
+        }
+
+        return $s . (!empty($aParts['query']) ? '&' . $aParts['query'] : '');
+    }
+
+	/**
+     * Perform SEO redorect from regular pages if needed
+     * @param $sSeoLink SEO link
+     * @param $sPrefix prefix to add to the final URL, usually BX_DOL_URL_ROOT
+     * @return unSEO link string on success, false if no transform is needed
+     */
+    static public function seoRedirect ()
+    {
+        if (!getParam('permalinks_seo_links'))
+            return;
+
+        list($sPageLink, $aPageParams) = bx_get_base_url_inline();
+        $sLink = bx_append_url_params($sPageLink, $aPageParams);
+        if (0 === strpos($sLink, BX_DOL_URL_ROOT))
+            $sLink = substr($sLink, strlen(BX_DOL_URL_ROOT));
+
+        if (defined('BX_MULTISITE_MODULE')) {
+            $s = BxDolPage::multisiteLinkCheck ($sLink);
+            if (false !== $s) {
+                header('Location:' . $s, true, 301);
+                exit;
+            }
+        }
+
+        if ($sSeoLink = self::transformSeoLink ($sLink, BX_DOL_URL_ROOT)) {
+            header("Location:{$sSeoLink}", true, 301);
+            exit;
+        }
+    }
+
+    static public function getSeoHash($s)
+    {
+        return base_convert(substr(md5($s), -8), 16, 36);
+    }
+    
+    /**
+     * Static method to Get embed code
+     * @return string.
+     */
+    static public function getEmbedData ($sUrl)
+    {
+        $sUrl = urldecode($sUrl);
+        $aParams = [];
+        
+        $aUrl = bx_get_base_url($sUrl);
+        if (!isset($aUrl[1]['i'])){
+            return [];
+        }
+            
+        $sUri = $aUrl[1]['i'];
+        unset($aUrl[1]['i']);
+        $aParams = $aUrl[1];
+
+        $sAuthorName = $sAuthorUrl = $sThumb = '';
+        if (isset($aParams['id'])){
+            $sContentInfoObject = BxDolPageQuery::getContentInfoObjectNameByURI($sUri);
+            $oContentInfo = BxDolContentInfo::getObjectInstance($sContentInfoObject);
+
+            $sTitle = $oContentInfo->getContentTitle($aParams['id']);
+            $iAuthor = $oContentInfo->getContentAuthor($aParams['id']);
+            $sAuthorName = BxDolProfile::getInstance($iAuthor)->getDisplayName();
+            $sAuthorUrl = BxDolProfile::getInstance($iAuthor)->getUrl();
+            $sThumb = $oContentInfo->getContentThumb($aParams['id']);
+            $sHtml = $oContentInfo->getContentEmbed($aParams['id']);
+
+        }
+        else{
+            $oPage = BxDolPage::getObjectInstanceByURI($sUri, false, true);
+            $aPage = $oPage->getObject();
+            $sTitle = $oPage->_getPageTitle();
+            $sHtml = BxDolTemplate::getInstance()->parseHtmlByName('embed.html', [
+                'title' => $sTitle,
+                'url' => BX_DOL_URL_ROOT . 'page.php?a=embed&o=' . $oPage->getName()
+            ]);
+        }
+        return ['url' => $sUrl, 'title' => $sTitle, 'author_name' => $sAuthorName, 'author_url' => $sAuthorUrl, 'thumbnail_url' => $sThumb, 'html' => $sHtml];
+    }
+
+    /**
+     * Display complete page
+     */
+    public function displayPage ($oTemplate = null)
+    {
+        $sSessionKey = 'sys_entrance_url';
+        $oSession = BxDolSession::getInstance();
+        if(!$oSession->getValue($sSessionKey)) {
+            list($sPageLink, $aPageParams) = bx_get_base_url_inline();
+
+            $oSession->setValue($sSessionKey, bx_append_url_params($sPageLink, array_intersect_key($aPageParams, array_flip(['i', 'id', 'profile_id']))));
+        }
+
+        if(isLogged())
+            BxDolAccount::getInstance()->isNeedChangePassword();
+
+        if ($this->isLockedFromUnauthenticated ($this->_aObject['uri'])) {
+            $this->redirectToLoginForm();
+            exit;
+        }
+
+        if(!$oTemplate)
+            $oTemplate = BxDolTemplate::getInstance();
+
+        $oTemplate->setPageNameIndex (BX_PAGE_DEFAULT);
+        $oTemplate->setPageUrl($this->_aObject['url']);
+        $oTemplate->setPageType ($this->getType());
+        $oTemplate->setPageInjections ($this->getInjections());
+        $oTemplate->setPageContent ('page_main_code', $this->getCode());
+        $oTemplate->getPageCode();
+    }
+
+    public function getId ()
+    {
+    	return (int)$this->_aObject['id'];
+    }
+
+    public function getType ()
+    {
+        if (defined('BX_MULTISITE_FORCE_PAGETYPE') && defined('BX_MULTISITE_MODULE')) {
+
+            list($sPageLink, $aPageParams) = bx_get_base_url_inline();
+            $sLink = bx_append_url_params($sPageLink, $aPageParams);
+            if (0 === strpos($sLink, BX_DOL_URL_ROOT))
+                $sLink = substr($sLink, strlen(BX_DOL_URL_ROOT));
+
+            if (BxDolPage::getMultisite ($sLink))
+                return BX_MULTISITE_FORCE_PAGETYPE;
+        }
+
+        return (int)$this->_aObject['type_id'];
+    }
+
+    public function getModule ()
+    {
+    	return $this->_aObject['module'];
+    }
+    
+    public function getSubMenu ()
+    {
+        return $this->_aObject['submenu'];
+    }
+
+    public function getMetaDescription ()
+    {
+        return $this->_aObject['meta_description'];
+    }
+
+    public function getName ()
+    {
+    	return $this->_sObject;
+    }
+    
+    public function getObject ()
+    {
+    	return $this->_aObject;
+    }
+    
+    public function getInjections()
+    {
+        $aResult = array();
+
+        foreach($this->_aObject as $sKey => $sValue)
+            if(strpos($sKey, 'inj_') === 0)
+                $aResult[substr($sKey, 4)] = $sValue;
+
+        return $aResult;
+    }
+
+    public function setTitle ($sTitle)
+    {
+        return $this->_aObject['title'] = $sTitle;
+    }
+
+    /**
+     * Add replace markers. Markers are replaced in raw, html, lang blocks and page title, description, keywords and block titles.
+     * @param $a array of markers as key => value
+     * @return true on success or false on error
+     */
+    public function addMarkers ($a)
+    {
+        if (empty($a) || !is_array($a))
+            return false;
+        $this->_aMarkers = array_merge ($this->_aMarkers, $a);
+        return true;
+    }
+
+    public function isAvailablePage ()
+    {
+        return $this->_isAvailablePage($this->_aObject);
+    }
+
+    public function isVisiblePage ()
+    {
+        return $this->_isVisiblePage($this->_aObject);
+    }
+
+    /**
+     * Check if page can be seen by viewer.
+     * @param array $a - page object.
+     * @return type boolean (true or false).
+     */
+    public function isPage()
+    {
+        return $this->_isAvailablePage($this->_aObject) === true && $this->_isVisiblePage($this->_aObject) === true;
+    }
+
+    public function isVisiblePageSubmenu ()
+    {
+        return $this->_aObject['submenu'] != 'disabled';
+    }
+
+    public function isEditAllowed ()
+    {
+        return false;
+    }
+
+    public function isDeleteAllowed ()
+    {
+        return false;
+    }
+
+    /**
+     * Replace provided markers in a string
+     * @param $mixed string or array to replace markers in
+     * @return string where all occured markers are replaced
+     */
+    protected function _replaceMarkers ($mixed, $aAdditionalMarkers = array())
+    {
+        return bx_replace_markers($mixed, array_merge($this->_aMarkers, $aAdditionalMarkers));
+    }
+
+    /**
+     * Check if page block is visible.
+     */
+    protected function _isVisibleBlock ($a)
+    {
+        return BxDolAcl::getInstance()->isMemberLevelInSet($a['visible_for_levels']);
+    }
+
+    /**
+     * Check if page is available.
+     * @param type $a - page object.
+     * @return boolean true if available, false or a string with error message otherwise.
+     */
+    protected function _isAvailablePage ($a)
+    {
+        return true;
+    }
+
+    /**
+     * Check if page is visible.
+     * @param array $a - page object.
+     * @return boolean true if visible, false or a string with error message otherwise.
+     */
+    protected function _isVisiblePage ($a)
+    {
+        return isAdmin() || BxDolAcl::getInstance()->isMemberLevelInSet($a['visible_for_levels']);
+    }
+
+    public static function isLockedFromUnauthenticated ($sUri)
+    {
+        if (!isLogged() && getParam('sys_lock_from_unauthenticated') && !defined('BX_DOL_CRON_EXECUTE')) {
+            $aURIs = explode(',', getParam('sys_lock_from_unauthenticated_exceptions'));
+            array_walk($aURIs, function (&$sVal) {
+                $sVal = trim($sVal);
+            });
+            $aI = array_combine($aURIs, array_fill(0, count($aURIs), 1));
+            if (!preg_match('/\/oauth2\//', $_SERVER['REQUEST_URI']) /*&& !preg_match('/searchKeyword.php$/', $_SERVER['PHP_SELF'])*/ && !preg_match('/member.php$/', $_SERVER['PHP_SELF']) && !isset($aI[$sUri])) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    public static function redirectToLoginForm ()
+    {
+        header("Location: " . bx_append_url_params(bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=login')), ['relocate' => bx_get_self_url()]));
+        exit;
+    }
+}
+
+/** @} */

+ 393 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolPaginate.php

@@ -0,0 +1,393 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+define('BX_DOL_PAGINATE_PER_PAGE_DEFAULT', 10);
+
+/**
+ * Paginage for any content.
+ * It is used to create paginate, configuring it via input parameters.
+ * Paginate don't support total number of pages, moreover it is not recommended to count all records - it slows down the site.
+ * To correctly determine last page we need to pass number of available records on the current page plus one - so we always know if next page is available.
+ *
+ *
+ * Two types of paginate presentation is supported:
+ * - getPaginate() - to get default paginate, it is better to use it on the whole page.
+ * - getSimplePaginate() - to get limited paginate, it is better to use in some boxes, where availabel space is limited or for ajax paginate.
+ *
+ *
+ * The list of available input parameters:
+ *
+ *
+ * Parameters:
+ * - start - position of the first item.
+ * - num - number of available items on the page, it should be number of items per page + 1 (+1 is needed to correctly determine last page). It is possible to set this value automatically @see setNumFromDataArray.
+ * - per_page - number of items displayed on the page.
+ * - page_url - page URL to go through pages, special markers are automatically replaced.
+ * - on_change_page - JavaScript code to be called on change page.
+ * - info - display info.
+ * - view_all_url - URL for 'view all' page. This url is not showed by default. It is convinient to use with @see getSimplePaginate.
+ * - view_all_caption - optional caption for 'view all' link.
+ *
+ *
+ * Available markers to replace in 'page_url' and 'on_change_page' parameters:
+ * - {per_page} - current number of items to display per page.
+ * - {start} - the number to display items starting from.
+ *
+ *
+ * Example of usage:
+ * @code
+ * $oPaginate = new BxDolPaginate(array(
+ *      'start' => 0,
+ *      'num' => 11,
+ *      'per_page' => 10,
+ *      'on_change_page' => 'changePage({start}, {per_page})'
+ * ));
+ * $oPaginate->getPaginate();
+ * @endcode
+ *
+ *
+ * Memberships/ACL:
+ * Doesn't depend on user's membership.
+ *
+ *
+ * Alerts:
+ * no alerts available
+ *
+ */
+abstract class BxDolPaginate extends BxDol
+{
+    protected static $_isCssAdded = false;
+
+    protected $_oTemplate;
+
+    protected $_aParams; ///< an array with initially provided params
+
+    protected $_iStart; ///< start display items from this number
+    protected $_iPerPage; ///< results per page
+    protected $_iNum; ///< available results, you need to query per_page + 1 results, so paginate can determine last page
+
+    protected $_iTotal; ///< total number of results. (Optional)
+    protected $_bTotal; ///< show displayed items info with total value
+
+    protected $_sPageUrl; ///< page url of next/prev page, special markers will be replaced here automatically
+    protected $_sOnChangePage; ///< on click of next/prev page, special markers will be replaced here automatically
+    protected $_bInfo; ///< show displayed items info
+
+    protected $_sViewAllUrl; ///< view "all results" url, for "simple" paginate
+    protected $_sViewAllCaption; ///< 'view all' link caption
+
+    protected $_sButtonsClass; ///< add this class to buttons class attribute
+    protected $_sPaginateClass; ///< add this class to whole paginate container div
+
+    /**
+     * Constructor
+     */
+    public function __construct($aParams, $oTemplate = null)
+    {
+        parent::__construct();
+
+        if ($oTemplate)
+            $this->_oTemplate = $oTemplate;
+        else
+            $this->_oTemplate = BxDolTemplate::getInstance();
+
+        if (isset($aParams['count']))
+           trigger_error ('Paginate "count" is deprecated - use "num" instead: ' . get_class($this), E_USER_ERROR);
+
+        //--- Main settings ---//
+        $this->_aParams = $aParams;
+
+        $this->_iStart = isset($aParams['start']) && (int)$aParams['start'] > 0 ? (int)$aParams['start'] : 0;
+        $this->_iPerPage = isset($aParams['per_page']) && (int)$aParams['per_page'] > 0 ? (int)$aParams['per_page'] : BX_DOL_PAGINATE_PER_PAGE_DEFAULT;
+        $this->_iNum = isset($aParams['num']) ? (int)$aParams['num'] : 0;
+
+        $this->_iTotal = isset($aParams['total']) ? (int)$aParams['total'] : 0;
+        $this->_bTotal = $this->_iTotal > 0;
+
+        $this->_bInfo = isset($aParams['info']) ? (bool)$aParams['info'] : true;
+        $this->_sButtonsClass = isset($aParams['buttons_class']) ? $aParams['buttons_class'] : '';
+        $this->_sViewAllUrl = isset($aParams['view_all_url']) ? $aParams['view_all_url'] : false;
+        $this->_sViewAllCaption = isset($aParams['view_all_caption']) ? $aParams['view_all_caption'] : _t('_sys_paginate_view_all');
+
+        $this->_sPaginateClass = isset($aParams['paginate_class']) ? $aParams['paginate_class'] : '';
+
+        // page url
+        $this->_sPageUrl = isset($aParams['page_url']) ? $aParams['page_url'] : BX_DOL_URL_ROOT;
+
+        // on click (js mode)
+        $this->_sOnChangePage = isset($aParams['on_change_page']) ? $aParams['on_change_page'] : '';
+    }
+
+    /**
+     * Get number of available results per page. If it is not last page,
+     * then this number is number of result per page plus one.
+     * @return integer
+     */
+    public function getNum()
+    {
+        return $this->_iNum;
+    }
+
+    /**
+     * Set number of available items on the page directly from data array.
+     * Since data array should contain additional record - we will pop last item from array automatically.
+     * @return nothing.
+     */
+    public function setNumFromDataArray(&$a, $isAutoPopLastElement = true)
+    {
+        if ($a && is_array($a))
+            $this->_iNum = count($a);
+        else
+            $this->_iNum = 0;
+
+        if ($this->_iNum > $this->_iPerPage && $isAutoPopLastElement)
+            array_pop($a);
+    }
+
+    /**
+     * Set number of available items on the page,
+     * it should be number of items per page + 1 (+1 is needed to correctly determine last page).
+     * It is possible to set this value automatically @see setNumFromDataArray.
+     * @param $i positive integer.
+     * @return true on success or false if $i param isn't correct.
+     */
+    public function setNum($i)
+    {
+        if ($i >= 0) {
+            $this->_iNum = (int)$i;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Position, the data is showing from.
+     * @return integer.
+     */
+    public function getStart()
+    {
+        return $this->_iStart;
+    }
+
+    /**
+     * Set the starting position, the data is showing from.
+     * @param $i positive integer.
+     * @return true on siccess or false on error.
+     */
+    public function setStart($i)
+    {
+        if ($i >= 0) {
+            $this->_iStart = (int)$i;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Get number of records per page.
+     * @return integer.
+     */
+    public function getPerPage()
+    {
+        return $this->_iPerPage;
+    }
+
+    /**
+     * Set number of records per page.
+     * @param $i positive integer.
+     * @return integer.
+     */
+    public function setPerPage($i)
+    {
+        if ((int)$i > 0) {
+            $this->_iPerPage = $i;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Set string to pass to 'onclick' for change page button.
+     * The following markers are replaced automatically:
+     * - {per_page}
+     * - {start}
+     *
+     * @param $i positive integer.
+     * @return integer.
+     */
+    public function setOnChangePage($s)
+    {
+        $this->_sOnChangePage = $s;
+    }
+
+    /**
+     * @return true if previous page is available, or false if not
+     */
+    public function isPrevAvail ()
+    {
+        return $this->_iStart > 0 ? true : false;
+    }
+
+    /**
+     * @return true if next page is available, or false if not
+     */
+    public function isNextAvail ()
+    {
+        return $this->_iNum > $this->_iPerPage ? true : false;
+    }
+
+    /**
+     * Get default paginate, it is better to use it on the whole page.
+     * @param $iStart - @see setStart.
+     * @param $iNum - @see setNum and @see setNumFromDataArray.
+     * @param $iPerPage - @see setPerPage.
+     * @return HTML string.
+     */
+    public function getPaginate($iStart = -1, $iNum = -1, $iPerPage = -1)
+    {
+        $this->setNum($iNum);
+        if (!$this->_iNum)
+            return '';
+
+        $this->setStart($iStart);
+        $this->setPerPage($iPerPage);
+
+        if (0 == $this->getStart() && !$this->isNextAvail ())
+            return '';
+
+        $aReplacement = $this->_getReplacement();
+
+        //--- Previous Page button ---//
+        $sPrevLnkUrl = 'javascript:void(0);';
+        $sPrevLnkClick = '';
+        $sPrevClassAdd = ' bx-btn-disabled';
+        if ($this->isPrevAvail()) {
+            $iStartPrev = $this->_iStart - $this->_iPerPage > 0 ? $this->_iStart - $this->_iPerPage : 0;
+            $aReplacementLink = array_merge($aReplacement, array('start' => $iStartPrev));
+            $sPrevLnkUrl = $this->_getPageChangeUrl($aReplacementLink);
+            $sPrevLnkClick = $this->_getPageChangeOnClick($aReplacementLink);
+            $sPrevClassAdd = '';
+        }
+
+        //--- Next Page button ---//
+        $sNextLnkUrl = 'javascript:void(0);';
+        $sNextLnkClick = '';
+        $sNextClassAdd = ' bx-btn-disabled';
+        if ($this->isNextAvail()) {
+            $aReplacementLink = array_merge($aReplacement, array('start' => $this->_iStart + $this->_iPerPage));
+            $sNextLnkUrl = $this->_getPageChangeUrl($aReplacementLink);
+            $sNextLnkClick = $this->_getPageChangeOnClick($aReplacementLink);
+            $sNextClassAdd = '';
+        }
+
+        $sClassAdd = ($this->_sButtonsClass ? ' ' . $this->_sButtonsClass : '');
+
+        $aVariables = [
+            'bx_if:info' => [
+                'condition' => $this->_bInfo && !$this->_bTotal,
+                'content' => [
+                    'text' => _t('_sys_paginate_info', $this->_iStart + 1, $this->_iStart + ($this->isNextAvail () ? $this->_iPerPage : $this->_iNum)),
+                ],
+            ],
+            'bx_if:total' => [
+                'condition' => $this->_bTotal,
+                'content' => [
+                    'text' => _t('_sys_paginate_total', $this->_iStart + 1, $this->_iStart + ($this->isNextAvail() ? $this->_iPerPage : $this->_iNum), $this->_iTotal),
+                ],
+            ],
+            'bx_if:view_all' => [
+                'condition' => (bool)$this->_sViewAllUrl,
+                'content' => [
+                    'lnk_url' => $this->_sViewAllUrl,
+                    'lnk_title' => $this->_sViewAllCaption,
+                    'lnk_content' => $this->_sViewAllCaption,
+                ],
+            ],
+            'btn_prev' => $this->_getButton('prev', [
+                'class' => $sClassAdd . $sPrevClassAdd,
+                'href' => $sPrevLnkUrl,
+                'onclick' => $sPrevLnkClick,
+            ]),
+            'btn_next' => $this->_getButton('next', [
+                'class' => $sClassAdd . $sNextClassAdd,
+                'href' => $sNextLnkUrl,
+                'onclick' => $sNextLnkClick,
+            ]),
+            'paginate_class' => $this->_sPaginateClass,
+            'bx_repeat:attrs' => [
+                ['key' => 'bx-data-start', 'value' => $this->_iStart],
+                ['key' => 'bx-data-perpage', 'value' => $this->_iPerPage]
+            ]
+        ];
+
+        $this->addCssJs();
+        return $this->_oTemplate->parseHtmlByName('paginate.html', $aVariables);
+    }
+
+    /**
+     * Get limited paginate, it is better to use in some boxes, where availabel space is limited or for ajax paginate.
+     * @param $sViewAllUrl - url to page for 'view all' link.
+     * @param $iStart - @see setStart.
+     * @param $iNum - @see setNum and @see setNumFromDataArray.
+     * @param $iPerPage - @see setPerPage.
+     * @return HTML string.
+     */
+    public function getSimplePaginate($sViewAllUrl = '', $iStart = -1, $iNum = -1, $iPerPage = -1)
+    {
+        if($sViewAllUrl)
+            $this->_sViewAllUrl = $sViewAllUrl;
+
+        if(!isset($this->_aParams['info']))
+            $this->_bInfo = false;
+
+        $this->_sButtonsClass .= ($this->_sButtonsClass ? ' ' : '') . 'bx-btn-small bx-btn-symbol-small';
+        $this->_sPaginateClass = 'bx-paginate-simple';
+
+        return $this->getPaginate($iStart, $iNum, $iPerPage);
+    }
+
+    public function addCssJs ()
+    {
+        if (self::$_isCssAdded)
+            return false;
+        $this->_oTemplate->addCss('paginate.css');
+        self::$_isCssAdded = true;
+        return true;
+    }
+
+    protected function _getReplacement()
+    {
+        return array(
+            'start' => $this->_iStart,
+            'per_page' => $this->_iPerPage,
+        );
+    }
+
+    protected function _getPageChangeUrl($aReplacement)
+    {
+        return $this->_oTemplate->parseHtmlByContent($this->_sPageUrl, $aReplacement, array('{', '}'));
+    }
+
+    protected function _getPageChangeOnClick($aReplacement)
+    {
+        return !empty($this->_sOnChangePage) ? 'onclick="javascript:' . $this->_oTemplate->parseHtmlByContent($this->_sOnChangePage, $aReplacement, array('{', '}')) . '; return false;"' : '';
+    }
+
+    protected function _getButtonIconPrev()
+    {
+        return 'angle-double-left';
+    }
+
+    protected function _getButtonIconNext()
+    {
+        return 'angle-double-right';
+    }
+}
+
+/** @} */

+ 562 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolPayments.php

@@ -0,0 +1,562 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolPayments extends BxDolFactory implements iBxDolSingleton
+{
+    protected $_oDb;
+
+    protected $_aObjects;
+    protected $_sActive;
+
+    protected function __construct()
+    {
+        parent::__construct();
+
+        $this->_oDb = new BxDolPaymentsQuery();
+
+        $this->_aObjects = $this->_oDb->getObjects();
+        $this->_sActive = getParam('sys_default_payment');
+    }
+
+    static public function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses']['BxDolPayments']))
+            $GLOBALS['bxDolClasses']['BxDolPayments'] = new BxDolPayments();
+
+        return $GLOBALS['bxDolClasses']['BxDolPayments'];
+    }
+
+    public function setActive($sActive)
+    {
+        $this->_sActive = $sActive;
+    }
+
+    public function getActive()
+    {
+    	return $this->_sActive;
+    }
+
+    public function getActiveObject()
+    {
+        if(!$this->isActive())
+            return null;
+
+    	return BxDolModule::getInstance($this->_sActive);
+    }
+
+    public function isActive()
+    {
+    	if(empty($this->_sActive))
+            return false;
+
+    	if(!BxDolModuleQuery::getInstance()->isModuleByName($this->_sActive))
+            return false;
+
+    	return true;
+    }
+
+    public function isCreditsOnly()
+    {
+    	if(empty($this->_sActive) || !BxDolRequest::serviceExists($this->_sActive, 'is_credits_only'))
+            return false;
+
+        return bx_srv($this->_sActive, 'is_credits_only');
+    }
+
+    public function getDetailsUrl()
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_details_url', 'Details'))
+            return '';
+
+    	return bx_srv($this->_sActive, 'get_details_url', array(), 'Details');
+    }
+
+    public function getCurrencyInfo($iVendorId = 0)
+    {
+    	if(empty($this->_sActive) || !BxDolRequest::serviceExists($this->_sActive, 'get_currency_info'))
+            return [];
+
+    	$aSrvParams = [$iVendorId];
+        return bx_srv($this->_sActive, 'get_currency_info', $aSrvParams);
+    }
+    
+    public function getCurrencyCode($iVendorId = 0)
+    {
+    	if(empty($this->_sActive) || !BxDolRequest::serviceExists($this->_sActive, 'get_currency_code'))
+            return '';
+
+    	$aSrvParams = [$iVendorId];
+        return bx_srv($this->_sActive, 'get_currency_code', $aSrvParams);
+    }
+
+    public function getCurrencySign($iVendorId = 0)
+    {
+    	if(empty($this->_sActive) || !BxDolRequest::serviceExists($this->_sActive, 'get_currency_sign'))
+            return '';
+
+    	$aSrvParams = [$iVendorId];
+        return bx_srv($this->_sActive, 'get_currency_sign', $aSrvParams);
+    }
+
+    public function convert($fAmount, $sCurrencyFrom, $sCurrencyTo)
+    {
+        if(empty($this->_sActive) || !BxDolRequest::serviceExists($this->_sActive, 'convert'))
+            return false;
+
+    	$aSrvParams = [$fAmount, $sCurrencyFrom, $sCurrencyTo];
+        return bx_srv($this->_sActive, 'convert', $aSrvParams);
+    }
+
+    public function isAcceptingPayments($iVendorId, $sPaymentType = '')
+    {
+    	if(empty($this->_sActive) || !BxDolRequest::serviceExists($this->_sActive, 'is_accepting_payments'))
+            return false;
+
+    	$aSrvParams = array($iVendorId, $sPaymentType);
+        return bx_srv($this->_sActive, 'is_accepting_payments', $aSrvParams);
+    }
+
+    /**
+     * @deprecated since version 11.0.0
+     * 
+     * @see BxDolPayments::isProviderOptions
+     */
+    public function isPaymentProvider($iVendorId, $sVendorProvider, $sPaymentType = '')
+    {
+    	return $this->isProviderOptions($iVendorId, $sVendorProvider);
+    }
+
+    /**
+     * @deprecated since version 11.0.0
+     * 
+     * @see BxDolPayments::getProviderOptions
+     */
+    public function getPaymentProvider($iVendorId, $sVendorProvider, $sPaymentType = '')
+    {
+    	return $this->getProviderOptions($iVendorId, $sVendorProvider, $sPaymentType);
+    }
+
+    public function isProviderOptions($iVendorId, $sVendorProvider, $sPaymentType = '')
+    {
+        if(empty($this->_sActive) || !BxDolRequest::serviceExists($this->_sActive, 'is_provider_options'))
+            return false;
+
+        $aSrvParams = array($iVendorId, $sVendorProvider, $sPaymentType);
+        return bx_srv($this->_sActive, 'is_provider_options', $aSrvParams);
+    }
+
+    public function getProvider($sProvider, $mixedVendorId = BX_PAYMENT_EMPTY_ID)
+    {
+        if(empty($this->_sActive) || !BxDolRequest::serviceExists($this->_sActive, 'get_provider'))
+            return false;
+
+        $aSrvParams = array($sProvider, $mixedVendorId);
+        return bx_srv($this->_sActive, 'get_provider', $aSrvParams);
+    }
+
+    public function getProviderOptions($iVendorId, $sVendorProvider, $sPaymentType = '')
+    {
+        if(empty($this->_sActive) || !BxDolRequest::serviceExists($this->_sActive, 'get_provider_options'))
+            return false;
+
+        $aSrvParams = array($iVendorId, $sVendorProvider, $sPaymentType);
+        return bx_srv($this->_sActive, 'get_provider_options', $aSrvParams);
+    }
+
+    public function getModulesWithPayments()
+    {
+        if(empty($this->_sActive) || !BxDolRequest::serviceExists($this->_sActive, 'get_modules_with_payments'))
+            return false;
+
+        return bx_srv($this->_sActive, 'get_modules_with_payments', array());
+    }
+
+    public function getProductsNames($iVendor = 0, $iLimit = 1000)
+    {
+        $aRet = array();
+        $aModules = $this->getModulesWithPayments();
+        foreach ($aModules as $sModule) {
+            if(!BxDolRequest::serviceExists($sModule, 'get_products_names'))
+                continue;
+            $aKeys = bx_srv($sModule, 'get_products_names', array($iVendor, $iLimit));
+            if (!$aKeys)
+                continue;
+            $aValues = array_fill(0, count($aKeys), $sModule);
+            $aRet = array_merge($aRet, array_combine($aKeys, $aValues));
+        }
+
+        return $aRet;
+    }
+
+    public function getPayments()
+    {
+        $aPayments = array(
+            '' => _t('_Select_one')
+        );
+        foreach($this->_aObjects as $aObject) {
+            if(empty($aObject) || !is_array($aObject))
+                continue;
+
+            $aPayments[$aObject['object']] = _t($aObject['title']);
+        }
+
+        return $aPayments;
+    }
+
+    public function updateDependentModules($sModule = 'all', $bInstall = true)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'update_dependent_modules'))
+            return;
+
+        bx_srv($this->_sActive, 'update_dependent_modules', array($sModule, $bInstall));
+    }
+
+    public function getProvidersCart($iVendorId)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_providers_cart'))
+    		return array();
+
+    	$aSrvParams = array($iVendorId);
+        return bx_srv($this->_sActive, 'get_providers_cart', $aSrvParams);
+    }
+
+    public function getOption($sOption)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_option'))
+            return '';
+
+    	return bx_srv($this->_sActive, 'get_option', array($sOption));
+    }
+
+    public function getOrdersUrl()
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_orders_url', 'Orders'))
+            return '';
+
+    	return bx_srv($this->_sActive, 'get_orders_url', array(), 'Orders');
+    }
+    
+    public function getOrdersCount($sType)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'get_orders_count', 'Orders'))
+            return array();
+
+        $aSrvParams = array($sType);
+        return bx_srv($this->_sActive, 'get_orders_count', $aSrvParams, 'Orders');
+    }
+
+    public function getOrdersInfo($aConditions)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'get_orders_info', 'Orders'))
+            return array();
+
+        $aSrvParams = array($aConditions);
+        return bx_srv($this->_sActive, 'get_orders_info', $aSrvParams, 'Orders');
+    }
+
+    public function getPendingOrdersUrl()
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_pending_orders_url', 'Orders'))
+            return '';
+
+    	return bx_srv($this->_sActive, 'get_pending_orders_url', array(), 'Orders');
+    }
+
+    public function getPendingOrdersInfo($aConditions)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'get_pending_orders_info', 'Orders'))
+            return array();
+
+        $aSrvParams = array($aConditions);
+        return bx_srv($this->_sActive, 'get_pending_orders_info', $aSrvParams, 'Orders');
+    }
+
+    public function getSubscriptionOrdersInfo($aConditions)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'get_subscription_orders_info', 'Subscriptions'))
+            return array();
+
+        $aSrvParams = array($aConditions);
+        return bx_srv($this->_sActive, 'get_subscription_orders_info', $aSrvParams, 'Subscriptions');
+    }
+
+    public function getSubscriptionsInfo($aConditions, $bCheckInProvider = false)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'get_subscriptions_info', 'Subscriptions'))
+            return array();
+
+        $aSrvParams = array($aConditions, $bCheckInProvider);
+        return bx_srv($this->_sActive, 'get_subscriptions_info', $aSrvParams, 'Subscriptions');
+    }
+
+    public function getInvoicesUrl()
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_invoices_url', 'Commissions'))
+            return '';
+
+    	return bx_srv($this->_sActive, 'get_invoices_url', array(), 'Commissions');
+    }
+
+    public function getInvoicesCount($sType)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'get_invoices_count', 'Commissions'))
+            return array();
+
+        $aSrvParams = array($sType);
+        return bx_srv($this->_sActive, 'get_invoices_count', $aSrvParams, 'Commissions');
+    }
+
+    public function getCartUrl($iVendor = 0)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_cart_url', 'Cart'))
+    		return '';
+
+    	return bx_srv($this->_sActive, 'get_cart_url', array($iVendor), 'Cart');
+    }
+
+    public function getCartItemsCount()
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_cart_items_count', 'Cart'))
+            return 0;
+
+        $aSrvParams = array();
+        return bx_srv($this->_sActive, 'get_cart_items_count', $aSrvParams, 'Cart');
+    }
+    
+    public function getCartItems($iVendorId, $iModuleId)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_cart_items', 'Cart'))
+            return 0;
+
+        $aSrvParams = array($iVendorId, $iModuleId);
+        return bx_srv($this->_sActive, 'get_cart_items', $aSrvParams, 'Cart');
+    }
+
+    public function getCartItemDescriptor($iVendorId, $iModuleId, $iItemId, $iItemCount = false)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_cart_item_descriptor', 'Cart'))
+            return '';
+
+    	$aSrvParams = array($iVendorId, $iModuleId, $iItemId, $iItemCount);
+        return bx_srv($this->_sActive, 'get_cart_item_descriptor', $aSrvParams, 'Cart');
+    }
+
+    /**
+     * Returns cart JavaScript code which should be included in the page to make "Add To Cart" and "Subscribe" buttons work properly.
+     */
+    public function getCartJs($sType = '', $iVendorId = 0)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_cart_js', 'Cart'))
+            return '';
+
+        $aSrvParams = array($sType, $iVendorId);
+        return bx_srv($this->_sActive, 'get_cart_js', $aSrvParams, 'Cart');
+    }
+
+    /**
+     * Adds an item to cart in background mode.
+     */
+	public function addToCart($iVendorId, $mixedModuleId, $iItemId, $iItemCount, $aCustom = array())
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'add_to_cart', 'Cart'))
+            return array();
+
+        $aSrvParams = array($iVendorId, $mixedModuleId, $iItemId, $iItemCount, $aCustom);
+        return bx_srv($this->_sActive, 'add_to_cart', $aSrvParams, 'Cart');
+    }
+
+    /**
+     * Returns "Add To Cart" JavaScript code to use in onclick attribute.
+     */
+    public function getAddToCartJs($iVendorId, $mixedModuleId, $iItemId, $iItemCount, $bNeedRedirect = false, $aCustom = array())
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_add_to_cart_js', 'Cart'))
+            return array();
+
+        $aSrvParams = array($iVendorId, $mixedModuleId, $iItemId, $iItemCount, $bNeedRedirect, $aCustom);
+        return bx_srv($this->_sActive, 'get_add_to_cart_js', $aSrvParams, 'Cart');
+    }
+
+    /**
+     * Returns the whole "Add To Cart" link including HTML tag BUTTON and cart JavaScript code.
+     */
+    public function getAddToCartLink($iVendorId, $mixedModuleId, $iItemId, $iItemCount, $bNeedRedirect = false, $aCustom = array())
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'get_add_to_cart_link', 'Cart'))
+            return '';
+
+        $aSrvParams = array($iVendorId, $mixedModuleId, $iItemId, $iItemCount, $bNeedRedirect, $aCustom);
+        return bx_srv($this->_sActive, 'get_add_to_cart_link', $aSrvParams, 'Cart');
+    }
+
+    /**
+     * Subscribe an item in background mode.
+     */
+    public function subscribeWithAddons($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount = 1, $sItemAddons = '', $sRedirect = '', $aCustom = array())
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'subscribe_with_addons', 'Subscriptions'))
+            return array();
+
+        $aSrvParams = array($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount, $sItemAddons, $sRedirect, $aCustom);
+        return bx_srv($this->_sActive, 'subscribe_with_addons', $aSrvParams, 'Subscriptions');
+    }
+
+    public function getSubscriptionsUrl()
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_subscriptions_url', 'Subscriptions'))
+            return '';
+
+    	return bx_srv($this->_sActive, 'get_subscriptions_url', array(), 'Subscriptions');
+    }
+
+    public function getSubscribeUrl($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount = 1)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_subscribe_url', 'Subscriptions'))
+            return '';
+
+    	return bx_srv($this->_sActive, 'get_subscribe_url', array($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount), 'Subscriptions');
+    }
+
+    /**
+     * Returns "Subscribe" JavaScript code to use in onclick attribute.
+     */
+    public function getSubscribeJs($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount = 1, $sRedirect = '', $aCustom = array())
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_subscribe_js', 'Subscriptions'))
+            return array();
+
+        $aSrvParams = array($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount, $sRedirect, $aCustom);
+        return bx_srv($this->_sActive, 'get_subscribe_js', $aSrvParams, 'Subscriptions');
+    }
+
+    /**
+     * Returns "Subscribe" JavaScript code to use in onclick attribute.
+     */
+    public function getSubscribeJsWithAddons($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount = 1, $sItemAddons = '', $sRedirect = '', $aCustom = array())
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_subscribe_js_with_addons', 'Subscriptions'))
+            return array();
+
+        $aSrvParams = array($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount, $sItemAddons, $sRedirect, $aCustom);
+        return bx_srv($this->_sActive, 'get_subscribe_js_with_addons', $aSrvParams, 'Subscriptions');
+    }
+
+    /**
+     * Returns the whole "Subscribe" link including HTML tag BUTTON and cart JavaScript code.
+     */
+    public function getSubscribeLink($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount = 1, $sRedirect = '', $aCustom = array())
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'get_subscribe_link', 'Subscriptions'))
+            return '';
+
+        $aSrvParams = array($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount, $sRedirect, $aCustom);
+        return bx_srv($this->_sActive, 'get_subscribe_link', $aSrvParams, 'Subscriptions');
+    }
+
+    public function sendSubscriptionExpirationLetters($iPendingId, $sOrderId)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'send_subscription_expiration_letters', 'Subscriptions'))
+            return '';
+
+        $aSrvParams = array($iPendingId, $sOrderId);
+        return bx_srv($this->_sActive, 'send_subscription_expiration_letters', $aSrvParams, 'Subscriptions');
+    }
+
+    public function authorizeCheckoutSingle($iVendorId, $sProvider, $aItems = array())
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'authorize_checkout'))
+            return '';
+
+        $aSrvParams = array(BX_PAYMENT_TYPE_SINGLE, $iVendorId, $sProvider, $aItems);
+        return bx_srv($this->_sActive, 'authorize_checkout', $aSrvParams);
+    }
+    
+    public function authorizeCheckoutRecurring($iVendorId, $sProvider, $aItems = array())
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'authorize_checkout'))
+            return '';
+
+        $aSrvParams = array(BX_PAYMENT_TYPE_RECURRING, $iVendorId, $sProvider, $aItems);
+        return bx_srv($this->_sActive, 'authorize_checkout', $aSrvParams);
+    }
+    
+    public function captureAuthorizedCheckoutSingle($sOrder)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'capture_authorized_checkout'))
+            return '';
+
+        $aSrvParams = array(BX_PAYMENT_TYPE_SINGLE, $sOrder);
+        return bx_srv($this->_sActive, 'capture_authorized_checkout', $aSrvParams);
+    }
+    
+    public function captureAuthorizedCheckoutRecurring($sOrder)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'capture_authorized_checkout'))
+            return '';
+
+        $aSrvParams = array(BX_PAYMENT_TYPE_RECURRING, $sOrder);
+        return bx_srv($this->_sActive, 'capture_authorized_checkout', $aSrvParams);
+    }
+
+    public function initializeCheckout($iVendorId, $sProvider, $aItems = array())
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'initialize_checkout'))
+            return '';
+
+        $aSrvParams = array($iVendorId, $sProvider, $aItems);
+        return bx_srv($this->_sActive, 'initialize_checkout', $aSrvParams);
+    }
+
+    public function cancelSubscription($sOrderId)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'cancel', 'Subscriptions'))
+            return false;
+
+        $aSrvParams = [$sOrderId];
+        return bx_srv($this->_sActive, 'cancel', $aSrvParams, 'Subscriptions');
+    }
+
+    public function processOrder($iSellerId, $iClientId, $iModuleId, $aItems, $sType, $sOrder)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'process_order', 'Orders'))
+            return false;
+
+        $aSrvParams = array($iSellerId, $iClientId, $iModuleId, $aItems, $sType, $sOrder);
+        return bx_srv($this->_sActive, 'process_order', $aSrvParams, 'Orders');
+    }
+
+    public function processOrderByPending($iPendingId, $sOrder)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'process_order_by_pending', 'Orders'))
+            return false;
+
+        $aSrvParams = array($iPendingId, $sOrder);
+        return bx_srv($this->_sActive, 'process_order_by_pending', $aSrvParams, 'Orders');
+    }
+
+    public function refundOrder($sOrder)
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'refund_order', 'Orders'))
+            return false;
+
+        $aSrvParams = array($sOrder);
+        return bx_srv($this->_sActive, 'refund_order', $aSrvParams, 'Orders');
+    }
+
+    public function generateLicense()
+    {
+        if(!BxDolRequest::serviceExists($this->_sActive, 'generate_license'))
+            return '';
+
+        $aSrvParams = array();
+        return bx_srv($this->_sActive, 'generate_license', $aSrvParams, 'Module', true);
+    }
+}
+
+/** @} */

+ 1299 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolSearch.php

@@ -0,0 +1,1299 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+define('BX_DOL_SEARCH_RESULTS_PER_PAGE_DEFAULT', 10);
+define('BX_DOL_SEARCH_KEYWORD_PAGE', 'site-search-page');
+
+/**
+ * Search class for different content.
+ *
+ * Allows present content in main search area for all modules.
+ *
+ * To add taking part content from your module to search area you need add a record to `sys_objects_search` table:
+ * @code
+ *   ID - autoincremented id for internal usage
+ *   ObjectName - your unique module name, with vendor prefix, lowercase and spaces are underscored
+ *   Title - language key for module's choicer in global search form
+ *   ClassName - class name which is responsible for search process ad showing result for found results
+ *   ClassPath - file where your ClassName is stored (if class is stored in template folder then you can use key {tmpl})
+ * @endcode
+ *
+ * For using this class you should have unit of BxDolSearchResult class (see description below) with method processing which is responsible for processing of search request.
+ *
+ * Example of usage can be seen in the BoonEx modules, for example: Posts
+ *
+ * Memberships/ACL:
+ * Doesn't depend on user's membership.
+ *
+ * Alerts:
+ * no alerts available
+ *
+ */
+class BxDolSearch extends BxDol
+{
+    protected $_bIsApi;
+
+    protected $aClasses = array(); ///< array of all search classes
+    protected $aChoice  = array(); ///< array of current search classes which were choosen in search area
+    protected $_bRawProcessing = false; ///< display search results without design box and paginate
+    protected $_bDataProcessing = false; ///< return result as array of data
+
+    protected $_bLiveSearch = false;
+    protected $_sMetaType = '';
+    protected $_sCategoryObject = ''; ///< by default = object from sys_objects_category table, for multicategories set to 'multi' string 
+    protected $_aCustomSearchCondition = array();
+    protected $_aCustomCurrentCondition = array();
+    protected $_sUnitTemplate = '';
+
+    /**
+     * Constructor
+     * @param array $aChoice - array of choosen classes (will take a part only existing in `sys_objects_search` table)
+     */
+    public function __construct ($aChoice = '')
+    {
+        parent::__construct();
+
+        $this->_bIsApi = bx_is_api();
+
+        $this->aClasses = BxDolDb::getInstance()->fromCache('sys_objects_search', 'getAllWithKey',
+           'SELECT `ID` as `id`,
+                   `Title` as `title`,
+                   `ClassName` as `class`,
+                   `ClassPath` as `file`,
+                   `ObjectName`,
+                   `GlobalSearch`
+            FROM `sys_objects_search`
+            ORDER BY `Order` ASC', 'ObjectName'
+        );
+
+        if (is_array($aChoice) && !empty($aChoice)) {
+            foreach ($aChoice as $sValue) {
+                if (isset($this->aClasses[$sValue]))
+                    $this->aChoice[$sValue] = $this->aClasses[$sValue];
+            }
+        } else {
+            $this->aChoice = $this->aClasses;
+        }
+    }
+
+    /**
+     * create units for all classes and calling their processing methods
+     */
+    public function response ()
+    {
+        $sCode = $this->_bDataProcessing ? array() : '';
+
+        $bSingle = count($this->aChoice) == 1;
+        foreach($this->aChoice as $sKey => $aValue) {
+            if(!$this->_sMetaType && !$aValue['GlobalSearch'])
+                continue;
+
+            $sClassName = 'BxTemplSearchResult';
+            if(!empty($aValue['class'])) {
+                $sClassName = $aValue['class'];
+                if(!empty($aValue['file']))
+                    require_once(BX_DIRECTORY_PATH_ROOT . $aValue['file']);
+            }
+
+            $oEx = new $sClassName();            
+            if($this->_sMetaType && !$oEx->isMetaEnabled($this->_sMetaType))
+                continue;
+
+            $oEx->setId($aValue['id']);
+            $oEx->setLiveSearch($this->_bLiveSearch);
+            $oEx->setMetaType($this->_sMetaType);
+            $oEx->setCategoryObject($this->_sCategoryObject);
+            $oEx->setCenterContentUnitSelector(false);
+            $oEx->setSingleSearch($bSingle);
+            $oEx->setCustomSearchCondition($this->_aCustomSearchCondition);
+            
+            $oEx->aCurrent = array_merge_recursive($oEx->aCurrent, $this->_aCustomCurrentCondition);
+            if($this->_sUnitTemplate)
+                $oEx->setUnitTemplate($this->_sUnitTemplate);
+
+            if($this->_bDataProcessing) {
+                if($this->_bIsApi) {
+                    if($bSingle)
+                        $sCode = $oEx->decodeDataAPI($oEx->getSearchData());
+                    else
+                        $sCode[$sKey] = $oEx->getSearchQuery($sKey);
+                }
+                else
+                    $sCode[$sKey] = $oEx->getSearchData();
+            }
+            else
+                $sCode .= $this->_bRawProcessing ? $oEx->processingRaw() : $oEx->processing();
+        }
+
+        if($this->_bIsApi && !$bSingle && is_array($sCode)) {
+            $bExtendedUnits = getParam('sys_api_extended_units') == 'on';
+
+            $aQueries = [];
+            foreach($sCode as $aQuery)
+                if(!empty($aQuery['query']))
+                    $aQueries[] = $aQuery['query'];
+
+            $aItems = BxDolDb::getInstance()->getAll('(' . implode(') UNION (', $aQueries) . ') ORDER BY `added` DESC ' . current($sCode)['limit']);
+
+            $sCode = [];
+            foreach($aItems as $aItem)
+                if(($oContentInfo = BxDolContentInfo::getObjectInstance($aItem['content_info'])) !== false)
+                    $sCode[] = $oContentInfo->getContentInfoAPI($aItem['id'], $bExtendedUnits);
+        }
+
+        return $sCode;
+    }
+
+    public function getEmptyResult ()
+    {
+        $sKey = _t('_Empty');
+        return DesignBoxContent($sKey, MsgBox($sKey), BX_DB_PADDING_DEF);
+    }
+
+    protected function getKeyTitlesPairs ()
+    {
+        $a = array();
+        foreach ($this->aChoice as $sKey => $r)
+            if ($this->_sMetaType || $r['GlobalSearch'])
+                $a[$sKey] = _t($r['title']);
+        return $a;
+    }
+
+    public function setLiveSearch($bLiveSearch)
+    {
+        $this->_bLiveSearch = $bLiveSearch;
+    }
+
+    public function setMetaType($s)
+    {
+        $aMetaTypes = array('location_country', 'location_country_city', 'location_country_state', 'mention', 'keyword');
+        if (in_array($s, $aMetaTypes))
+            $this->_sMetaType = $s;
+    }
+    
+    public function setCategoryObject($s)
+    {
+        $this->_sCategoryObject = $s;
+    }
+
+    /**
+     * Set custom search condition to use istead of GET/POST variables
+     * @param $a array of params, such as 'keyword', 'state', 'city'
+     * @return nothing
+     */
+    public function setCustomSearchCondition($a)
+    {
+        $this->_aCustomSearchCondition = $a;
+    }
+
+    /**
+     * Set custom data for aCurrent array
+     * @param $a array of params
+     * @return nothing
+     */
+    public function setCustomCurrentCondition($a)
+    {
+        $this->_aCustomCurrentCondition = $a;
+    }
+
+    /**
+     * Set custom unit template
+     * @param $s template name
+     * @return nothing
+     */
+    public function setUnitTemplate($s)
+    {
+        $this->_sUnitTemplate = $s;
+    }
+    
+    /**
+     * Display search results without design box and paginate
+     */ 
+    public function setRawProcessing($b)
+    {
+        $this->_bRawProcessing = $b;
+    }
+
+    /**
+     * Return search result as array of data
+     */ 
+    public function setDataProcessing($b)
+    {
+        $this->_bDataProcessing = $b;
+    }
+}
+
+/*
+ * Search class for processing search requests and displaying search results.
+ *
+ * Allows present content from modules on search params or internal (via fields of class) conditions.
+ *
+ * Example of usage (you can see example in any of default Dolhpin's modules):
+ *
+ * 1. Extends your own search class from this one (or from BxBaseSearchResult or BxBaseSearchResultSharedMedia classes)
+ * 2. Set necessary fields of class (using as example BxFilesSearch):
+ *
+ * @code
+ *
+ *  // main field of search class
+ *  $this->aCurrent = array(
+ *
+ *      // name of module
+ *      'name' => 'bx_files',
+ *
+ *      // language key with name of module
+ *      'title' => '_bx_files',
+ *
+ *      // main content table
+ *      'table' => 'bx_files_main',
+ *
+ *      // array of all fields which can be choosen for select in result query
+ *      'ownFields' => array('ID', 'Title', 'Uri', 'Desc', 'Date', 'Size', 'Ext', 'Views', 'Rate', 'RateCount', 'Type'),
+ *
+ *      // array of fields which take a part in search by keyword (global search and related words only - in other cases leave it blank)
+ *      'searchFields' => array('Title', 'Tags', 'Desc', 'Categories'),
+ *
+ *      // array of join tables
+ *      //   'type' - type of join
+ *      //   'table' - join table
+ *      //   'mainField' - field from main table for 'on' condition
+ *      //   'onField' - field from joining table for 'on' condition
+ *      //   'joinFields' - array of fields from joining table
+ *
+ *      'join' => array(
+ *          'profile' => array(
+ *              'type' => 'left',
+ *              'table' => 'Profiles',
+ *              'mainField' => 'Owner',
+ *              'onField' => 'ID',
+ *              'joinFields' => array('NickName'),
+ *          ),
+ *          // ...
+ *      ),
+ *
+ *      // array of search parameters
+ *      //   'value' - value of search parameter (can be number, string or array)
+ *      //   'field' - field which will take value of search, may be lefy as blank then in search query will be pasted to WHERE conditon via operator AGAINST
+ *      //   'operator' - operator between field and value (can be 'in', 'not in', '>', '<', 'against', 'like' and '=' by default),
+ *      //   'paramName' - GET param which will keep 'value' for pagianation
+ *
+ *      'restriction' => array(
+ *          'activeStatus' => array('value'=>'approved', 'field'=>'Status', 'operator'=>'=', 'paramName' => 'status'),
+ *          'albumType' => array('value'=>'', 'field'=>'Type', 'operator'=>'=', 'paramName'=>'albumType', 'table'=>'sys_albums'),
+ *      ),
+ *
+ *      // array of pagination
+ *      //   'perPage' - units per page
+ *      //   'start' - show search results starting from this number
+ *      //   'num' - number of element to show on current page + 1
+ *
+ *      'paginate' => array('perPage' => 10, 'start' => 0, 'num' => 11),
+ *
+ *      // sort mode - by default it was last (DESC by date)
+ *      'sorting' => 'last',
+ *
+ *      // mode of units presentation in search result, can be 'short' or 'full' view
+ *      'view' => 'full',
+ *
+ *      // field of id
+ *      'ident' => 'ID',
+ *
+ *      // rss feed array
+ *      'rss' => array(
+ *          'title' => '',
+ *          'link' => '',
+ *          'image' => '',
+ *          'profile' => 0,
+ *          'fields' => array (
+ *              'Link' => '',
+ *              'Title' => 'Title',
+ *              'DateTimeUTS' => 'Date',
+ *              'Desc' => 'Desc',
+ *              'Photo' => '',
+ *          ),
+ *      ),
+ *  );
+ *
+ *  // array of fields renamings
+ *  $aPseud - filling in costructor by function _getPseud
+ *
+ *  // unique identificator from `sys_objects_search` table
+ *  $id;
+ *
+ * @endcode
+ *
+ * Memberships/ACL:
+ * Doesn't depend on user's membership.
+ *
+ * Alerts:
+ * no alerts available
+ *
+ */
+
+class BxDolSearchResult implements iBxDolReplaceable
+{
+    public $aCurrent; ///< search results configuration
+
+    protected $aPseud; ///< array of fields renamings
+    protected $id; ///< unique identificator of block on mixed search results page
+    protected $bDisplayEmptyMsg = false; ///< display empty message instead of nothing, when no results
+    protected $sDisplayEmptyMsgKey = ''; ///< custom empty message language key, instead of default "empty" message
+    protected $bProcessPrivateContent = true; ///< check each item for privacy, if view isn't allowed then display private view instead
+    protected $aPrivateConditionsIndexes = array('restriction' => array(), 'join' => array()); ///< conditions indexes for bProcessPrivateContent
+    protected $bForceAjaxPaginate = false; ///< force ajax paginate
+    protected $iPaginatePerPage = BX_DOL_SEARCH_RESULTS_PER_PAGE_DEFAULT; ///< default 'per page' value for paginate.
+
+    protected $_bIsApi;
+    protected $_bSingleSearch = true;
+    protected $_bLiveSearch = false;
+    protected $_sMetaType = '';
+    protected $_sMode = '';
+    protected $_aParams = [];
+    protected $_sCategoryObject = '';
+    protected $_aCustomSearchCondition = array();
+
+    protected $_aMarkers = array (); ///< markers to replace somewhere, usually title and browse url (defined in custom class)
+    
+    /**
+     * constructor
+     * filling identificator field
+     */
+    function __construct ()
+    {
+        $this->_bIsApi = bx_is_api();
+
+        if (isset($this->aPseud['id']))
+            $this->aCurrent['ident'] = $this->aPseud['id'];
+    }
+
+    public function getId()
+    {
+        return $this->id;
+    }
+
+    public function setId($sId)
+    {
+        $this->id = $sId;
+    }
+
+    public function getModuleName()
+    {
+        return isset($this->aCurrent['module_name']) ? $this->aCurrent['module_name'] : '';
+    }
+
+    public function getContentInfoName()
+    {
+        return isset($this->aCurrent['name']) ? $this->aCurrent['name'] : '';
+    }
+
+    public function setAjaxPaginate($b = true)
+    {
+        $this->bForceAjaxPaginate = $b;
+    }
+
+    public function setPaginatePerPage($iPerPage)
+    {
+        $this->iPaginatePerPage = $iPerPage;
+    }
+
+    public function setSingleSearch($bSingleSearch)
+    {
+        $this->_bSingleSearch = $bSingleSearch;
+    }
+
+    public function setLiveSearch($bLiveSearch)
+    {
+        $this->_bLiveSearch = $bLiveSearch;
+    }
+
+    public function setMetaType($s)
+    {
+        $this->_sMetaType = $s;
+    }
+
+    public function isMetaEnabled($s)
+    {
+        if (empty($this->aCurrent['object_metatags']))
+            return false;
+            
+        if (!($o = BxDolMetatags::getObjectInstance($this->aCurrent['object_metatags'])))
+            return false;
+
+        switch ($s) {
+            case 'location_country_city':
+            case 'location_country_state':            
+            case 'location_country':
+                return $o->locationsIsEnabled();
+            case 'mention':
+                return $o->mentionsIsEnabled();
+            case 'keyword':
+                return $o->keywordsIsEnabled();
+        }
+
+        return false;
+    }
+    
+    public function setCategoryObject($s)
+    {
+        $this->_sCategoryObject = $s;
+    }
+
+    public function setCustomSearchCondition($a)
+    {
+        $this->_aCustomSearchCondition = $a;
+    }
+    
+    public function setCustomCurrentCondition($a)
+    {
+        $this->aCurrent = array_merge_recursive($this->aCurrent, $a);
+    }
+
+    public function setCategoriesCondition($sKeyword)
+    {
+		$this->aCurrent['join']['multicat'] = array(
+            'type' => 'INNER',
+            'table' => 'sys_categories2objects',
+            'mainField' => 'id',
+            'onField' => 'object_id',
+            'joinFields' => array(),
+        );
+		
+		if (isset($this->aCurrent['tableSearch'])){
+			$this->aCurrent['join']['multicat']['mainTable'] = $this->aCurrent['tableSearch'];
+		}
+		
+        $this->aCurrent['join']['multicat2'] = array(
+           'type' => 'INNER',
+           'table' => 'sys_categories',
+           'mainField' => 'category_id',
+           'mainTable' => 'sys_categories2objects',
+           'onField' => 'id',
+           'joinFields' => array(),
+       );
+       $this->aCurrent['restriction']['multicat'] = array('value' => $sKeyword, 'field' => 'value', 'operator' => '=', 'table' => 'sys_categories');
+    }
+    
+    /**
+     * Display empty message if there is no content, custom empty message can be used.
+     * @param $b - boolan value to enable or disable 'empty' message
+     * @param $sLangKey [optional] - custom 'empty' message
+     */
+    public function setDisplayEmptyMsg($b, $sLangKey = '')
+    {
+        $this->bDisplayEmptyMsg = $b;
+        if ($sLangKey)
+           $this->sDisplayEmptyMsgKey = $sLangKey;
+    }
+
+    /**
+     * Perform privacy checking for every unit
+     * @param $b - boolan value to enable or disable privacy checking
+     */
+    public function setProcessPrivateContent ($b)
+    {
+        $this->bProcessPrivateContent = $b;
+        if ($b) {
+            // unset condition which was set when 'bProcessPrivateContent' was set to 'false'
+            foreach ($this->aPrivateConditionsIndexes['restriction'] as $sKey) 
+                unset($this->aCurrent['restriction'][$sKey]);
+            foreach ($this->aPrivateConditionsIndexes['join'] as $sKey)
+                unset($this->aCurrent['join'][$sKey]);
+        }
+    }
+
+    /**
+     * Get search results without design box and paginate
+     * @return html code
+     */
+    function processingRaw ()
+    {
+        $sCode = $this->displayResultBlock();
+        return $this->aCurrent['paginate']['num'] > 0 ? $sCode : '';
+    }
+    
+    /**
+     * Get html box of search results (usually used in grlobal search)
+     * @return html code
+     */
+    function processing ()
+    {
+        if (bx_is_api()) {
+            return $this->processingAPI();
+        }
+
+        $sCode = $this->displayResultBlock();
+        if ($this->aCurrent['paginate']['num'] > 0) {
+            $sPaginate = $this->showPagination();
+            $sCode = $this->displaySearchBox($sCode, $sPaginate);
+        }
+        else {
+            $sCode = $this->bDisplayEmptyMsg ? $this->displaySearchBox($this->displayResultBlockEmpty()) : '';
+        }
+
+        return $sCode;
+    }
+
+    function processingAPI () 
+    {
+        $sModule = 'system';
+        $sUnitType = 'content';
+        if(!empty($this->oModule)) {
+            $sModule = $this->oModule->getName();
+
+            if(method_exists($this->oModule, 'serviceActAsProfile') && $this->oModule->serviceActAsProfile())
+                 $sUnitType = 'profile';
+            if(method_exists($this->oModule, 'serviceIsGroupProfile') && $this->oModule->serviceIsGroupProfile())
+                 $sUnitType = 'context';
+        }
+
+        $sUnit =  'list';
+        if(in_array($this->sUnitViewDefault, ['showcase', 'gallery']))
+            $sUnit = 'card';
+        
+        $aData = defined('BX_API_PAGE') ? [] : $this->decodeDataAPI($this->getSearchData());
+        
+        $aParams =  [
+            'per_page' => $this->aCurrent['paginate']['perPage'],
+            'start' => $this->aCurrent['paginate']['start'],
+            'type' => $this->_sMode,
+        ];
+
+        if (isset($this->_aParams['author'])){
+            $aParams['author'] = $this->_aParams['author'];
+        }
+
+        if (isset($this->_aParams['category'])){
+            $aParams['category'] = $this->_aParams['category'];
+        }
+
+        return [
+            'module' => $sModule,
+            'unit' => 'general-' . $sUnitType . '-' . $sUnit,
+            'request_url' => !empty($this->aCurrent['api_request_url']) ? $this->aCurrent['api_request_url'] : '/api.php?r=' . $sModule . '/browse/&params[]=',
+            'data' =>  $aData,
+            'params' => $aParams
+        ];
+    }
+
+    function decodeDataAPI ($a)
+    {
+        return $a;
+    }
+
+    /**
+     * Get html output of search result
+     * @return html code
+     */
+    function displayResultBlock ()
+    {
+        $aData = $this->getSearchData();
+
+        $sCode = '';
+        if (count($aData) > 0) {
+            $sCode = $this->addCustomParts();
+            foreach ($aData as $iKey => $aValue) {
+                $sCode .= $this->displaySearchUnit($aValue);
+            }
+        }
+        return $sCode;
+    }
+
+    /**
+     * Get html output of empty search result
+     * @return html code
+     */
+    function displayResultBlockEmpty ()
+    {
+        return MsgBox(_t($this->sDisplayEmptyMsgKey ? $this->sDisplayEmptyMsgKey : '_Empty'));
+    }
+            
+    /**
+     * Add different code before html output (usually redeclared)
+     * no return result
+     */
+    function addCustomParts ()
+    {
+
+    }
+
+    /**
+     * Get XML string for rss output
+     */
+    function rss ()
+    {
+        if (!isset($this->aCurrent['rss']['fields']) || !isset($this->aCurrent['rss']['link']))
+            return '';
+
+        $aData = $this->getSearchData();
+        $f = &$this->aCurrent['rss']['fields'];
+        if ($aData) {
+            foreach ($aData as $k => $a) {
+                $aData[$k][$f['Link']] = $this->getRssUnitLink ($a);
+
+                if(isset($f['Image']))
+                    $aData[$k][$f['Image']] = $this->getRssUnitImage ($a, $f['Image']);
+            }
+        }
+
+        $oRss = new BxDolRssFactory ();
+
+        return $oRss->GenRssByCustomData(
+            $aData,
+            isset($this->aCurrent['rss']['title']) && $this->aCurrent['rss']['title'] ? $this->aCurrent['rss']['title'] : $this->aCurrent['title'],
+            $this->aCurrent['rss']['link'],
+            $this->aCurrent['rss']['fields'],
+            isset($this->aCurrent['rss']['image']) ? $this->aCurrent['rss']['image'] : '',
+            isset($this->aCurrent['rss']['profile']) ? $this->aCurrent['rss']['profile'] : 0
+        );
+    }
+
+    /**
+     * Output RSS XML with XML header
+     */
+    function outputRSS ()
+    {
+        header('Content-Type: text/xml; charset=UTF-8');
+        echo $this->rss();
+    }
+
+    /**
+     * Return rss unit link (redeclared)
+     */
+    function getRssUnitLink (&$a)
+    {
+        // override this functions to return permalink to rss unit
+    }
+
+	/**
+     * Return rss unit image (redeclared)
+     */
+    function getRssUnitImage (&$a, $sField)
+    {
+        // override this functions to return image for rss unit
+    }
+
+    /**
+     * Naming fields in query's body
+     * @param string  $sFieldName  name of field
+     * @param string  $sTableName  name of field's table
+     *                             $param string $sOperator of field's calculation (like MAX)
+     * @param boolean $bRenameMode indicator for renaming and unsetting fields from field of class $this->aPseud
+     *                             return $sqlUnit sql code and unsetting elements from aPseud field
+     */
+    function setFieldUnit ($sFieldName, $sTableName, $sOperator = '', $bRenameMode = true)
+    {
+        if (!empty($sOperator))
+            $sqlUnit  = "$sOperator(`$sTableName`.`$sFieldName`)";
+        else
+            $sqlUnit  = "`$sTableName`.`$sFieldName`";
+
+        if (isset($this->aPseud) && $bRenameMode !== false) {
+            $sKey = array_search($sFieldName, $this->aPseud);
+            if ($sKey !== false) {
+                $sqlUnit .= " as `$sKey`";
+                unset($this->aPseud[$sKey]);
+            }
+        }
+        return $sqlUnit . ', ';
+    }
+
+    /**
+     * Get html code of of every search unit
+     * @param array $aData array of every search unit
+     *                     return html code
+     */
+    function displaySearchUnit ($aData)
+    {
+
+    }
+
+    /**
+     * Get html code of search box with search results
+     * @param string $sCode html code of search results
+     *                      $param $sPaginate html code of paginate
+     *                      return html code
+     */
+    function displaySearchBox ($sCode, $sPaginate = '')
+    {
+
+    }
+
+    /**
+     * Get html code of pagination
+     */
+    function showPagination ($bAdmin = false, $bChangePage = true, $bPageReload = true)
+    {
+
+    }
+
+    /**
+     * Get array of data with search results
+     * return array with data
+     */
+    function getSearchData ()
+    {
+        bx_alert('simple_search', 'before_get_data', 0, false, [
+            'object' => &$this->aCurrent, 
+            'mode' => $this->_sMode
+        ]);
+
+        $this->aPseud = $this->_getPseud();
+        $this->setConditionParams();
+        $aData = $this->aCurrent['paginate']['num'] > 0 ? $this->getSearchDataByParams() : [];
+
+        bx_alert('simple_search', 'get_data', 0, false, [
+            'object' => &$this->aCurrent, 
+            'mode' => $this->_sMode, 
+            'search_results' => &$aData
+        ]);
+
+        return $aData;
+    }
+
+    /**
+     * Get query [query, limit] for search results. Is used for combined search from different sections.
+     * @param type $aParams array with params
+     * @return type array with query and limit
+     */
+    function getSearchQuery($sObject, $aParams = [])
+    {
+        if(!is_array($aParams))
+            $aParams = [];
+        $aParams = array_merge($aParams, ['for_union' => true]);
+
+        $this->aPseud = [
+            'id' => !empty($this->aCurrent['ident']) ? $this->aCurrent['ident'] : 'id', 
+            'added' => !empty($this->aCurrent['added']) ? $this->aCurrent['added'] : 'added'
+        ];
+
+        $this->setConditionParams();
+        $aQuery = $this->getSearchDataByParams($aParams);
+
+        bx_alert('simple_search', 'get_query', 0, false, [
+            'object' => &$this->aCurrent, 
+            'mode' => $this->_sMode, 
+            'search_object' => $sObject,
+            'search_query' => &$aQuery
+        ]);
+
+        return $aQuery;
+    }
+
+    /**
+     * Get array with code for sql elements
+     * @param $bRenameMode indicator of renmaing fields
+     * return array with joinFields, ownFields, groupBy and join elements
+     */
+    function getJoins ($bRenameMode = true)
+    {
+        $aSql = array();
+        // joinFields & join
+        if (isset($this->aCurrent['join']) && is_array($this->aCurrent['join'])) {
+            $aSql = array('join'=>'', 'ownFields'=>'', 'joinFields'=>'', 'groupBy'=>'');
+            foreach ($this->aCurrent['join'] as $sKey => $aValue) {
+                $sAlias = isset($aValue['table_alias']) ? $aValue['table_alias'] : $aValue['table'];
+                $sTableAlias = isset($aValue['table_alias']) ? " AS {$aValue['table_alias']} " : '';
+
+                if (is_array($aValue['joinFields'])) {
+                    foreach ($aValue['joinFields'] as $sValue) {
+                        $aSql['joinFields'] .= $this->setFieldUnit($sValue, $sAlias, isset($aValue['operator']) ? $aValue['operator'] : null, $bRenameMode);
+                    }
+                }
+                // group by
+                if (isset($aValue['groupTable']))
+                    $aSql['groupBy'] =  "GROUP BY `{$aValue['groupTable']}`.`{$aValue['groupField']}`, ";
+                $sOn = isset($aValue['mainTable']) ? $aValue['mainTable'] : $this->aCurrent['table'];
+                $aSql['join'] .= " {$aValue['type']} JOIN `{$aValue['table']}` $sTableAlias ON " . (!empty($aValue['on_sql']) ? $aValue['on_sql'] : "`{$sAlias}`.`{$aValue['onField']}`=" . trim($this->setFieldUnit($aValue['mainField'], $sOn, isset($aValue['mainFieldFunc']) ? $aValue['mainFieldFunc'] : '', false), ", "));
+                $aSql['ownFields'] .= $this->setFieldUnit($aValue['mainField'], $sOn, '', $bRenameMode);
+            }
+            $aSql['joinFields'] = trim($aSql['joinFields'], ', ');
+            $aSql['groupBy'] = trim($aSql['groupBy'], ', ');
+        }
+        return $aSql;
+    }
+
+    /**
+     * Concat sql parts of query, run it and return result array
+     * @param $aParams addon param
+     * return $aData multivariate array
+     */
+    function getSearchDataByParams ($aParams = '')
+    {
+        $bForUnion = isset($aParams['for_union']) && $aParams['for_union'] === true;
+
+        $aSql = array('ownFields'=>'', 'joinFields'=>'', 'order'=>'');
+
+        // searchFields
+        if($bForUnion) {
+            $sTable = isset($this->aCurrent['tableSearch']) ? $this->aCurrent['tableSearch'] : $this->aCurrent['table'];
+
+            $aSql['ownFields'] .= $this->setFieldUnit(!empty($this->aCurrent['ident']) ? $this->aCurrent['ident'] : 'id', $sTable);
+            $aSql['ownFields'] .= $this->setFieldUnit(!empty($this->aCurrent['added']) ? $this->aCurrent['added'] : 'added', $sTable);
+            $aSql['ownFields'] .= "'" . $this->getContentInfoName()  . "' AS `content_info`";
+        }
+        else
+            foreach ($this->aCurrent['ownFields'] as $sValue)
+                $aSql['ownFields'] .= $this->setFieldUnit($sValue, $this->aCurrent['table']);
+
+        // joinFields & join
+        $aJoins = $this->getJoins();
+        if (!empty($aJoins)) {
+            if(!$bForUnion) {
+                $aSql['ownFields'] .= $aJoins['ownFields'];
+                $aSql['ownFields'] .= $aJoins['joinFields'];
+            }
+
+            $aSql['join'] = $aJoins['join'];
+            $aSql['groupBy'] = $aJoins['groupBy'];
+        } 
+
+        $aSql['ownFields'] = trim($aSql['ownFields'], ', ');
+
+        // from
+        $aSql['from'] = " FROM `{$this->aCurrent['table']}`";
+
+        // where
+        $aSql['where'] = $this->getRestriction();
+
+        // limit
+        $aSql['limit'] = $this->getLimit();
+
+        // sorting
+        $this->setSorting();
+
+        $aSort = $this->getSorting($this->aCurrent['sorting']);
+        foreach ($aSort as $sKey => $sValue)
+            $aSql[$sKey] .= $sValue;
+
+        // execution
+        $sqlQuery = 'SELECT';
+
+        if(isset($this->aCurrent['distinct']) && $this->aCurrent['distinct'] === true)
+            $sqlQuery .= ' DISTINCT';
+
+        $sqlQuery .= ' ' . $aSql['ownFields'];
+
+        $sqlQuery .= ' ' . $aSql['from'];
+
+        if (isset($aSql['join']))
+            $sqlQuery .= ' ' . $aSql['join'];
+
+        if (isset($aSql['where']))
+            $sqlQuery .= ' ' . $aSql['where'];
+
+        if (isset($aSql['groupBy']))
+            $sqlQuery .= ' ' . $aSql['groupBy'];
+
+        if($bForUnion)
+            return [
+                'query' => $sqlQuery, 
+                'limit' => $aSql['limit']
+            ];
+
+        if (isset($aSql['order']))
+            $sqlQuery .= ' ' . $aSql['order'];
+
+        if (isset($aSql['limit']))
+            $sqlQuery .= ' ' . $aSql['limit'];
+
+        // echoDbg($sqlQuery);
+        $aRes = BxDolDb::getInstance()->getAll($sqlQuery);
+        return $aRes;
+    }
+
+    /**
+     * Set class fields condition params and paginate array
+     */
+    function setConditionParams()
+    {
+        $this->aCurrent['paginate']['num'] = 0;
+
+        // keyword
+        $sKeyword = bx_process_input(isset($this->_aCustomSearchCondition['keyword']) ? $this->_aCustomSearchCondition['keyword'] : bx_get('keyword'));
+
+        if ($this->_bLiveSearch && empty($sKeyword))
+            return;
+
+        if ($sKeyword !== false) {
+            if (substr($sKeyword, 0, 1) == '@') {
+                $sModule = $this->aCurrent['module_name'];
+                $sMethod = 'act_as_profile';
+                if(!bx_is_srv($sModule, $sMethod) || !bx_srv($sModule, $sMethod))
+                    return;
+
+                $sKeyword = substr($sKeyword, 1);
+            }
+
+            $this->aCurrent['restriction']['keyword'] = array(
+                'value' => $sKeyword,
+                'field' => '',
+                'operator' => 'against'
+            );
+
+            // for search results we need to show all items, not only public content
+            $this->setProcessPrivateContent(true);
+        }
+
+        // owner
+        if (isset($_GET['ownerName'])) {
+            $sName = bx_process_input($_GET['ownerName']);
+            $iUser = (int)BxDolProfileQuery::getInstance()->getIdByNickname($sName);
+            BxDolMenu::getInstance()->setCurrentProfileID($iUser);
+        } elseif (isset($_GET['userID']))
+            $iUser = bx_process_input($_GET['userID'], BX_DATA_INT);
+
+        if (!empty($iUser))
+            $this->aCurrent['restriction']['owner']['value'] = $iUser;
+        
+        // meta info
+        if ($this->_sMetaType && !empty($this->aCurrent['object_metatags'])) {
+            $o = BxDolMetatags::getObjectInstance($this->aCurrent['object_metatags']);
+            if ($o) {
+                unset($this->aCurrent['restriction']['keyword']);
+                switch ($this->_sMetaType) {
+                    case 'location_country':
+                        $o->locationsSetSearchCondition($this, $sKeyword);
+                        break;
+                    case 'location_country_state':
+                        $o->locationsSetSearchCondition($this, $sKeyword, bx_process_input(isset($this->_aCustomSearchCondition['state']) ? $this->_aCustomSearchCondition['state'] : bx_get('state')));
+                        break;
+                    case 'location_country_city':
+                        $o->locationsSetSearchCondition($this, $sKeyword, bx_process_input(isset($this->_aCustomSearchCondition['state']) ? $this->_aCustomSearchCondition['state'] : bx_get('state')), bx_process_input(isset($this->_aCustomSearchCondition['city']) ? $this->_aCustomSearchCondition['city'] : bx_get('city')));
+                        break;
+                    case 'location_state':
+                        $o->locationsSetSearchCondition($this, false, $sKeyword);
+                        break;
+                    case 'location_city':
+                        $o->locationsSetSearchCondition($this, false, false, $sKeyword);
+                        break;
+                    case 'mention':
+                        $oCmts = !empty($this->sModuleObjectComments) ? BxDolCmts::getObjectInstance($this->sModuleObjectComments, 0, false) : false;
+                        $o->mentionsSetSearchCondition($this, $sKeyword, $oCmts ? $oCmts->getSystemId() : 0);
+                        break;
+                    case 'keyword':
+                        $oCmts = !empty($this->sModuleObjectComments) ? BxDolCmts::getObjectInstance($this->sModuleObjectComments, 0, false) : false;
+                        $o->keywordsSetSearchCondition($this, $sKeyword, $oCmts ? $oCmts->getSystemId() : 0);
+                        break;
+                }
+            }
+        }
+
+        // category
+        if ($this->_sCategoryObject){
+            if(($o = BxDolCategory::getObjectInstance($this->_sCategoryObject)) && $this->_bSingleSearch) {
+                if ($this->aCurrent['name'] == $o->getSearchObject()) {
+                    unset($this->aCurrent['restriction']['keyword']);
+                    $o->setSearchCondition($this, $sKeyword);
+                }
+            }
+            if ($this->_sCategoryObject == 'multi'){
+                unset($this->aCurrent['restriction']['keyword']);
+                $this->setCategoriesCondition($sKeyword);
+            }
+        }
+
+        $this->setPaginate();
+        $iNum = $this->getNum();
+        if ($iNum > 0)
+            $this->aCurrent['paginate']['num'] = $iNum;
+    }
+
+    /**
+     * Check number of records on current page
+     * return number of records on current page + 1
+     */
+    function getNum ()
+    {
+        $aJoins = $this->getJoins(false);
+        $sqlQuery =  "SELECT * FROM `{$this->aCurrent['table']}` " . (isset($aJoins['join']) ? ' ' . $aJoins['join'] : '' ) . $this->getRestriction() . (isset($aJoins['groupBy']) ? ' ' . $aJoins['groupBy'] : '') . ' ' . $this->getLimit(true);
+        return count(BxDolDb::getInstance()->getAll($sqlQuery));
+    }
+
+    /**
+     * Get total number of records
+     * return total number of records
+     */
+    function getTotal ()
+    {
+        $aJoins = $this->getJoins(false);
+        $sqlQuery =  "SELECT COUNT(*) FROM `{$this->aCurrent['table']}` " . (isset($aJoins['join']) ? ' ' . $aJoins['join'] : '' ) . $this->getRestriction() . (isset($aJoins['groupBy']) ? ' ' . $aJoins['groupBy'] : '');
+        return BxDolDb::getInstance()->getOne($sqlQuery);
+    }
+
+    /**
+     * Check restriction params and make condition part of query
+     * return $sqlWhere sql code of query for WHERE part
+     */
+    function getRestriction ()
+    {
+        $oDb = BxDolDb::getInstance();
+        $sqlWhere = '';
+        if (isset($this->aCurrent['restriction'])) {
+            $aWhere[] = '1';
+            foreach ($this->aCurrent['restriction'] as $sKey => $aValue) {
+                $sqlCondition = '';
+                if (isset($aValue['operator']) && isset($aValue['value']) && $aValue['value'] !== '' && $aValue['value'] !== false && $aValue['value'] !== null) {
+                   $sFieldTable = isset($aValue['table']) ? $aValue['table'] : $this->aCurrent['table'];
+                   $sqlCondition = "`{$sFieldTable}`.`{$aValue['field']}` ";
+                   switch ($aValue['operator']) {
+                       case 'empty value':
+                            $sqlCondition .= " = '' ";
+                            break;
+                       case 'not empty value':
+                            $sqlCondition .= " != '' ";
+                            break;
+                       case 'nothing':
+                            $sqlCondition = " 0 ";
+                            break;
+                       case 'against':
+                            $aCond = isset($aValue['field']) && strlen($aValue['field']) > 0 ? $aValue['field'] : $this->aCurrent['searchFields'];
+                            $sqlCondition = !empty($aCond) ? $this->getSearchFieldsCond($aCond, $aValue['value']) : "";
+                            break;
+                       case 'like':
+                            $sqlCondition .= "LIKE " . $oDb->escape('%' . $aValue['value'] . '%');
+                            break;
+                       case 'in':
+                       case 'not in':
+                            $sValuesString = $this->getMultiValues($aValue['value']);
+                            $sqlCondition .= strtoupper($aValue['operator']) . '(' . $sValuesString . ')';
+                            break;
+                       case 'in_set':
+                           $sqlCondition = "1 << (" . $sqlCondition . " - 1) & " . (int)$aValue['value'];
+                           break;
+                       default:
+                               $sqlCondition .= $aValue['operator'] . (isset($aValue['no_quote_value']) && $aValue['no_quote_value'] ?  $aValue['value'] : $oDb->escape($aValue['value']));
+                       break;
+                    }
+                }
+                if (strlen($sqlCondition) > 0)
+                    $aWhere[] = $sqlCondition;
+            }
+            $sqlWhere .= "WHERE ". implode(' AND ', $aWhere) . (isset($this->aCurrent['restriction_sql']) ? $this->aCurrent['restriction_sql'] : '');
+        }
+
+        return $sqlWhere;
+    }
+
+    /**
+     * Get limit part of query
+     * return $sqlFrom code for limit part pf query
+     */
+    function getLimit ($isAddPlusOne = false)
+    {
+        if (!isset($this->aCurrent['paginate']))
+            return;
+
+        $sqlFrom = (int)$this->aCurrent['paginate']['start'] > 0 ? (int)$this->aCurrent['paginate']['start'] : 0;
+        $sqlTo = $this->aCurrent['paginate']['perPage'] + ($isAddPlusOne ? 1 : 0);
+        if ($sqlTo > 0)
+            return 'LIMIT ' . $sqlFrom . ', ' . $sqlTo;
+    }
+
+    /**
+     * Set sorting field of class
+     */
+    function setSorting ()
+    {
+        $this->aCurrent['sorting'] = isset($_GET[$this->aCurrent['name'] . '_mode']) ? $_GET[$this->aCurrent['name'] . '_mode'] : $this->aCurrent['sorting'];
+    }
+
+    /**
+     * Get sorting part of query according current sorting mode
+     * @param string $sSortType sorting type
+     *                          return array with sql elements order and ownFields
+     */
+    function getSorting ($sSortType = 'last')
+    {
+        if (isset($this->aCurrent['sorting_sql']))
+            return array('order' => $this->aCurrent['sorting_sql']);
+        
+        $aOverride = $this->getAlterOrder();
+        if (is_array($aOverride) && !empty($aOverride))
+            return $aOverride;
+
+       $aSql = array();
+       switch ($sSortType) {
+           case 'rand':
+                $aSql['order'] = "ORDER BY RAND()";
+                break;
+           case 'score':
+                if (is_array($this->aCurrent['restriction']['keyword'])) {
+                    $sPseud = '';
+                    if ('on' == getParam('useLikeOperator')) {
+                        $aSql['order'] = "ORDER BY `score` DESC";
+                        $sPseud = 'score';
+                    }
+                    $aSql['ownFields'] .= $this->getSearchFieldsCond($this->aCurrent['searchFields'], $this->aCurrent['restriction']['keyword']['value'], $sPseud) . ',';
+                }
+                break;
+           case 'none':
+                $aSql['order'] = "";
+                break;
+           default:
+                $aSql['order'] = "ORDER BY `date` DESC";
+        }
+        return $aSql;
+    }
+
+    /**
+     * Return own varaint for sorting (redeclare if necessary)
+     * return array of sql elements
+     */
+    function getAlterOrder ()
+    {
+        return array();
+    }
+
+    /**
+     * Set paginate fields of class according GET params 'start' and 'per_page'
+     * forcePage is need for setting most important number of current page
+     */
+    function setPaginate ()
+    {
+        $iStart = 0;
+        $iPerPage = 0;
+
+        //--- Check in aCurrent (low priority).
+        if(isset($this->aCurrent['paginate']['perPage']) && (int)$this->aCurrent['paginate']['perPage'] != 0)
+            $iPerPage = (int)$this->aCurrent['paginate']['perPage'];
+        
+        //--- Check in aParams (middle priority).
+        if(isset($this->_aParams['per_page']) && (int)$this->_aParams['per_page'] != 0)
+            $iPerPage = (int)$this->_aParams['per_page'];
+
+        //--- Check in GET params (high priority).
+        if(isset($_GET['per_page']) && (int)$_GET['per_page'] != 0)
+            $iPerPage = (int)$_GET['per_page'];
+
+        //--- Trying to get from System settings.
+        if (empty($iPerPage))
+            $iPerPage = (int)getParam('sys_per_page_search_keyword_' . ($this->_bSingleSearch ? 'single' : 'plural'));
+
+        //--- Use default value in case of emergency.
+        if (empty($iPerPage))
+            $iPerPage = BX_DOL_SEARCH_RESULTS_PER_PAGE_DEFAULT;
+        
+        if ($this->iPaginatePerPage != BX_DOL_SEARCH_RESULTS_PER_PAGE_DEFAULT)
+            $iPerPage = $this->iPaginatePerPage;
+
+        $this->aCurrent['paginate']['perPage'] = $iPerPage;
+
+        if(!isset($this->aCurrent['paginate']['forceStart'])) {
+            if(isset($this->_aParams['start']))
+                $iStart = (int)$this->_aParams['start'];
+
+            if(($iStartGet = bx_get('start')) !== false)
+                $iStart = (int)$iStartGet;
+        }
+        else
+            $iStart = (int)$this->aCurrent['paginate']['forceStart'];
+
+        if($iStart < 0)
+            $iStart = 0;
+
+        $this->aCurrent['paginate']['start'] = $iStart;
+    }
+
+    function unsetPaginate()
+    {
+    	unset($this->aCurrent['paginate']);
+    }
+
+    /**
+     * Get sql where condition for search fields
+     * @param array of search fields
+     * @param string $sKeyword keyword value for search
+     * @param string sPseud for setting new name for generated set of fields in query
+     *                         return sql code of WHERE part in query
+     */
+    function getSearchFieldsCond ($aFields, $sKeyword, $sPseud = '')
+    {
+        if (!$sKeyword)
+            return '';
+
+        $sTable = empty($this->aCurrent['tableSearch']) ? $this->aCurrent['table'] : $this->aCurrent['tableSearch'];
+
+        $oDb = BxDolDb::getInstance();
+
+        $bLike = getParam('useLikeOperator');
+
+        if (!is_array($aFields))
+            $aFields = array($aFields);
+
+        if ($bLike == 'on') {
+            $sKeyword = $oDb->escape('%' . preg_replace('/\s+/', '%', $sKeyword) . '%');
+
+            $sSqlWhere = '';
+            foreach ($aFields as $sValue)
+                $sSqlWhere .= "`{$sTable}`.`$sValue` LIKE  " . $sKeyword . " OR ";
+
+            $sSqlWhere = '(' . trim($sSqlWhere, 'OR ') . ')';
+
+        } else {
+        	$sKeyword = $oDb->escape($sKeyword);
+
+            $sSqlWhere = '';
+            foreach ($aFields as $sValue)
+                $sSqlWhere .= "`{$sTable}`.`$sValue`, ";
+
+            $sSqlWhere = trim($sSqlWhere, ', ');
+            $sSqlWhere = " MATCH({$sSqlWhere}) AGAINST (" . $sKeyword . ") ";
+
+            if (!empty($sPseud))
+                $sSqlWhere .= " AS `$sPseud` ";
+        }
+
+        return $sSqlWhere;
+    }
+
+    /**
+     * Get set from several values for 'in' and 'not in' operators
+     * @param $aValues array of values
+     * return sql code for field with operator IN (NOT IN)
+     */
+    function getMultiValues ($aValues)
+    {
+        $oDb = BxDolDb::getInstance();
+        return $oDb->implode_escape($aValues);
+    }
+
+    /**
+     * System method for filling aPseud array.
+     * Fill field aPseud for current class (if you will use own getSearchUnit methods then not necessary to redeclare).
+     */
+    function _getPseud ()
+    {
+
+    }
+
+    /**
+     * Add replace markers. Markers are replaced in titles and browse urls
+     * @param $a array of markers as key => value
+     * @return true on success or false on error
+     */
+    public function addMarkers ($a)
+    {
+        if (empty($a) || !is_array($a))
+            return false;
+        $this->_aMarkers = array_merge ($this->_aMarkers, $a);
+        return true;
+    }
+
+    /**
+     * Replace provided markers in a string
+     * @param $mixed string or array to replace markers in
+     * @return string where all occured markers are replaced
+     */
+    protected function _replaceMarkers ($mixed)
+    {
+        return bx_replace_markers($mixed, $this->_aMarkers);
+    }
+}
+
+/** @} */

+ 1563 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolStorage.php

@@ -0,0 +1,1563 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+define('BX_DOL_STORAGE_ERR_OK', 0);
+
+// 1-8 are standard upload form errors
+/*
+UPLOAD_ERR_INI_SIZE
+Value: 1; The uploaded file exceeds the upload_max_filesize directive in php.ini.
+
+UPLOAD_ERR_FORM_SIZE
+Value: 2; The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.
+
+UPLOAD_ERR_PARTIAL
+Value: 3; The uploaded file was only partially uploaded.
+
+UPLOAD_ERR_NO_FILE
+Value: 4; No file was uploaded.
+
+UPLOAD_ERR_NO_TMP_DIR
+Value: 6; Missing a temporary folder. Introduced in PHP 4.3.10 and PHP 5.0.3.
+
+UPLOAD_ERR_CANT_WRITE
+Value: 7; Failed to write file to disk. Introduced in PHP 5.1.0.
+
+UPLOAD_ERR_EXTENSION
+Value: 8; A PHP extension stopped the file upload. PHP does not provide a way to ascertain which extension caused the file upload to stop; examining the list of loaded extensions with phpinfo() may help. Introduced in PHP 5.2.0.
+*/
+
+define('BX_DOL_STORAGE_ERR_NO_INPUT_METHOD', 1000); ///< there is such input method available
+define('BX_DOL_STORAGE_ERR_NO_FILE', 1001); ///< there is no file to upload
+define('BX_DOL_STORAGE_INVALID_FILE', 1002); ///< uploaded file is invalid or hack attempts
+define('BX_DOL_STORAGE_ERR_FILE_TOO_BIG', 1003); ///< file is too big
+define('BX_DOL_STORAGE_ERR_WRONG_EXT', 1004); ///< wrong file extension
+define('BX_DOL_STORAGE_ERR_USER_QUOTA_EXCEEDED', 1005); ///< user quota exceeded
+define('BX_DOL_STORAGE_ERR_OBJECT_QUOTA_EXCEEDED', 1006); ///< storage object quota exceeded
+define('BX_DOL_STORAGE_ERR_SITE_QUOTA_EXCEEDED', 1007); ///< site quota exceeded
+define('BX_DOL_STORAGE_ERR_ENGINE_ADD', 1008); ///< some other error during file adding occured, related to particular storage engine
+
+define('BX_DOL_STORAGE_ERR_FILE_NOT_FOUND', 2001); ///< file not found
+define('BX_DOL_STORAGE_ERR_UNLINK', 2002); ///< file deletion failed
+
+define('BX_DOL_STORAGE_ERR_DB', 5001); ///< database error
+define('BX_DOL_STORAGE_ERR_FILESYSTEM_PERM', 5002); ///< filesystem permissions error
+define('BX_DOL_STORAGE_ERR_PERMISSION_DENIED', 5003); ///< permission denied
+define('BX_DOL_STORAGE_ERR_ENGINE_GET', 5004); ///< some other error during file getting occured, related to particular storage engine
+define('BX_DOL_STORAGE_ERR_NOT_IMPLEMENTED', 5005); ///< this feature isn't implemented in storage engine
+
+define('BX_DOL_STORAGE_DEFAULT_MIME_TYPE', 'octet/stream'); ///< default mime type if it is not find by extension
+
+define('BX_DOL_STORAGE_DEFAULT_ICON', 'mime-type-any.svg'); ///< default icon if no other icon can be determined by file extension
+define('BX_DOL_STORAGE_DEFAULT_ICON_FONT', 'far file'); ///< default font icon if no other icon can be determined by file extension
+
+define('BX_DOL_STORAGE_QUEUED_DELETIONS_PER_RUN', 200); ///< max number of file deletions per one cron run, @see BxDolStorage::pruneDeletions
+
+/**
+ * This class unify storage.
+ * As the result there are many advantages:
+ * - files can be stored as on localhost as on remote storage, for example Amazon s3
+ * - all files are in one place and separated from other files, so the data can be organised more easily,
+ *   for example moved to dedicated disk if there is not enough storage
+ * - simplicity of usage, there are hight level classes to handle all necessary operations, including upload and security
+ * - quotas settings, so you always control how much space you are going to use
+ * - persistent storage; uploaded, but not saved files appear upon page reload or future submission of the same form
+ *
+ *
+ * Usage.
+ *
+ * Step 1:
+ * Add record to 'sys_objects_storage' table, like you doing this for Comments or Voting objects:
+ * - object - your storage object name, usually it is in the following format - vendor prefix, underscore, module prefix;
+ *   for example for BoonEx Forum module it can be bx_forum.
+ * - engine - storage engine, for now the following engines are supported:
+ *     1. Local - local storage, by default files are stored in /storage/ subfolder 
+ *     2. S3 - Amazon S3 storage, files are stored on Amazon S3 storage, you need to point AWS Access Key, AWS Secret Key and AWS Bucket in the settings
+ * - params - custom storage engine params as php serialized string, supported params:
+ *      1. fields - list of additional fields to add to database as key(field name) and value(func or serialized service call to get the value)
+ * - token_life - life of the security token in seconds for private files
+ * - cache_control - control browser cache, allow browser to store files in browser's cache for this number of seconds, to disable browser cache,
+ *   or let browser to decide on its own set it to 0(zero)
+ * - levels - store files in subfolders, generated from filename; it is useful when there is limit of number of files/folders per directory;
+ *   for example if level is 2 and file name is abc.jpg then the file will be stored in a/b/abc.jpg folder, set to to 0(zero) to disable this feature
+ * - table_files - table where file info is stored, please refer to step 2 for more details
+ * - ext_mode - file extensions restriction mode:
+ *     1. allow-deny - allow only file types in ext_allow field and deny all other file types, ext_deny field is ignored.
+ *     2. deny-allow - allow all files except the ones specified in ext_deny field, ext_allow field is ignored.
+ * - ext_allow - allowed file extensions, comma separated, it is in effect when ext_mode is allow-deny; example - jpg,gif,png
+ * - ext_deny - denied file extensions, comma separated, it is in effect when ext_mode is deny-allow; example - exe,com,bat
+ * - quota_size - storage engine quota in bytes, the summary of all uploaded files can not be bigger than this number
+ * - current_size - current storage engine usage, the sum of all uploaded file sizes
+ * - quota_number - max number of files allowed in this storage engine
+ * - current_number - current number of files in this storage engine
+ * - max_file_size - max file size for this storage engine, please note that other server settings are used if they are less than this setting option
+ * - ts - unix timestamp of the last file upload
+ *
+ *
+ * Step 2:
+ * Create table for files.
+ *
+ * @code
+ * CREATE TABLE `my_sample_files` (
+ *   `id` int(11) NOT NULL AUTO_INCREMENT,
+ *   `profile_id` int(10) unsigned NOT NULL,
+ *   `remote_id` varchar(255) NOT NULL,
+ *   `path` varchar(255) NOT NULL,
+ *   `file_name` varchar(255) NOT NULL,
+ *   `mime_type` varchar(128) NOT NULL,
+ *   `ext` varchar(32) NOT NULL,
+ *   `size` int(11) NOT NULL,
+ *   `added` int(11) NOT NULL,
+ *   `modified` int(11) NOT NULL,
+ *   `private` int(11) NOT NULL,
+ *   PRIMARY KEY (`id`),
+ *   UNIQUE KEY `remote_id` (`remote_id`)
+ * );
+ * @endcode
+ *
+ * You need to enter this table name in 'table_files' field in 'sys_objects_storage' table, mentioned in step 1.
+ * The files will be added to this table automatically, all you need is to save 'id' from this table, so you can refer to the file by the 'id'.
+ * It is not recommended to change this table, it is better to create another table which will be connected with this one by file 'id'.
+ *
+ *
+ * Step 3:
+ * Handling upload.
+ *
+ * Sample HTML form:
+ *  @code
+ * <form enctype="multipart/form-data" method="POST" action="store_file.php">
+ *     Choose a file to upload:
+ *     <input name="file" type="file" />
+ *     <br />
+ *     <input type="submit" name="add" value="Upload File" />
+ * </form>
+ * @endcode
+ *
+ * Add server code in sample store_file.php file:
+ *
+ * @code
+ * require_once('./inc/header.inc.php');
+ * require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+ *
+ * BxDolStorage::pruning(); // pruning is needed to clear expired security tokens, you can call it on cron when your server is not busy
+ * $oStorage = BxDolStorage::getObjectInstance('my_module'); // create storage object instance, 'my_module' is value of 'object' field in 'sys_objects_storage' table
+ *
+ * if (isset($_POST['add'])) { // if form is submitted
+ *         $iId = $oStorage->storeFileFromForm($_FILES['file'], true, 0); // store file from submitted HTML form, 'file' is input name with field, true means store file as private, 0 is profile id
+ *         if ($iId) { // storeFileFromForm returns file id, not false value means operation is successful.
+ *             // save $iId somewhere, so you can refer to the file after
+ *             $iCount = $oStorage->afterUploadCleanup($iId, $iProfileId); // since we saved $iId, we remove it from the orphans list, so it will not appear on the form next time (persistent storage)
+ *             echo "uploaded file id: " . $iId . "(deleted orphans:" . $iCount . ")";
+ *         } else {
+ *             // something went wrong - print the error
+ *             echo "error uploading file: " . $oStorage->getErrorString()
+ *         }
+ * }
+ * @endcode
+ *
+ * Please refer to the functions definition for more additional description of functions params.
+ *
+ *
+ * Step 4:
+ * Displaying the file.
+ *
+ * Use the following code to retrieve saved file. Remember you saved filed id somewhere in the previous step.
+ * Lets assume that the uploaded file is image, then we can show it using the following code:
+ *
+ * @code
+ * require_once('./inc/header.inc.php');
+ * require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+ *
+ * $oStorage = BxDolStorage::getObjectInstance('my_module');
+ *
+ * $iId = 1234; // since you've saved it somewhere in the previous step, you can retrieve it here
+ *
+ * echo "Uploaded image: <img src="' . $oStorage->getFileUrlById($iId) . '" />;";
+ * @endcode
+ *
+ * It will show the file, regardless if it is private or public.
+ * You need to control it by yourself who will view the file.
+ * The difference in viewing private files is that link to the file is expiring after N seconds,
+ * you control this period using 'token_life' field in 'sys_objects_storage' table.
+ *
+ */
+abstract class BxDolStorage extends BxDolFactory implements iBxDolFactoryObject
+{
+    protected $_aObject; ///< object properties
+    protected $_iCacheControl; ///< browser cache in seconds, 0 - disabled
+    protected $_aParams; ///< custom params
+    protected $_iErrorCode; ///< last error code
+    protected $_oDb; ///< database relates function are in this object
+    protected $_aMimeTypesViewable = ['audio/', 'image/', 'video/']; ///< file types (by mime type) to allow view file in browser instead of downloading
+    
+    /**
+     * constructor
+     */
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+        $this->_iCacheControl = $aObject['cache_control'];
+        $this->_aParams = $aObject['params'] ? unserialize($aObject['params']) : '';
+        $this->_oDb = new BxDolStorageQuery($aObject);
+    }
+
+    /**
+     * Get storage object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    public static function getObjectInstance($sObject)
+    {
+        if (isset($GLOBALS['bxDolClasses']['BxDolStorage!'.$sObject]))
+            return $GLOBALS['bxDolClasses']['BxDolStorage!'.$sObject];
+
+        $aObject = BxDolStorageQuery::getStorageObject($sObject);        
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        $aExtMarkers = [
+            'image' => getParam('sys_files_ext_images'),
+            'video' => getParam('sys_files_ext_video'),
+            'imagevideo' => getParam('sys_files_ext_imagevideo'), 
+            'audio' => getParam('sys_files_ext_audio'),
+            'dangerous' => getParam('sys_files_ext_dangerous')
+        ];
+        
+        $aObject['ext_allow'] = bx_replace_markers($aObject['ext_allow'], $aExtMarkers);
+        $aObject['ext_deny'] = bx_replace_markers($aObject['ext_deny'], $aExtMarkers);
+
+        $sClass = 'BxDolStorage' . $aObject['engine'];
+        $o = new $sClass($aObject);
+
+        if (!$o->isInstalled() || !$o->isAvailable())
+            return false;
+
+        return ($GLOBALS['bxDolClasses']['BxDolStorage!'.$sObject] = $o);
+    }
+
+    /**
+     * Delete old security tokens from database.
+     * It is alutomatically called upin cron execution, usually once in a day.
+     * @return number of deleted records
+     */
+    public static function pruning()
+    {
+        $iDeleted = 0;
+        $a = BxDolStorageQuery::getStorageObjects();
+        foreach ($a as $aObject) {
+            $oDb = new BxDolStorageQuery($aObject);
+            $iDeleted += $oDb->prune();
+        }
+        return $iDeleted;
+    }
+
+    /**
+     * Delete files queued for deletions
+     * It is alutomatically called upin cron execution, usually one time per minute.
+     * Max number of deletetion per time is defined in @see BX_DOL_STORAGE_QUEUED_DELETIONS_PER_RUN
+     * @return number of deleted records
+     */
+    public static function pruneDeletions()
+    {
+        $iDeleted = 0;
+        $a = BxDolStorageQuery::getQueuedFilesForDeletion(BX_DOL_STORAGE_QUEUED_DELETIONS_PER_RUN);
+        foreach ($a as $r) {
+            $o = BxDolStorage::getObjectInstance($r['object']);
+            $iDeleted += ($o && $o->deleteFile($r['file_id']) ? 1 : 0);
+        }
+
+        return $iDeleted;
+    }
+
+    /**
+     * Check if module has any files pending for deletion, it is supposed that all module storage object names are prefixed with module name
+     * @param $sPrefix - usually module name
+     * @return number of files pending for deletion which were found by prefix
+     */
+    public static function isQueuedFilesForDeletion ($sPrefix)
+    {
+        return BxDolStorageQuery::isQueuedFilesForDeletion($sPrefix);
+    }
+
+    /**
+     * Get file token for private files.
+     * @param $iFileId file
+     * @return file token string
+     */
+    public function genToken($iFileId)
+    {
+        return $this->_oDb->genToken($iFileId);
+    }
+    
+    /**
+     * Change storage engine. It's possible to change it when there is no files in storage engine.
+     * @param $sEngine new storage engine
+     * @return true on success or false on error
+     */ 
+    public function changeStorageEngine ($sEngine)
+    {
+        if (0 == $this->_aObject['current_size'] && 0 == $this->_aObject['current_number'])
+            return $this->_oDb->changeStorageEngine($sEngine);
+        return false;
+    }
+
+    /**
+     * Is storage engine available?
+     * @return boolean
+     */
+    function isAvailable()
+    {
+        return true;
+    }
+
+    /**
+     * Are required php modules installed for this storage engine ?
+     * @return boolean
+     */
+    public function isInstalled()
+    {
+        return true;
+    }
+
+    public function getObject()
+    {
+    	return $this->_aObject['object'];
+    }
+
+    public function getObjectData()
+    {
+    	return $this->_aObject;
+    }
+
+    /**
+     * Get error code from the last occured error
+     * @return error code
+     */
+    public function getErrorCode()
+    {
+        return $this->_iErrorCode;
+    }
+
+    /**
+     * Get error string from the last occured error
+     * @return error string
+     */
+    public function getErrorString()
+    {
+        bx_import('BxDolLanguages');
+        $a = array (
+            1000 => '_sys_storage_err_no_input_method',
+            1001 => '_sys_storage_err_no_file',
+            1002 => '_sys_storage_invalid_file',
+            1003 => '_sys_storage_err_file_too_big',
+            1004 => '_sys_storage_err_wrong_ext',
+            1005 => '_sys_storage_err_user_quota_exceeded',
+            1006 => '_sys_storage_err_object_quota_exceeded',
+            1007 => '_sys_storage_err_site_quota_exceeded',
+            1008 => '_sys_storage_err_engine_add',
+
+            2001 => '_sys_storage_err_file_not_found',
+            2002 => '_sys_storage_err_unlink',
+
+            5001 => '_sys_storage_err_db',
+            5002 => '_sys_storage_err_filesystem_perm',
+            5003 => '_sys_storage_err_permission_denied',
+            5004 => '_sys_storage_err_engine_get',
+            5005 => '_sys_storage_err_not_implemented',
+        );
+        return _t($a[$this->_iErrorCode]);
+    }
+
+    /**
+     * Get max file size allowed for current user, it checks user quota, object quota, site quota and php setting
+     * @param $iProfileId profile id to check quota for
+     * @return quota size in bytes
+     */
+    public function getMaxUploadFileSize ($iProfileId)
+    {
+        $iMin = PHP_INT_MAX;
+
+        $aUserQuota = $this->_oDb->getUserQuota($iProfileId);
+        if ($aUserQuota['max_file_size'] && $aUserQuota['max_file_size'] < $iMin)
+            $iMin = $aUserQuota['max_file_size'];
+
+        $aObjectQuota = $this->_oDb->getStorageObjectQuota();
+        if ($aObjectQuota['max_file_size'] && $aObjectQuota['max_file_size'] < $iMin)
+            $iMin = $aObjectQuota['max_file_size'];
+
+        // TODO: get and check site quota
+
+        if (!defined('BX_DOL_CRON_EXECUTE')) {
+            $iUploadMaxFilesize = return_bytes(ini_get('upload_max_filesize'));
+            if ($iUploadMaxFilesize && $iUploadMaxFilesize < $iMin)
+                $iMin = $iUploadMaxFilesize;
+        }
+
+        return $iMin;
+    }
+
+    /**
+     * Store file in the storage area. It is not recommended to use this function directly,
+     * use other funtions like: storeFileFromForm, storeFileFromXhr, storeFileFromPath, storeFileFromUrl
+     * @param $sMethod upload method, like regular Form upload, upload from URL, etc
+     * @param $aMethodParams upload method params
+     * @param $sName file name with extention
+     * @param $isPrivate private or public file
+     * @param $iProfileId profile id of the upload action performer
+     * @param $iContentId content id to associate with ghost file
+     * @return id of added file on success, false on error - to get exact error string call getErrorString()
+     */
+    public function storeFile($sMethod, $aMethodParams, $sName = false, $isPrivate = true, $iProfileId = 0, $iContentId = 0)
+    {
+        // setup input source using helper classes, like $_FILES or some URL for example
+
+        $sHelperClass = 'BxDolStorageHelper' . $sMethod;
+        if (!class_exists($sHelperClass)) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_NO_INPUT_METHOD);
+            return false;
+        }
+
+        $oHelper = new $sHelperClass($aMethodParams);
+
+        // check for errors, like size and extentions checking
+
+        if ($iImmediateError = $oHelper->getImmediateError()) {
+            $this->setErrorCode($iImmediateError);
+            return false;
+        }
+
+        $sExt = $this->getFileExt($oHelper->getName());
+        $sMimeType = $this->getMimeTypeByFileName($oHelper->getName());
+
+        if (!$this->isValidExt($sExt)) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_WRONG_EXT);
+            return false;
+        }
+
+        // before upload callback + additional checking
+
+        if (!$this->onBeforeFileAdd (array(
+            'profile_id' => $iProfileId,
+            'content_id' => $iContentId,
+            'file_name' => $oHelper->getName(),
+            'mime_type' => $sMimeType,
+            'ext' => $sExt,
+            'size' => $oHelper->getSize(),
+            'private' => $isPrivate ? 1 : 0,
+        ))) {
+            return false;
+        }
+
+        // create tmp file
+
+        $sTmpFile = tempnam(BX_DIRECTORY_PATH_TMP, $this->_aObject['object']);
+        if (!$oHelper->save($sTmpFile)) {
+            $this->setErrorCode(BX_DOL_STORAGE_INVALID_FILE);
+            return false;
+        }
+
+        // process additional custom fields, like video duration and video dimension
+
+        $aAdditionalFields = array();
+        if (isset($this->_aParams['fields']) && is_array($this->_aParams['fields'])) {
+            foreach ($this->_aParams['fields'] as $sField => $mixedMethod) {
+                if (is_string($mixedMethod) && method_exists($this, $mixedMethod)) {
+                    $aAdditionalFields[$sField] = $this->$mixedMethod($sTmpFile, $sMimeType, $sExt, $this);
+                }
+                elseif (is_array($mixedMethod) && isset($mixedMethod['module']) && isset($mixedMethod['method'])) {
+                    $mixedMethod['params'] = array($sTmpFile, $sMimeType, $sExt, $this);
+                    $aAdditionalFields[$sField] = call_user_func_array('bx_srv', array($mixedMethod['module'], $mixedMethod['method'], $mixedMethod['params'], isset($mixedMethod['class']) ? $mixedMethod['class'] : 'Module'));
+                } 
+            }
+        }
+
+        bx_alert('system', 'store_file', $iContentId, $iProfileId, array(
+            'storage_object' => $this->_aObject['object'],
+            'object' => $this,
+            'file_name' => $oHelper->getName(),
+            'mime_type' => $sMimeType,
+            'ext' => $sExt,
+            'size' => $oHelper->getSize(),
+            'file_path' => $sTmpFile,
+            'store_method' => $sMethod,
+            'store_method_params' => $aMethodParams,
+        ));
+
+        // store file to storage engine
+
+        $sLocalId = $this->genRandName();
+        $sPath = $this->genPath($sLocalId, $this->_aObject['levels']);
+        $sRemoteNamePath = $this->genRemoteNamePath ($sPath, $sLocalId, $sExt);
+
+        if (!$this->addFileToEngine($sTmpFile, $sLocalId, $oHelper->getName(), $isPrivate, $iProfileId)) {
+            unlink($sTmpFile);
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_ENGINE_ADD);
+            return false;
+        }
+        unlink($sTmpFile);
+
+        // add record in db
+
+        $iTime = time();
+        $iSize = $oHelper->getSize();
+        $bFileAdded = $this->_oDb->addFile($iProfileId, $sLocalId, $sRemoteNamePath, $oHelper->getName(), $sMimeType, $sExt, $iSize, $iTime, $isPrivate, $aAdditionalFields);
+        $iId = $this->_oDb->lastId();
+        if (!$bFileAdded || !$iId) {
+            $this->deleteFileFromEngine($sPath . $sLocalId, $isPrivate);
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_DB);
+            return false;
+        }
+
+        // after upload callback + triggers update
+
+        if (!$this->onFileAdded (array(
+            'id' => $iId,
+            'profile_id' => $iProfileId,
+            'content_id' => $iContentId,
+            'remote_id' => $sLocalId,
+            'path' => $sPath . $sLocalId,
+            'file_name' => $oHelper->getName(),
+            'mime_type' => $sMimeType,
+            'size' => $iSize,
+            'private' => $isPrivate ? 1 : 0,
+        ))) {
+            $this->deleteFileFromEngine($sPath . $sLocalId, $isPrivate);
+            $this->_oDb->deleteFile($iId);
+            return false;
+        }
+
+        return $iId;
+    }
+
+    /**
+     * convert default multiple files array into more logical one
+     */
+    public function convertMultipleFilesArray($aFiles)
+    {
+        if (!is_array($aFiles) || !is_array($aFiles['name']))
+            return false;
+        $aRet = array ();
+        foreach ($aFiles['name'] as $i => $sName) {
+            foreach ($aFiles as $sKey => $r) {
+                if (!$aFiles['name'][$i])
+                    break;
+                $aRet[$i][$sKey] = $aFiles[$sKey][$i];
+            }
+        }
+        return $aRet;
+    }
+
+    /**
+     * the same as storeFile, but it tries to do it directly from uploaded file
+     */
+    public function storeFileFromForm($aFile, $isPrivate = true, $iProfileId = 0, $iContentId = 0)
+    {
+        return $this->storeFile('Form', array('file' => $aFile), false, $isPrivate, $iProfileId, $iContentId);
+    }
+
+    /**
+     * the same as storeFile, but it tries to do it directly from HTML5 file upload method
+     */
+    public function storeFileFromXhr($sName, $isPrivate = true, $iProfileId = 0, $iContentId = 0)
+    {
+        return $this->storeFile('Xhr', array('name' => $sName), false, $isPrivate, $iProfileId, $iContentId);
+    }
+
+    /**
+     * the same as storeFile, but it tries to do it directly from local file
+     */
+    public function storeFileFromPath($sPath, $isPrivate = true, $iProfileId = 0, $iContentId = 0)
+    {
+        return $this->storeFile('Path', array('path' => $sPath), false, $isPrivate, $iProfileId, $iContentId);
+    }
+
+    /**
+     * the same as storeFileFromPath, but it tries to do it from URL
+     */
+    public function storeFileFromUrl($sUrl, $isPrivate = true, $iProfileId = 0, $iContentId = 0)
+    {
+        return $this->storeFile('Url', array('url' => $sUrl), false, $isPrivate, $iProfileId, $iContentId);
+    }
+
+    /**
+     * the same as storeFile, but it tries to do it directly from the same or another storage by file id
+     * @param $aParams['id'] - file ID in the storage
+     * @param $aParams['storage'] - the storage name
+     */
+    public function storeFileFromStorage($aParams, $isPrivate = true, $iProfileId = 0, $iContentId = 0)
+    {
+        if (!isset($aParams['id']) || !(int)$aParams['id'])
+            $aParams['id'] = 0;
+
+        if (!isset($aParams['storage']))
+            $aParams['storage'] = $this->_aObject['object'];
+
+        return $this->storeFile('Storage', $aParams, false, $isPrivate, $iProfileId, $iContentId = 0);
+    }
+
+    /**
+     * Delete file by file id.
+     */
+    public function deleteFile($iFileId, $iProfileId = 0)
+    {
+        $aFile = $this->_oDb->getFileById ($iFileId);
+        if (!$aFile) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_FILE_NOT_FOUND);
+            return false;
+        }
+
+        if (!$this->onBeforeFileDelete ($aFile, $iProfileId)) {
+            return false;
+        }
+
+        if (!$this->deleteFileFromEngine($aFile['path'], $aFile['private'])) {
+            return false;
+        }
+
+        $aGhost = $this->_oDb->getGhost($aFile['id']);
+
+        if (!$this->_oDb->deleteFile($aFile['id'])) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_DB);
+            return false;
+        }
+
+        if (!$this->onFileDeleted ($aFile, $iProfileId, $aGhost)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Queue file(s) for deletion. File(s) will be deleted later upon cron call (usually every minute).
+     * @param $mixedFileId file id or array of file ids.
+     * @return number of queued files
+     */
+    public function queueFilesForDeletion($mixedFileId)
+    {
+        if (!is_array($mixedFileId))
+            $mixedFileId = array ($mixedFileId);
+        bx_import('BxDolForm');
+        $oChecker = new BxDolFormCheckerHelper();
+        return $this->_oDb->queueFilesForDeletion (array_unique($oChecker->passInt($mixedFileId)));
+    }
+
+    /**
+     * Queue file(s) for deletion by getting neccesary files from ghosts table by profile id and content id
+     * @param $iProfileId profile id associated with files
+     * @param $iContentId content id associated with files, or false if to check by profile id only
+     * @return number of queued files
+     */
+    public function queueFilesForDeletionFromGhosts($iProfileId, $iContentId = false)
+    {
+        $aFiles = $this->getGhosts ($iProfileId, $iContentId);
+        return $this->queueFiles($aFiles);
+    }
+
+    /**
+     * Queue file(s) for deletion of the whole storage object
+     * @return number of queued files
+     */
+    public function queueFilesForDeletionFromObject()
+    {
+        $aFiles = $this->getFiles (false);
+        return $this->queueFiles($aFiles);
+    }
+
+    /**
+     * Download file.
+     * @param array $aFile downloading file info.
+     * @param boolean $bForceDownloadDialog if downloading to a local file system first is required and/or send the outout as attachment rather than inline.
+     */
+    public function download ($aFile, $sToken = false, $bForceDownloadDialog = 'auto')
+    {
+    	$bRet = true;
+        bx_alert($this->_aObject['object'], 'file_downloaded', $aFile['id'], bx_get_logged_profile_id(), array(
+        	'profile_ip' => getVisitorIP(),
+        	'file_info' => $aFile, 
+        	'return_value' => &$bRet
+        ));
+
+        return $bRet;
+    }
+
+    /**
+     * Set file private or public.
+     */
+    public function setFilePrivate($iFileId, $isPrivate = true)
+    {
+        if (!$this->_oDb->modifyFilePrivate ($iFileId, $isPrivate)) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_DB);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Get file url.
+     * @param $sRemoteId file remote id
+     * @return file url or false on error
+     */
+    public function getFileUrlByRemoteId($sRemoteId)
+    {
+        $aFile = $this->_oDb->getFileByRemoteId($sRemoteId);
+        if (!$aFile) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_FILE_NOT_FOUND);
+            return false;
+        }
+
+        return $this->getFileUrlById($aFile['id']);
+    }
+
+    /**
+     * Get file url.
+     * @param $iFileId file id
+     * @return file url or false on error
+     */
+    public function getFileUrlById($iFileId) { }
+
+    /**
+     * Get file info array by file id.
+     * @param $iFileId file id
+     * @return array
+     */
+    public function getFile($iFileId)
+    {
+        $a = $this->_oDb->getFileById($iFileId);
+        if (!$a) return false;
+
+        // update custom fields for previously uploaded files
+        if (!defined('BX_DOL_STORAGE_CUSTOM_FIELDS_UPDATE_SKIP') && isset($this->_aParams['fields']) && is_array($this->_aParams['fields'])) {
+            foreach ($this->_aParams['fields'] as $sField => $mixedMethod) {
+                if ($a[$sField])
+                    continue;
+
+                $sFileUrl = $this->getFileUrlById($iFileId);
+
+                if (is_string($mixedMethod) && method_exists($this, $mixedMethod)) {
+                    $a[$sField] = $this->$mixedMethod($sFileUrl, $a['mime_type'], $a['ext'], $this);
+                }
+                elseif (is_array($mixedMethod) && isset($mixedMethod['module']) && isset($mixedMethod['method'])) {
+                    $mixedMethod['params'] = array($sFileUrl, $a['mime_type'], $a['ext'], $this);
+                    $a[$sField] = call_user_func_array('bx_srv', array($mixedMethod['module'], $mixedMethod['method'], $mixedMethod['params'], isset($mixedMethod['class']) ? $mixedMethod['class'] : 'Module'));
+                } 
+                if ($a[$sField])
+                    $this->_oDb->modifyCustomField($iFileId, $sField, $a[$sField], false);
+            }
+        }
+
+        return $a;
+    }
+
+    /**
+     * Get ghost file info array by file id.
+     * @param $iFileId file id
+     * @return array
+     */
+    public function getGhost($iFileId)
+    {    
+        return $this->_oDb->getGhost($iFileId);
+    }
+
+    /**
+     * check if file is private or public
+     * @param $iFileId file id
+     * @return boolean
+     */
+    public function isFilePrivate($iFileId)
+    {
+        $aFile = $this->getFile ($iFileId);
+        return $aFile['private'] ? true : false;
+    }
+
+    /**
+     * Call this function after saving/associate just uploaded file id, so file is not orphaned/ghost.
+     * Ghost files appear on download form automaticaly during next upload,
+     * for example if file was uploaded but was not submitted for some reason.
+     * This mechanism ensure that the file is not lost.
+     * @param $mixedFileIds array of file ids or just one file id
+     * @param $iProfileId profile id
+     * @param return number of deleted ghost files
+     */
+    public function afterUploadCleanup($mixedFileIds, $iProfileId, $iContentId = false)
+    {
+        return $this->_oDb->deleteGhosts($mixedFileIds, $iProfileId, $iContentId);
+    }
+
+    /**
+     * Get ghost/orphaned files for particular user.
+     * @param $iProfileId profile id
+     * @param $iContentId content id, or false to not consider content id at all
+     * @param $isCheckAllAccountProfiles get all files associated with all account profiles
+     * @param $isAdmin if true, then don't check files ownership, it makes sense when $iContentId is provided, so it will return all files assiciated with content
+     * @return array of arrays
+     */
+    public function getGhosts($iProfileId, $iContentId = false, $isCheckAllAccountProfiles = false, $isAdmin = false)
+    {
+        if ($isCheckAllAccountProfiles && ($oProfile = BxDolProfile::getInstance($iProfileId))) {
+            $oAccount = $oProfile->getAccountObject();
+            $aProfiles = $oAccount->getProfilesIds(false, false);
+            return $this->_oDb->getGhosts($aProfiles, $iContentId, $isAdmin);
+        }
+        
+        return $this->_oDb->getGhosts($iProfileId, $iContentId, $isAdmin);
+    }
+    
+    /**
+     * Reorder ghost/orphaned files for particular content/user.
+     * @param $iProfileId profile id
+     * @param $iContentId content id, or false to not consider content id at all
+     * @param $aGhosts an ordered list of ghost/orphaned files' IDs.
+     * @return boolean result of operation
+     */
+    public function reorderGhosts($iProfileId, $iContentId, $aGhosts)
+    {
+        $bResult = true;
+
+        $iGhosts = count($aGhosts);
+        for($i = 0; $i < $iGhosts; $i++)
+            $bResult &= $this->_oDb->updateGhostOrder($iProfileId, $iContentId, (int)$aGhosts[$i], $i);
+
+        return $bResult;
+    }
+
+    /**
+     * Update ghosts' content id.
+     * @param $mixedFileIds array of file ids or just one file id
+     * @param $iProfileId profile id
+     * @param $iContentId content id
+     * @param $isAdmin if true, then don't check files ownership
+     * @return true on success or false otherwise
+     */
+    public function updateGhostsContentId($mixedFileIds, $iProfileId, $iContentId, $isAdmin = false)
+    {
+        $aProfiles = array();
+        if ($oProfile = BxDolProfile::getInstance($iProfileId)) {
+            $oAccount = $oProfile->getAccountObject();
+            $aProfiles = $oAccount->getProfilesIds(false);
+        }
+
+        return $this->_oDb->updateGhostsContentId($mixedFileIds, $iProfileId, $iContentId, $aProfiles, $isAdmin);
+    }
+
+    /**
+     * Get files list for particular user.
+     * @param $iProfileId profile id
+     * @return array of arrays
+     */
+    public function getFiles($iProfileId)
+    {
+        return $this->_oDb->getFiles($iProfileId);
+    }
+
+    /**
+     * Get all files in the storage
+     */
+    public function getFilesAll($iStart = 0, $iPerPage = 1000)
+    {
+        return $this->_oDb->getFilesAll($iStart, $iPerPage);
+    }
+
+    /**
+     * Get readable representation of restrictions by file extentions.
+     * @param $iProfileId profile id
+     * @return string
+     */
+    public function getRestrictionsTextExtensions ($iProfileId)
+    {
+        switch ($this->_aObject['ext_mode']) {
+            case 'allow-deny':
+                if (!$this->_aObject['ext_allow'])
+                    return _t('_sys_storage_restriction_ext_all_denied');
+                return _t('_sys_storage_restriction_ext_allowed', _t_format_extensions($this->_aObject['ext_allow']));
+            case 'deny-allow':
+                if (!$this->_aObject['ext_deny'])
+                    return _t('_sys_storage_restriction_ext_all_allowed');
+                return _t('_sys_storage_restriction_ext_denied', _t_format_extensions($this->_aObject['ext_deny']));
+            default:
+                return _t('_sys_storage_restriction_ext_all_denied');
+        }
+    }
+
+    /**
+     * @return list of allowed extensions, if ext_mode = 'allow-deny', returns false otherwise.
+     */
+    public function getAllowedExtensions ()
+    {
+        if ('allow-deny' == $this->_aObject['ext_mode'])
+            return _t_format_extensions($this->_aObject['ext_allow']);
+        return null;
+    }
+    
+    /**
+     * Get readable representation of restrictions by file size.
+     * @param $iProfileId profile id
+     * @return string
+     */
+    public function getRestrictionsTextFileSize ($iProfileId)
+    {
+        return _t('_sys_storage_restriction_size', _t_format_size($this->getMaxUploadFileSize($iProfileId)));
+    }
+
+    /**
+     * Get readable representation of all restrictions.
+     * @param $iProfileId profile id
+     * @return array of strings
+     */
+    public function getRestrictionsTextArray ($iProfileId)
+    {
+        $aTypes = array('Extensions', 'FileSize');
+        $aRet = array();
+        foreach ($aTypes as $sType) {
+            $sFunc = 'getRestrictionsText' . $sType;
+            $s = $this->$sFunc ($iProfileId);
+            if ($s)
+                $aRet[] = $s;
+        }
+        return $aRet;
+    }
+
+    /**
+     * Reread available mimetypes from particular file.
+     * It clears 'sys_storage_mime_types' table and fill it with data form provided file.
+     * The format of file is: mime/type _space_or_tab_ extentions_sperated_by_space.
+     * Usually the file is mime.types file from apache or /etc/mime.types from unix systems.
+     * @param $sFile file to read mime types from
+     * @return false if file was not found or can not be read, string with result on other case - it can contains file markup errors or localized "Success" string if everything went fine.
+     */
+    public function reloadMimeTypesFromFile ($sFile)
+    {
+        $sResult = '';
+
+        /* 
+         * These mime types must be manually added/replaced if they aren't defined in the mime type file
+         *
+
+            text/x-php                  php
+            text/x-coffeescript         coffee
+            text/x-common-lisp          lsp lisp
+            text/x-diff                 diff
+            text/x-go                   go
+            text/x-java                 java
+            text/x-lua                  lua
+            text/x-perl                 pl prl perl
+            text/x-python               py
+            text/nginx                  nginx
+            text/x-ini                  ini
+            text/x-ruby                 rb
+            text/x-sass                 sass
+            text/x-sh                   bash sh
+            text/x-swift                swift
+            text/x-vb                   vb
+            text/vbscript               vbs
+            text/x-vue                  vue
+            text/x-yaml                 yaml
+            text/x-sql                  sql
+            text/x-markdown             md
+            application/xquery          xq xquery
+            application/x-powershell    ps1
+            application/x-aspx          aps
+            application/x-jsp           jsp
+
+        */
+
+        $aIconsFont = array (
+            'far file-pdf' => array('pdf'),
+            'far file-word' => array('doc', 'docx'),
+            'far file-excel' => array('xls', 'xlt', 'xlsx', 'sxc', 'stc', 'ods', 'ots', 'sdc', 'csv', 'dif', 'slk', 'pxl'),
+            'far file-powerpoint' => array('ppt', 'pptx', 'sxi', 'sti', 'odp', 'sdp', 'sdd'),
+            'far file-code' => array('1st', 'aspx', 'asp', 'json', 'js', 'jsp', 'java', 'php', 'xml', 'html', 'xhtml', 'htm', 'rdf', 'xsd', 'xsl', 'xslt', 'sax', 'rss', 'dtd', 'cfm', 'js', 'asm', 'pl', 'prl', 'bas', 'b', 'fs', 'src', 'cs', 'ws', 'cgi', 'bat', 'py', 'c', 'cpp', 'cc', 'cp', 'h', 'hh', 'cxx', 'hxx', 'c++', 'm', 'lua', 'swift', 'sh', 'as', 'cob', 'tpl', 'lsp', 'x', 'cmd', 'rb', 'cbl', 'pas', 'pp', 'vb', 'vbs', 'f', 'perl', 'jl', 'lol', 'bal', 'pli', 'css', 'less', 'sass', 'saas', 'scss', 'bcc', 'coffee', 'jade', 'j', 'tea', 'c#', 'sas', 'diff', 'pro', 'for', 'sh', 'bsh', 'bash', 'twig', 'csh', 'lisp', 'lsp', 'cobol', 'pl', 'd', 'git', 'rb', 'hrl', 'cr', 'inp', 'a', 'go', 'as3', 'm', 'sql', 'md', 'mbox', 'nginx', 'pgp', 'asc', 'sig', 'ps1', 'rq', 'ttl', 'vue', 'xquery', 'xq'),
+            'far file-image' => 'image/', 
+            'far file-video' => 'video/',
+            'far file-audio' => 'audio/',
+            'far file-alt' => 'text/',
+            'far file-archive' => array('7z', '7zip', 'aar', 'ace', 'alz', 'arj', 'bz2', 'bza', 'bzip2', 'bzp', 'bzp2', 'cab', 'czip', 'gnutar', 'gz', 'gza', 'gzi', 'gzip', 'ha', 'lhz', 'lzma', 'pzip', 'rar', 'roo', 's7z', 'tar', 'tar-gz', 'tar-lzma', 'tar-z', 'taz', 'tbz', 'tbz2', 'tgz', 'tz', 'z', 'zip', 'zipx', 'zix', 'zoo'),
+        );
+        
+        $aIcons = array (
+            'mime-type-psd.svg' => array('psd'),
+            'mime-type-png.svg' => array('png'),
+            'mime-type-image.svg' => 'image/',
+            'mime-type-video.svg' => 'video/',
+            'mime-type-audio.svg' => 'audio/',
+            'mime-type-presentation.svg' => array('ppt', 'pptx', 'sxi', 'sti', 'odp', 'sdp', 'sdd'),
+            'mime-type-spreadsheet.svg' => array('xls', 'xlt', 'xlsx', 'sxc', 'stc', 'ods', 'ots', 'sdc', 'csv', 'dif', 'slk', 'pxl'),
+            'mime-type-document.svg' => array('doc', 'docx', 'odt', 'ott', 'sxw', 'stw', 'rtf', 'sdw', 'txt', 'pdb', 'psw', 'pdf'),
+            'mime-type-vector.svg' => array('ac5', 'ac6', 'aff', 'agd1', 'ai', 'ait', 'art', 'awg', 'b2f', 'cag', 'cbd', 'cdl', 'cdr', 'cdr3', 'cdr4', 'cdr5', 'cdr6', 'cdrw', 'cdx', 'cgm', 'cht', 'cil', 'cit', 'cnv', 'csl', 'ctn', 'cv5', 'cvg', 'cvi', 'cvl', 'cvs', 'cvx', 'dcs', 'ddoc', 'ddrw', 'ded', 'design', 'dmw', 'do', 'dpp', 'dpr', 'draw', 'drw', 'dsf', 'dsf', 'dsx', 'dvg', 'dxb', 'emb', 'evf', 'fcd', 'fh', 'fhd', 'fmv', 'fs', 'ft10', 'ft11', 'ft9', 'ft8', 'gem', 'gl2', 'graffle', 'gsd', 'gsd', 'hpg', 'hpgl', 'hpgl2', 'hpl', 'hplj', 'hpp', 'hppcl', 'idw', 'ima', 'macdraw', 'mgcb', 'mgs', 'mvg', 'nap', 'naplps', 'odg', 'p10', 'pat', 'pct', 'pd', 'pdw', 'pgs', 'pic', 'pif', 'pix', 'plo', 'plot', 'plt', 'ps', 'psid', 'pws', 'rdl', 's57', 'sdw', 'sif', 'sk2', 'slddwg', 'sp', 'spa', 'svf', 'svg', 'svgb', 'svgz', 'sxd', 'tdr', 'tlc', 'tng', 'vbr', 'vec', 'vect', 'veh', 'vml', 'vss', 'web', 'web', 'web', 'yal'),
+            'mime-type-archive.svg' => array('7z', '7zip', 'aar', 'ace', 'alz', 'arj', 'bz2', 'bza', 'bzip2', 'bzp', 'bzp2', 'cab', 'czip', 'gnutar', 'gz', 'gza', 'gzi', 'gzip', 'ha', 'lhz', 'lzma', 'pzip', 'rar', 'roo', 's7z', 'tar', 'tar-gz', 'tar-lzma', 'tar-z', 'taz', 'tbz', 'tbz2', 'tgz', 'tz', 'z', 'zip', 'zipx', 'zix', 'zoo'),
+        );
+
+        $f = fopen ($sFile, 'r');
+        if (!$f)
+            return false;
+
+        $this->_oDb->clearAllMimeTypes();
+
+        while (!feof($f) && ($s = fgets($f, 4096)) !== false) {
+            $s = trim($s);
+            if (!$s || '#' == $s[0])
+                continue;
+
+            $a = preg_split ("/[\s\\b]+/", $s, 2);
+            if (!isset($a[0]) || !isset($a[1]))
+                continue;
+
+            $sMimeType = $a[0];
+            $aExts = preg_split ("/[\s]+/", $a[1]);
+
+            foreach ($aExts as $sExt) {
+                $sIcon = $this->determineIcon($aIcons, $sExt, $sMimeType);
+                $sIconFont = $this->determineIcon($aIconsFont, $sExt, $sMimeType);
+
+                if (!$this->_oDb->addMimeType($sMimeType, $sExt, $sIcon, $sIconFont))
+                    $sResult .= _t('_Error') . ': ' . $sMimeType . "\t" . $sExt . "\n";
+            }
+        }
+
+        fclose($f);
+
+        if (!$sResult)
+            $sResult = _t('_Success');
+
+        return $sResult;
+    }
+
+    protected function determineIcon($aIcons, $sExt, $sMimeType)
+    {
+        foreach ($aIcons as $sIc => $if) {
+            if (!(is_array($if) && in_array($sExt, $if)) && !(is_string($if) && 0 === strncmp($if, $sMimeType, strlen($if))))
+                continue;
+            return $sIc;
+        }
+        return false;
+    }
+
+    /**
+     * Get file extension by file name.
+     * @param $sFileName file name
+     * @return file extention string
+     */
+    public function getFileExt ($sFileName)
+    {
+        return strtolower(pathinfo($sFileName, PATHINFO_EXTENSION));
+    }
+
+    /**
+     * Get file title by file name, actually file title is file name without extension.
+     * @param $sFileName file name
+     * @return file title string
+     */
+    public function getFileTitle ($sFileName)
+    {
+        return pathinfo($sFileName, PATHINFO_FILENAME);
+    }
+
+    /**
+     * Get file mime/type by file name.
+     * @param $sFileName file name
+     * @return file mime type string
+     */
+    public function getMimeTypeByFileName ($sFileName)
+    {
+        $sExt = $this->getFileExt($sFileName);
+        $sMimeType = $this->_oDb->getMimeTypeByExt($sExt);
+        if (!$sMimeType)
+            $sMimeType = BX_DOL_STORAGE_DEFAULT_MIME_TYPE;
+        return $sMimeType;
+    }
+
+    /**
+     * Get file icon by file name.
+     * File icon is just icon filename without patch or URL.
+     * File icons must be located in images/icons directory in your template subfolder.
+     * @param $sFileName file name
+     * @return file icon string
+     */
+    public function getIconNameByFileName ($sFileName)
+    {
+        $sExt = $this->getFileExt($sFileName);
+        $sIcon = $this->_oDb->getIconByExt($sExt);
+        if (!$sIcon)
+            $sIcon = BX_DOL_STORAGE_DEFAULT_ICON;
+        return $sIcon;
+    }
+
+    /**
+     * Get file font icon by file name.
+     * Actually just a class name of icon is returnted.
+     * @param $sFileName file name
+     * @return file icon string
+     */
+    public function getFontIconNameByFileName ($sFileName)
+    {
+        $sExt = $this->getFileExt($sFileName);
+        $sIcon = $this->_oDb->getIconFontByExt($sExt);
+        if (!$sIcon)
+            $sIcon = BX_DOL_STORAGE_DEFAULT_ICON_FONT;
+        return $sIcon;
+    }
+    
+    // ------------ internal functions - events
+
+    protected function onBeforeFileAdd ($aFileInfo)
+    {
+        if ($aFileInfo['size'] > $this->getMaxUploadFileSize($aFileInfo['profile_id'])) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_FILE_TOO_BIG);
+            return false;
+        }
+
+        // TODO: check site quota - BX_DOL_STORAGE_ERR_SITE_QUOTA_EXCEEDED
+
+        $aObjectQuota = $this->_oDb->getStorageObjectQuota();
+        if (
+            ($aObjectQuota['quota_size'] && ($aObjectQuota['current_size'] + $aFileInfo['size'] > $aObjectQuota['quota_size']))
+            ||
+            ($aObjectQuota['quota_number'] && ($aObjectQuota['current_number'] + 1 > $aObjectQuota['quota_number']))
+            ) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_OBJECT_QUOTA_EXCEEDED);
+            return false;
+        }
+
+        $aUserQuota = $this->_oDb->getUserQuota($aFileInfo['profile_id']);
+        if (
+            ($aUserQuota['quota_size'] && ($aUserQuota['current_size'] + $aFileInfo['size'] > $aUserQuota['quota_size']))
+            ||
+            ($aUserQuota['quota_number'] && ($aUserQuota['current_number'] + 1 > $aUserQuota['quota_number']))
+            ) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_USER_QUOTA_EXCEEDED);
+            return false;
+        }
+
+        $this->setErrorCode(BX_DOL_STORAGE_ERR_OK);
+
+        $bRet = true;
+        bx_alert($this->_aObject['object'], 'before_file_add', 0, $aFileInfo['profile_id'], array('file_info' => $aFileInfo, 'return_value' => &$bRet));
+        return $bRet;
+    }
+
+    protected function onFileAdded ($aFileInfo)
+    {
+        // TODO: update site quota - BX_DOL_STORAGE_ERR_SITE_QUOTA_EXCEEDED
+
+        if (!$this->_oDb->updateStorageObjectQuota($aFileInfo['size'], 1)) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_DB);
+            return false;
+        }
+
+        if (!$this->_oDb->updateUserQuota($aFileInfo['profile_id'], $aFileInfo['size'], 1)) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_DB);
+            return false;
+        }
+
+        if (!$this->insertGhost ($aFileInfo['id'], $aFileInfo['profile_id'], $aFileInfo['content_id'])) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_DB);
+            return false;
+        }
+
+        $this->setErrorCode(BX_DOL_STORAGE_ERR_OK);
+
+        $bRet = true;
+        bx_alert($this->_aObject['object'], 'file_added', $aFileInfo['id'], $aFileInfo['profile_id'], array('file_info' => $aFileInfo, 'return_value' => &$bRet));
+        return $bRet;
+    }
+
+    function insertGhost($iFileId, $iProfileId, $iContentId = 0)
+    {
+        return $this->_oDb->insertGhosts ($iFileId, $iProfileId, $iContentId);
+    }
+
+    function onBeforeFileDelete ($aFileInfo, $iProfileId)
+    {
+        $this->setErrorCode(BX_DOL_STORAGE_ERR_OK);
+
+        $bRet = true;
+        bx_alert($this->_aObject['object'], 'before_file_delete', $aFileInfo['id'], $iProfileId, array('file_info' => $aFileInfo, 'return_value' => &$bRet));
+        return $bRet;
+    }
+
+    function onFileDeleted ($aFileInfo, $iProfileId, $aGhost = false)
+    {
+        // TODO: update site quota
+
+        if (!$this->_oDb->updateStorageObjectQuota(-$aFileInfo['size'], -1)) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_DB);
+            return false;
+        }
+
+        if (!$this->_oDb->updateUserQuota($aFileInfo['profile_id'], -$aFileInfo['size'], -1)) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_DB);
+            return false;
+        }
+
+        $this->setErrorCode(BX_DOL_STORAGE_ERR_OK);
+
+        $bRet = true;
+        bx_alert($this->_aObject['object'], 'file_deleted', $aFileInfo['id'], $iProfileId, array('file_info' => $aFileInfo, 'ghost' => $aGhost, 'return_value' => &$bRet));
+        return $bRet;
+    }
+
+    // ------------ internal functions
+
+    protected function setErrorCode($i)
+    {
+        return ($this->_iErrorCode = $i);
+    }
+
+    protected function genRandName($isCheckForUniq = true)
+    {
+        $sRandName = strtolower(genRndPwd(32, false));
+        if ($isCheckForUniq) {
+            $iTries = 10;
+            do {
+                $aFile = $this->_oDb->getFileByRemoteId($sRandName);
+                $bExist = is_array($aFile) && $aFile;
+            } while (--$iTries && $bExist);
+        }
+        return $sRandName;
+    }
+
+    protected function genPath($s, $iLevels)
+    {
+        $sRet = '';
+        $i = 1;
+        while ($iLevels-- > 0)
+            $sRet .= substr($s, 0, $i++) . '/';
+        return $sRet;
+    }
+
+    protected function genRemoteNamePath ($sPath, $sLocalId, $sExt)
+    {
+        return $sPath . $sLocalId;
+    }
+
+    protected function isValidExt ($sExt)
+    {
+        switch ($this->_aObject['ext_mode']) {
+            case 'allow-deny':
+                if ($this->isAllowedExt($sExt))
+                    return true;
+                return false;
+            case 'deny-allow':
+                if ($this->isDeniedExt($sExt))
+                    return false;
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    protected function isAllowedExt ($sExt)
+    {
+        return $this->isAllowedDeniedExt($sExt, 'ext_allow');
+    }
+
+    protected function isDeniedExt ($sExt)
+    {
+        return $this->isAllowedDeniedExt($sExt, 'ext_deny');
+    }
+
+    protected function isAllowedDeniedExt ($sExt, $sExtMode)
+    {
+        if ('' == $this->_aObject[$sExtMode])
+            return false;
+        if (!is_array($this->_aObject[$sExtMode]))
+            $this->_aObject[$sExtMode] = explode(',', $this->_aObject[$sExtMode]);
+        return in_array ($sExt, $this->_aObject[$sExtMode]);
+    }
+
+    public function queueFiles($aFiles)
+    {
+        if (!$aFiles)
+            return 0;
+
+        $a = array();
+        foreach ($aFiles as $aFile)
+            $a[] = $aFile['id'];
+
+        return $this->queueFilesForDeletion($a);
+    }
+
+    protected function getFileDuration($sFilePath, $sMimeType, $sExt, $oStorage)
+    {
+        if (strncmp('video/', $sMimeType, 6) === 0)
+            return (int)BxDolTranscoderVideo::getDuration($sFilePath);
+        return 0;
+    }
+
+    protected function getFileDimensions($sFilePath, $sMimeType, $sExt, $oStorage)
+    {
+        if (strncmp('image/', $sMimeType, 6) === 0 && $o = BxDolTranscoderImage::getObjectAbstract()) {
+            $a = $o->getSize($sFilePath);
+            if ($a && isset($a['w']) && isset($a['h']))
+                return $a['w'] . 'x' . $a['h'];
+        }
+        
+        if (strncmp('video/', $sMimeType, 6) === 0 && $o = BxDolTranscoderVideo::getObjectAbstract()) {
+            $a = $o->getSize($sFilePath);
+            if ($a && isset($a['w']) && isset($a['h']))
+                return $a['w'] . 'x' . $a['h'];
+        }
+        return '';
+    }
+}
+
+
+/**
+ * Handle file uploads via XMLHttpRequest
+ */
+class BxDolStorageHelperXhr
+{
+    protected $sName;
+
+    function __construct ($aParams)
+    {
+        $this->sName = $aParams['name'];
+    }
+
+    function getImmediateError()
+    {
+        return BX_DOL_STORAGE_ERR_OK;
+    }
+
+    function save($path)
+    {
+        $input = fopen("php://input", "r");
+        $temp = tmpfile();
+        $realSize = stream_copy_to_stream($input, $temp);
+        fclose($input);
+
+        if (false === $this->getSize() || $realSize != $this->getSize())
+            return false;
+
+        $target = fopen($path, "w");
+        fseek($temp, 0, SEEK_SET);
+        stream_copy_to_stream($temp, $target);
+        fclose($target);
+        fclose($temp);
+
+        return true;
+    }
+
+    function getName()
+    {
+        return $this->sName;
+    }
+
+    function getSize()
+    {
+        if (isset($_SERVER["CONTENT_LENGTH"]))
+            return (int)$_SERVER["CONTENT_LENGTH"];
+        else
+            return false;
+    }
+}
+
+/**
+ * Handle file uploads via regular form post (uses the $_FILES array)
+ */
+class BxDolStorageHelperForm
+{
+    protected $aFile;
+
+    function __construct ($aParams)
+    {
+        $this->aFile = $aParams['file'];
+    }
+
+    function getImmediateError()
+    {
+        if (!$this->aFile['size'] || !$this->aFile['tmp_name'])
+            return BX_DOL_STORAGE_ERR_NO_FILE;
+
+        if (UPLOAD_ERR_OK != $this->aFile['error'])
+            return (int)$this->aFile['error'];
+
+        return BX_DOL_STORAGE_ERR_OK;
+    }
+
+    function save($path)
+    {
+        if (!move_uploaded_file($this->aFile['tmp_name'], $path)){
+            return false;
+        }
+        return true;
+    }
+
+    function getName()
+    {
+        return $this->aFile['name'];
+    }
+
+    function getSize()
+    {
+        return $this->aFile['size'];
+    }
+}
+
+/**
+ * Store file from local file path
+ */
+class BxDolStorageHelperPath
+{
+    protected $sPath;
+
+    function __construct ($aParams)
+    {
+        $this->sPath = $aParams['path'];
+    }
+
+    function getImmediateError()
+    {
+        if (!$this->sPath || !file_exists($this->sPath))
+            return BX_DOL_STORAGE_ERR_NO_FILE;
+
+        return BX_DOL_STORAGE_ERR_OK;
+    }
+
+    function save($path)
+    {
+        if (!copy($this->sPath, $path)) {
+            return false;
+        }
+        return true;
+    }
+
+    function getName()
+    {
+        return pathinfo($this->sPath, PATHINFO_BASENAME);
+    }
+
+    function getSize()
+    {
+        return filesize($this->sPath);
+    }
+}
+
+/**
+ * Store file from URL
+ */
+class BxDolStorageHelperUrl extends BxDolStorageHelperPath
+{
+    protected $aMime2Ext = array (
+        'image/bmp' => 'bmp',
+        'image/gif' => 'gif',
+        'image/jpeg' => 'jpg',
+        'image/pjpeg' => 'jpg',
+        'image/png' => 'png',
+    );
+
+    function __construct ($aParams)
+    {   
+        $aParams['path'] = '';
+        $sExt = pathinfo(parse_url($aParams['url'], PHP_URL_PATH), PATHINFO_EXTENSION);
+        if ($sTmpFilename = tempnam(BX_DIRECTORY_PATH_TMP, '')) {
+            $s = '';
+            if (!$sExt) {
+                $s = @file_get_contents($aParams['url']);
+                $oFinfo = new finfo(FILEINFO_MIME_TYPE);
+                $sMime = $oFinfo->buffer($s);
+                if (isset($this->aMime2Ext[$sMime]))
+                    $sExt = $this->aMime2Ext[$sMime];
+            }
+
+            $aParams['path'] =  $sTmpFilename . '.' . $sExt;
+            @rename($sTmpFilename, $aParams['path']);
+
+            if ($s) {
+                file_put_contents($aParams['path'], $s);
+            } 
+            else {
+                $hRead = @fopen($aParams['url'], "rb");
+                $hWrite = fopen($aParams['path'], "wb");
+                if (false !== $hRead && false !== $hWrite) {
+                    while (!feof($hRead)) {
+                        $data = fread($hRead, 8192);
+                        if (false === fwrite($hWrite, $data)) {
+                            fclose($hWrite);
+                            file_put_contents($aParams['path'], '');
+                            break;
+                        }
+                    }
+                    fclose($hRead);
+                    fclose($hWrite);
+                }
+            }
+        }
+        parent::__construct($aParams);
+    }
+
+    function getImmediateError()
+    {
+        $iRet = parent::getImmediateError();
+        if (BX_DOL_STORAGE_ERR_OK != $iRet)
+            return $iRet;
+        return $this->getSize() > 0 ? BX_DOL_STORAGE_ERR_OK : BX_DOL_STORAGE_INVALID_FILE;
+    }
+
+    function __destruct() 
+    {
+        @unlink($this->sPath);
+    }
+}
+
+/**
+ * Handle file uploads from the same or another storage object
+ */
+class BxDolStorageHelperStorage
+{
+    protected $iFileId;
+    protected $oStorage;
+    protected $aFile;
+
+    function __construct ($aParams)
+    {
+        $this->iFileId = $aParams['id'];
+        $this->oStorage = BxDolStorage::getObjectInstance($aParams['storage']);
+
+        $this->aFile = false;
+        if ($this->oStorage)
+            $this->aFile = $this->oStorage->getFile($this->iFileId);
+    }
+
+    function getImmediateError()
+    {
+        if (!$this->iFileId)
+            return BX_DOL_STORAGE_ERR_NO_FILE;
+
+        if (!$this->oStorage)
+            return BX_DOL_STORAGE_ERR_ENGINE_GET;
+
+        if (!$this->aFile)
+            return BX_DOL_STORAGE_ERR_NO_FILE;
+
+        return BX_DOL_STORAGE_ERR_OK;
+    }
+
+    function save($path)
+    {
+        $s = bx_file_get_contents ($this->oStorage->getFileUrlById($this->iFileId));
+        if (!$s)
+            return false;
+
+        return file_put_contents($path, $s) ? true : false;
+    }
+
+    function getName()
+    {
+        return $this->aFile['file_name'];
+    }
+
+    function getSize()
+    {
+        return $this->aFile['size'];
+    }
+}
+
+/** @} */

+ 966 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/classes/BxDolTranscoder.php

@@ -0,0 +1,966 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+define('BX_DOL_QUEUE_PENDING', 'pending'); ///< file is pending for processing
+define('BX_DOL_QUEUE_PROCESSING', 'processing'); ///< file is in the process
+define('BX_DOL_QUEUE_COMPLETE', 'complete'); ///< file is already processed
+define('BX_DOL_QUEUE_FAILED', 'failed'); ///< file processing was failed
+define('BX_DOL_QUEUE_DELETE', 'delete'); ///< file is subject to delete
+
+if (!defined('BX_DOL_MAX_RETINA_DIMENSION_MOBILE'))
+    define('BX_DOL_MAX_RETINA_DIMENSION_MOBILE', 500); ///< max image dimension which will be processes as retine on mobiles
+
+if (!defined('BX_DOL_SKIP_ATIME_UPDATE'))
+    define('BX_DOL_SKIP_ATIME_UPDATE', 43200); ///< time interval to not update last access time, it should be comaparatively low compared to atime_prining
+
+/**
+ * Base class for @see BxDolTranscoderImage and @see BxDolTranscoderVideo transcoders
+ */
+class BxDolTranscoder extends BxDolFactory implements iBxDolFactoryObject
+{
+    protected $_sLog = ''; ///< file conversion log
+    protected $_aObject; ///< object properties
+    protected $_oStorage; ///< storage object, transcoded files are stored here
+    protected $_oDb; ///< database queries object
+    protected $_sRetinaSuffix = '@2x'; ///< handler suffix for retina image file
+    protected $_sQueueTable = ''; ///< table with queued files for transcoding
+    protected $_sQueueStorage = ''; ///< storage object to storage temporary files
+    protected $_iDevicePixelRatio = false; ///< force device pixel ratio, 1 or 2
+
+    /**
+     * constructor
+     */
+    protected function __construct($aObject, $oStorage)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+        $this->_oStorage = $oStorage;
+        $this->_oDb = null;
+    }
+
+    /**
+     * Get transcode object instance.
+     * @param $sObject - name of transcode object.
+     * @return false on error or instance of transcoder class.
+     */
+    public static function getObjectInstance($sObject)
+    {
+        if (isset($GLOBALS['bxDolClasses'][__CLASS__ . '!' . $sObject]))
+            return $GLOBALS['bxDolClasses'][__CLASS__ . '!'.$sObject];
+
+        // get transcode object
+        $aObject = BxDolTranscoderQuery::getTranscoderObject($sObject);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        if ($aObject['source_params'])
+            $aObject['source_params'] = unserialize($aObject['source_params']);
+
+        $aObject['filters'] = false; // filters are initialized on demand
+
+        // create storage object to store transcoded data
+        $oStorage = BxDolStorage::getObjectInstance($aObject['storage_object']);
+        if (!$oStorage)
+            return false;
+
+        // create instance
+        $sClass = $aObject['override_class_name'] ? $aObject['override_class_name'] : 'BxDolTranscoderImage';
+        if (!empty($aObject['override_class_file']))
+            require_once(BX_DIRECTORY_PATH_ROOT . $aObject['override_class_file']);
+
+        $o = new $sClass ($aObject, $oStorage);
+
+        return ($GLOBALS['bxDolClasses'][__CLASS__ . '!' . $sObject] = $o);
+    }
+
+    /**
+     * Delete outdated transcoed data from all transcoeeed objects, by last access time.
+     * It called on cron, usually every day.
+     * @return total number of pruned/deleted files
+     */
+    static public function pruning ()
+    {
+        $iCount = 0;
+        $aObjects = self::getTranscoderObjects();
+        foreach ($aObjects as $aObject) {
+            if (!$aObject['atime_tracking'] || !$aObject['atime_pruning'])
+                continue;
+            $oTranscoder = self::getObjectInstance($aObject['object']);
+            if (!$oTranscoder)
+                continue;
+            $iCount += $oTranscoder->prune();
+        }
+        return $iCount;
+    }
+
+    /**
+     * Get transcoder objects array
+     * @return array
+     */
+    static protected function getTranscoderObjects ()
+    {
+        return BxDolTranscoderQuery::getTranscoderObjects ();
+    }
+
+    /**
+     * Process files queued for transcoding
+     * Can be performed on separate server.
+     */
+    static public function processQueue ($bQueuePruning = true)
+    {
+        if ($bQueuePruning)
+            self::pruneQueue();
+
+        $a = BxDolTranscoderQuery::getNextInQueue (gethostname());
+        foreach ($a as $r) {
+            $o = self::getObjectInstance($r['transcoder_object']);
+            if (!$o) {
+                BxDolTranscoderQuery::deleteFromQueueById($r['id']);
+                continue;
+            }
+
+            $aQueue = $o->getDb()->getFromQueue($r['file_id_source']); 
+            if (!$aQueue || $aQueue['status'] != BX_DOL_QUEUE_PENDING)
+                continue;
+            
+            $o->transcode($aQueue['file_id_source'], $aQueue['profile_id']);
+        }
+    }
+
+    static public function pruneQueue () 
+    {
+        $a = BxDolTranscoderQuery::getForDeletionFromQueue (gethostname());
+        if (!$a)
+            return false;
+
+        foreach ($a as $aQueue) {
+            $o = self::getObjectInstance($aQueue['transcoder_object']);
+            if ($o)
+                $o->deleteFromQueue($aQueue['file_id_source']);
+        }
+    }
+
+    /**
+     * Store completed files from tmp queue storage to the final storage
+     * Must be performed on the actual site.
+     */
+    static public function processCompleted ()
+    {
+        $a = BxDolTranscoderQuery::getCompletedFromQueue ();
+        foreach ($a as $r) {
+            $o = self::getObjectInstance($r['transcoder_object']);
+            $aQueue = $o->getDb()->getFromQueue($r['file_id_source']); 
+            if (!$aQueue || $aQueue['status'] != BX_DOL_QUEUE_COMPLETE)
+                continue;
+            
+            if ($aQueue['file_url_result'] && $aQueue['file_id_result']) {
+  
+                // get transcoded file from provided URL              
+                $sFileData = bx_file_get_contents ($aQueue['file_url_result']);
+                if (false === $sFileData) {
+                    $o->getDb()->updateQueueStatus($aQueue['file_id_source'], BX_DOL_QUEUE_FAILED, "download file failed: {$aQueue['file_url_result']}\n");
+                    bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: processCompleted failed, get transcoded file from URL ({$aQueue['file_url_result']}) failed");
+                    continue;   
+                }
+                
+                $sTmpFile = $o->getTmpFilename ('.' . $aQueue['file_ext_result']);
+                if (!file_put_contents($sTmpFile, $sFileData)) {
+                    $o->getDb()->updateQueueStatus($aQueue['file_id_source'], BX_DOL_QUEUE_FAILED, "store downloaded file failed\n");
+                    @unlink($sTmpFile);
+                    bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: processCompleted failed, store transcoded file to tmp file ({$sTmpFile}) failed");
+                    continue;
+                }
+                
+                // store downloaded file in final storage
+                if ($o->storeTranscodedFile($aQueue['file_id_source'], $sTmpFile, $aQueue['profile_id'])) {
+                   $o->getDb()->updateQueueStatus($aQueue['file_id_source'], BX_DOL_QUEUE_DELETE); // mark to delete it (deletion is performed on the server where it was transcoded)
+                } else {
+                    $o->getDb()->updateQueueStatus($aQueue['file_id_source'], BX_DOL_QUEUE_FAILED, "store file failed:\n" . $o->getLog());
+                    bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: processCompleted failed, store transcoded file ({$aQueue['file_id_source']}) in final storage failed: " . $o->getLog());
+                }
+
+                // delete tmp local file
+                @unlink($sTmpFile);
+            }
+        }
+    }
+
+    /**
+     * Register handlers array
+     * It can be called upon module enable event.
+     * @param $mixed array of transcoders objects, or just one object
+     */
+    static public function registerHandlersArray ($mixed)
+    {
+        self::_callFuncForObjectsArray ($mixed, 'registerHandlers');
+    }
+
+    /**
+     * Register system handlers
+     */
+    static public function registerHandlersSystem()
+    {
+        $sPrefix = 'sys_';
+
+        $aResults = array();
+        $aTranscoders = BxDolTranscoderQuery::getTranscoderObjects();
+        foreach($aTranscoders as $aTranscoder) {
+            if(strncasecmp($aTranscoder['object'], $sPrefix, strlen($sPrefix)) !== 0)
+                continue;
+
+            $aResults[] = $aTranscoder['object'];
+        }
+
+        if(!empty($aResults))
+            self::registerHandlersArray($aResults);
+    }
+
+    /**
+     * Unregister handlers array
+     * It can be called upon module disbale event.
+     * @param $mixed array of transcoders objects, or just one object
+     */
+    static public function unregisterHandlersArray ($mixed)
+    {
+        self::_callFuncForObjectsArray ($mixed, 'unregisterHandlers');
+    }
+
+    /**
+     * Cleanup (queue for deletion all resized files)
+     * It can be called upon module disbale event.
+     * @param $mixed array of transcoders objects, or just one object
+     */
+    static public function cleanupObjectsArray ($mixed)
+    {
+        self::_callFuncForObjectsArray ($mixed, 'cleanup');
+    }
+
+    /**
+     * Called automatically, upon local(transcoded) file deletetion.
+     */
+    static public function onAlertResponseFileDeleteLocal ($oAlert, $sObject)
+    {
+        $oTranscoder = self::getObjectInstance($sObject);
+        if (!$oTranscoder)
+            return;
+
+        if ($oAlert->sAction != 'file_deleted')
+            return;
+
+        $oTranscoder->onDeleteFileLocal($oAlert->iObject);
+    }
+
+    /**
+     * Called automatically, upon original file deletetion.
+     */
+    static public function onAlertResponseFileDeleteOrig ($oAlert, $sObject)
+    {
+        $oTranscoder = self::getObjectInstance($sObject);
+        if (!$oTranscoder)
+            return;
+
+        if ($oAlert->sAction != 'file_deleted')
+            return;
+
+        $oTranscoder->onDeleteFileOrig($oAlert->iObject);
+    }
+
+    /**
+     * Called automatically, upon local(transcoded) file deletetion.
+     */
+    public function onDeleteFileLocal($iFileId)
+    {
+        return $this->_oDb->deleteFileTraces($iFileId);
+    }
+
+    /**
+     * Called automatically, upon original file deletetion.
+     */
+    public function onDeleteFileOrig($mixedHandler)
+    {
+        // deleted files queued for transcoding
+        $this->_oDb->deleteFromQueue ($mixedHandler);
+
+        // delete main file
+        $iFileId = $this->_oDb->getFileIdByHandler($mixedHandler);
+        if ($iFileId && !$this->_oStorage->deleteFile($iFileId)) {
+            bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: onDeleteFileOrig failed for handler ({$mixedHandler}) delete failed");
+            return false;
+        }
+
+        // delete retina file
+        $iFileId = $this->_oDb->getFileIdByHandler($mixedHandler . $this->_sRetinaSuffix);
+        if ($iFileId && !$this->_oStorage->deleteFile($iFileId)) {
+            bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: onDeleteFileOrig failed for handler ({$mixedHandler}) delete retina file failed");
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Register necessary alert handlers for automatic deletetion of transcoded data if source file is deleted.
+     * Make sure that you call it once, before first usage, for example upon module installation.
+     */
+    public function registerHandlers ()
+    {
+        if (!$this->_oDb->registerHandlers ())
+            return false;
+        return $this->clearCacheDB();
+    }
+
+    /**
+     * Unregister alert handlers for automatic deletetion of transcoded data if source file is deleted.
+     * Make sure that you call it once, for example upon module uninstallation.
+     */
+    public function unregisterHandlers ()
+    {
+        if (!$this->_oDb->unregisterHandlers ())
+            return false;
+        return $this->clearCacheDB();
+    }
+
+    /**
+     * Delete (queue for deletion) all resized files for the current object
+     */
+    public function cleanup ()
+    {
+        return $this->_oStorage->queueFilesForDeletionFromObject();
+    }
+
+    /**
+     * Get storage object where transcoded data is stored
+     */
+    public function getStorage()
+    {
+        return $this->_oStorage;
+    }
+
+    /**
+     * Get database object instance
+     */
+    public function getDb()
+    {
+        return $this->_oDb;
+    }
+
+    /**
+     * check if transcoder suppors given file mime type
+     */ 
+    public function isMimeTypeSupported($sMimeType)
+    {
+        return false;
+    }
+
+    /**
+     * Get transcoded file url.
+     * If transcoded file is ready then direct url to the file is returned.
+     * If there is no transcoded data available, then special url is returned, upon opening this url image is transcoded automatically and redirects to the ready transcoed image.
+     * @param $mixedHandler - file handler
+     * @return file url, or false on error.
+     */
+    public function getFileUrl($mixedHandler)
+    {
+         if (bx_is_api())
+            return $this->getOrigFileUrl($mixedHandler);
+        
+        if ($this->isFileReady($mixedHandler)) {
+
+            $mixedHandler = $this->processHandlerForRetinaDevice($mixedHandler);
+
+            $iFileId = $this->_oDb->getFileIdByHandler($mixedHandler);
+            if (!$iFileId) {
+                bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: getFileUrl failed for handler ({$mixedHandler}) source file id wasn't found");
+                return false;
+            }
+
+            $aFile = $this->_oStorage->getFile($iFileId);
+            if (!$aFile) {
+                bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: getFileUrl failed for handler ({$mixedHandler}) source file wasn't found in the source storage");
+                return false;
+            }
+
+            if ($this->_aObject['atime_tracking'])
+                $this->_oDb->updateAccessTime($mixedHandler);
+
+            return $this->_oStorage->getFileUrlById($iFileId);
+        }
+
+        if ($this->_sQueueTable && !$this->addToQueue($mixedHandler)) { // add file to queue (when queue is enabled) if it isn't transcoded yet
+            bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: getFileUrl failed for handler ({$mixedHandler}), can't add file to queue");
+            return false;
+        }
+
+        return $this->getFileUrlNotReady($mixedHandler);
+    }
+
+    /**
+     * alias for @see getFileUrl
+     */
+    public function getFileUrlById($mixedHandler)
+    {
+        if (bx_is_api())
+            return $this->getOrigFileUrl($mixedHandler);
+        
+        return $this->getFileUrl($mixedHandler);
+    }
+
+    /**
+     * Check if transcoded data is available. No need to call it directly, it is called automatically when it is needed.
+     * @param $mixedHandler - file handler
+     * @param $isCheckOutdated - check if transcoded file is outdated
+     * @return false if there is no ready transcoded file is available or it is outdated, true if file is ready
+     */
+    public function isFileReady ($mixedHandler, $isCheckOutdated = true)
+    {
+        $sMethodFileReady = 'isFileReady_' . $this->_aObject['source_type'];
+        return $this->$sMethodFileReady($mixedHandler, $isCheckOutdated);
+    }
+
+    /**
+     * Transcode file, no need to call it directly, it is called automatically when it is needed.
+     * @param $mixedHandler - file handler
+     * @param $iProfileId - optional profile id, to assign transcoded file ownership to, usually it is NOT assigned to any particular profile, so just leave it default
+     * @return true on success, false on error
+     */
+    public function transcode ($mixedHandler, $iProfileId = 0)
+    {
+        $sExtChange = false;
+        $bRet = true;
+
+        $iPID = function_exists('posix_getpid') ? posix_getpid() : 0;
+        $this->_oDb->updateQueueStatus($mixedHandler, BX_DOL_QUEUE_PROCESSING, '', gethostname(), '', '', '', $iPID);
+
+        do {
+
+            // create tmp file locally
+            $sSuffix = $this->_sQueueStorage ? 'Queue' : $this->_aObject['source_type'];
+            $sMethodStoreFile = 'storeFileLocally_' . $sSuffix;
+            $sTmpFile = $this->$sMethodStoreFile($mixedHandler);
+            if (!$sTmpFile) {
+                $this->_oDb->updateQueueStatus($mixedHandler, BX_DOL_QUEUE_FAILED, "store local tmp file failed ($sMethodStoreFile)");
+                $bRet = false;
+                break;
+            }
+
+            // appply filters to tmp file
+            $this->initFilters ();
+            foreach ($this->_aObject['filters'] as $aParams) {
+                $this->clearLog();
+                $sMethodFilter = 'applyFilter_' . $aParams['filter'];
+                if (!method_exists($this, $sMethodFilter))
+                    continue;
+                if (!$this->$sMethodFilter($sTmpFile, $aParams['filter_params'])) {
+                    unlink($sTmpFile);
+                    $this->_oDb->updateQueueStatus($mixedHandler, BX_DOL_QUEUE_FAILED, "filter failed: $sMethodFilter\n" . $this->getLog());
+                    $bRet = false;
+                    break 2;
+                }
+
+                if (!empty($aParams['filter_params']['force_type'])) {
+                    switch ($aParams['filter_params']['force_type']) {
+                        case 'jpeg':
+                        case 'jpg':
+                            $sExtChange = 'jpg';
+                            break;
+                        default:
+                            $sExtChange = $aParams['filter_params']['force_type'];
+                    }
+                }
+
+                if ($sExtChange) {
+                    $iDotPos = strrpos($sTmpFile, '.');
+                    $sExtOld = false === $iDotPos ? '' : substr($sTmpFile, $iDotPos+1);
+                    if ($sExtOld != $sExtChange) {
+                        $sTmpFileOld = $sTmpFile;
+                        $sTmpFile = false === $iDotPos ? $sTmpFile . '.' . $sExtChange : substr_replace($sTmpFile, $sExtChange, $iDotPos+1, strlen($sExtOld));
+                        if (!rename($sTmpFileOld, $sTmpFile)) {
+                            @unlink($sTmpFileOld);
+                            $this->_oDb->updateQueueStatus($mixedHandler, BX_DOL_QUEUE_FAILED, "change file extension failed: $sTmpFileOld => $sTmpFile");
+                            $bRet = false;
+                            break 2;
+                        }
+                    }
+                }
+            }
+
+            bx_alert('system', 'transcode', $mixedHandler, false, array(
+                'transcoder_object' => $this->_aObject['object'],
+                'object' => $this,
+                'db' => $this->_oDb,
+                'file_path' => $sTmpFile,
+                'ret' => &$bRet,
+            ));
+
+            $mixedHandler = $this->processHandlerForRetinaDevice($mixedHandler);
+                    
+            if ($this->_sQueueStorage) {
+
+                $sFileUrlResult = '';
+                if (!($iFileIdResult = $this->storeTranscodedFileInQueueStorage($mixedHandler, $sTmpFile, $sFileUrlResult)))
+                    $this->_oDb->updateQueueStatus($mixedHandler, BX_DOL_QUEUE_FAILED, "store queue tmp file failed:\n" . $this->getLog());
+                else
+                    $this->_oDb->updateQueueStatus($mixedHandler, BX_DOL_QUEUE_COMPLETE, '', '', $iFileIdResult, $sFileUrlResult, $sExtChange);
+
+            } else {
+
+                $bRet = $this->storeTranscodedFile($mixedHandler, $sTmpFile, $iProfileId);
+                
+                if ($bRet)
+                    $this->deleteFromQueue($mixedHandler);
+                else
+                    $this->_oDb->updateQueueStatus($mixedHandler, BX_DOL_QUEUE_FAILED, "store file failed:\n" . $this->getLog());
+                    
+            }
+
+            @unlink($sTmpFile);
+
+        } while (false);
+
+        bx_alert($this->_aObject['object'], 'transcoded', $mixedHandler, false, array(
+            'object' => $this,
+            'ret' => $bRet,
+        ));
+
+        return $bRet;
+    }
+
+    /**
+     * Delete outdated files by last access time.
+     * @return number of pruned/deleted files
+     */
+    public function prune ()
+    {
+        $aFiles = $this->_oDb->getFilesForPruning();
+        if (!$aFiles)
+            return false;
+        $iCount = 0;
+        foreach ($aFiles as $r)
+            $iCount += $this->_oStorage->deleteFile($r['file_id']) ? 1 : 0;
+        return $iCount;
+    }
+
+    public function forceDevicePixelRatio ($i)
+    {
+        if (!$i)
+            $this->_iDevicePixelRatio = false;
+        elseif ($i >= 2) 
+            $this->_iDevicePixelRatio = 2;
+        else
+            $this->_iDevicePixelRatio = 1;
+    }
+
+    public function getDevicePixelRatio ()
+    {   
+        $iRet = 1;
+        if($this->_iDevicePixelRatio)
+            $iRet = $this->_iDevicePixelRatio;
+        else if(!isset($this->_aObject['source_params']['disable_retina']) || !$this->_aObject['source_params']['disable_retina'])
+            $iRet = bx_get_device_pixel_ratio();
+
+        // don't resize images on mobile if they are already big enough
+        if ($iRet > 1 && $this->isMobile() && $this->getMaxResizeDimention() >= BX_DOL_MAX_RETINA_DIMENSION_MOBILE)
+            $iRet = 1;
+
+        return $iRet;
+    }
+
+    public function getOrigFileUrl ($mixedHandler)
+    {
+        $sMethodStoreFile = 'getOrigFileUrl_' . $this->_aObject['source_type'];
+        return $this->$sMethodStoreFile($mixedHandler);
+    }
+
+    /**
+     * Get file url when file isn't transcoded yet
+     */
+    public function getFileUrlNotReady($mixedHandler)
+    {
+        return false;
+    }
+
+    public function getTmpFilename ($sOverrideName = false)
+    {
+        if ($sOverrideName)
+            return BX_DIRECTORY_PATH_TMP . rand(10000, 99999) . $sOverrideName;
+        return tempnam(BX_DIRECTORY_PATH_TMP, $this->_aObject['object']);
+    }
+
+    public function clearLog()
+    {
+        $this->_sLog = '';
+    }
+
+    public function addToLog($s)
+    {
+        $this->_sLog .= $s;
+    }
+
+    public function getLog()
+    {
+        return $this->_sLog;
+    }
+
+    public function deleteFromQueue ($mixedHandler)
+    {
+        $sErrMsg = "delete queue tmp file failed:\n";
+        $a = $this->_oDb->getFromQueue($mixedHandler);
+        if (!$a || $a['server'] != gethostname()) // deletion must be performed on the server where it was transcoded
+            return false;
+        
+        if ($a['file_id_result'] && $this->_sQueueStorage && 0 !== strncmp($a['log'], $sErrMsg, strlen($sErrMsg))) {
+            $oStorage = BxDolStorage::getObjectInstance($this->_sQueueStorage);
+            if (!$oStorage->deleteFile($a['file_id_result'])) {
+                $this->_oDb->updateQueueStatus($mixedHandler, BX_DOL_QUEUE_FAILED, $sErrMsg . $this->getLog());
+                bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: deleteFromQueue failed for handler ({$mixedHandler}): " . $this->getLog());
+                return false;
+            }
+        }
+
+        return $this->_oDb->deleteFromQueue($mixedHandler);
+    }
+
+    public function storeTranscodedFile ($mixedHandler, $sTmpFile, $iProfileId)
+    {
+        $sMethodIsPrivate = 'isPrivate_' . $this->_aObject['source_type'];
+        $isPrivate = $this->$sMethodIsPrivate($mixedHandler);
+        $iFileId = $this->_oStorage->storeFileFromPath ($sTmpFile, $isPrivate, $iProfileId);
+        if (!$iFileId) {
+            $this->addToLog($this->_oStorage->getErrorString());
+            bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: storeTranscodedFile failed for handler ({$mixedHandler}): " . $this->_oStorage->getErrorString());
+            return false;
+        }
+
+        if (!$this->_oDb->updateHandler($iFileId, $mixedHandler)) {
+            $this->_oStorage->deleteFile($iFileId);
+            return false;
+        }
+
+        $this->_oStorage->afterUploadCleanup($iFileId, $iProfileId);
+
+        return true;
+    }
+
+    protected function storeTranscodedFileInQueueStorage ($mixedHandler, $sTmpFile, &$sFileUrlResult)
+    {
+        if (!$this->_sQueueStorage)
+            return false;
+
+        $oStorage = BxDolStorage::getObjectInstance($this->_sQueueStorage);
+
+        $iFileIdResult = $oStorage->storeFileFromPath ($sTmpFile, 1, 0);
+        if (!$iFileIdResult) {
+            $this->addToLog($oStorage->getErrorString());
+            bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: storeTranscodedFileInQueueStorage failed for handler ({$mixedHandler}): " . $oStorage->getErrorString());
+            return false;
+        }
+
+        if (!($sFileUrlResult = $oStorage->getFileUrlById($iFileIdResult))) {
+            $this->_oStorage->deleteFile($iFileIdResult);
+            return false;
+        }
+
+        $oStorage->afterUploadCleanup($iFileIdResult, 0);
+
+        return $iFileIdResult;
+    }
+
+    protected function addToQueue($mixedHandler, $iProfileId = 0)
+    {
+        if ($this->_oDb->getFromQueue($mixedHandler))
+            return true;
+        $sFileUrl = $this->getOrigFileUrl($mixedHandler);
+        return $this->_oDb->addToQueue ($mixedHandler, $sFileUrl, $iProfileId);
+    }
+
+    protected function applyFilter_Resize ($sFile, $aParams)
+    {
+        $o = BxDolImageResize::getInstance();
+        $o->removeCropOptions ();
+
+        $o->setSize (isset($aParams['w']) ? round($aParams['w'] * $this->getDevicePixelRatio()) : null, isset($aParams['h']) ? round($aParams['h'] * $this->getDevicePixelRatio()) : null);
+
+        if (isset($aParams['crop_resize']) && $aParams['crop_resize'])
+            $o->setAutoCrop (true);
+        elseif (isset($aParams['square_resize']) && $aParams['square_resize'])
+            $o->setSquareResize (true);
+        else
+            $o->setSquareResize (false);
+
+        if (IMAGE_ERROR_SUCCESS == $o->resize($sFile))
+            return true;
+
+        bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: applyFilter_Resize failed for file ({$sFile}): " . $o->getError());
+
+        return false;
+    }
+
+    protected function isFileReady_Folder ($mixedHandlerOrig, $isCheckOutdated = true)
+    {
+        $mixedHandler = $this->processHandlerForRetinaDevice($mixedHandlerOrig);
+
+        $iFileId = $this->_oDb->getFileIdByHandler($mixedHandler);
+        if (!$iFileId)
+            return false;
+
+        $aFile = $this->_oStorage->getFile($iFileId);
+        if (!$aFile)
+            return false;
+
+        if ($isCheckOutdated) { // warning, $isCheckOutdated is partially supported for Folder source type - file modification is not checked
+            if ($this->_aObject['ts'] > $aFile['modified'] || !$this->getFilePath_Folder($mixedHandlerOrig)) { // if we changed transcoder object params or original file is deleted
+                // delete file, so it will be recreated next time
+                if ($this->_oStorage->deleteFile($aFile['id']))
+                    return false;
+            }
+        }
+
+        return true;
+    }
+
+    protected function isFileReady_Storage ($mixedHandlerOrig, $isCheckOutdated = true)
+    {
+        $mixedHandler = $this->processHandlerForRetinaDevice($mixedHandlerOrig);
+
+        $iFileId = $this->_oDb->getFileIdByHandler($mixedHandler);
+        if (!$iFileId)
+            return false;
+
+        $aFile = $this->_oStorage->getFile($iFileId);
+        if (!$aFile)
+            return false;
+
+        if ($isCheckOutdated) {
+            $oStorageOriginal = BxDolStorage::getObjectInstance($this->_aObject['source_params']['object']);
+            if ($oStorageOriginal) {
+                $aFileOriginal = $oStorageOriginal->getFile($mixedHandlerOrig);
+                if (!$aFileOriginal || $aFileOriginal['modified'] > $aFile['modified'] || $this->_aObject['ts'] >= $aFile['modified']) { // if original file was changed OR we changed transcoder object params
+                    // delete file, so it will be recreated next time
+                    if ($this->_oStorage->deleteFile($aFile['id']))
+                        return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    protected function isPrivate_Folder ($mixedHandler)
+    {
+        if ('no' == $this->_aObject['private'])
+            return false;
+        return true;
+    }
+
+    protected function isPrivate_Storage ($mixedHandler)
+    {
+        switch ($this->_aObject['private']) {
+            case 'no':
+                return false;
+            case 'yes':
+                return true;
+            default:
+            case 'auto':
+                $oStorageOriginal = BxDolStorage::getObjectInstance($this->_aObject['source_params']['object']);
+                if (!$oStorageOriginal)
+                    return true; // in case of error - make sure that file is not public accidentally
+                $aFile = $oStorageOriginal->getFile($mixedHandler);
+                return $oStorageOriginal->isFilePrivate($aFile['id']);
+        }
+    }
+
+    protected function getFilePath_Folder ($mixedHandler)
+    {
+        $sPath = $this->_aObject['source_params']['path'] . $mixedHandler;
+        if (!file_exists($sPath))
+            $sPath = BX_DIRECTORY_PATH_ROOT . $sPath;
+
+        if (!file_exists($sPath)) {
+            bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: getFilePath_Folder failed, file ({$sPath}) doesn't exist for handler ({$mixedHandler})");
+            return false;
+        }
+
+        return $sPath;
+    }
+
+    protected function storeFileLocally_Folder ($mixedHandler)
+    {
+        $sPath = $this->getFilePath_Folder($mixedHandler);
+        if (!$sPath) {
+            bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: storeFileLocally_Folder failed, source file ({$sPath}) doesn't exist for handler ({$mixedHandler})");
+            return false;
+        }
+
+        $sTmpFile = $this->getTmpFilename ($mixedHandler);
+        if (!copy($sPath, $sTmpFile)) {
+            bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: storeFileLocally_Folder failed, copy from file ({$sPath}) to file ({$sTmpFile}) failed");
+            return false;
+        }
+
+        return $sTmpFile;
+    }
+
+    protected function storeFileLocally_Storage ($mixedHandler)
+    {
+        $oStorageOriginal = BxDolStorage::getObjectInstance($this->_aObject['source_params']['object']);
+        if (!$oStorageOriginal) {
+            bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: storeFileLocally_Storage failed, source storage({$this->_aObject['source_params']['object']}) isn't available");
+            return false;
+        }
+
+        $aFile = $oStorageOriginal->getFile($mixedHandler);
+        if (!$aFile) {
+            bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: storeFileLocally_Storage failed, file({$mixedHandler}) wasn't found in source storage ({$this->_aObject['source_params']['object']})");
+            return false;
+        }
+
+        $sUrl = $oStorageOriginal->getFileUrlById($mixedHandler);
+        if (!$sUrl) {
+            bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: storeFileLocally_Storage failed, file({$mixedHandler}) wasn't found in source storage ({$this->_aObject['source_params']['object']})");
+            return false;
+        }
+
+        $sFileData = bx_file_get_contents ($sUrl, [], 'get', []);
+        if (false === $sFileData) {
+            bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: storeFileLocally_Storage failed, getting file from URL({$sUrl}) failed");
+            return false;
+        }
+
+        $sTmpFile = $this->getTmpFilename ($aFile['file_name']);
+        if (!file_put_contents($sTmpFile, $sFileData)) {
+            bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: storeFileLocally_Storage failed, file save failed ({$sTmpFile})");
+            return false;
+        }
+
+        return $sTmpFile;
+    }
+
+    protected function storeFileLocally_Queue ($mixedHandler)
+    {   
+        $aQueue = $this->_oDb->getFromQueue($mixedHandler);
+        if (!$aQueue || !$aQueue['file_url_source']) {
+            bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: storeFileLocally_Queue failed, file ({$mixedHandler}) wasn't found in queue");
+            return false;
+        }
+
+        $sFileData = bx_file_get_contents ($aQueue['file_url_source'], [], 'get', []);
+        if (false === $sFileData) {
+            bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: storeFileLocally_Queue failed, getting file from URL({$aQueue['file_url_source']}) failed");
+            return false;
+        }
+
+        $sTmpFile = $this->getTmpFilename ($mixedHandler);
+        if (!file_put_contents($sTmpFile, $sFileData)) {
+            bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: storeFileLocally_Queue failed, file save failed ({$sTmpFile})");
+            return false;
+        }
+
+        return $sTmpFile;
+    }
+
+    protected function getOrigFileUrl_Folder ($mixedHandler)
+    {
+        $sPath = $this->_aObject['source_params']['path'] . $mixedHandler;
+        if (!file_exists(BX_DIRECTORY_PATH_ROOT . $sPath)) {
+            bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: getOrigFileUrl_Folder failed, source file ({$sPath}) doesn't exist");
+            return false;
+        }
+        
+        return BX_DOL_URL_ROOT . $sPath;
+    }
+
+    protected function getOrigFileUrl_Storage ($mixedHandler)
+    {
+        $oStorageOriginal = BxDolStorage::getObjectInstance($this->_aObject['source_params']['object']);
+        if (!$oStorageOriginal) {
+            bx_log('sys_transcoder', "[{$this->_aObject['object']}] ERROR: getOrigFileUrl_Storage failed, source storage ({$this->_aObject['source_params']['object']}) doesn't exist");
+            return false;
+        }
+
+        return $oStorageOriginal->getFileUrlById($mixedHandler);
+    }
+
+    protected function initFilters ()
+    {
+        if (false !== $this->_aObject['filters']) // filters are already initilized
+            return;
+
+        // get transcoder filters
+        $aFilters = $this->_oDb->getTranscoderFilters();
+        $this->_aObject['filters'] = array();
+        foreach ($aFilters as $aFilter) {
+            $aFilter['filter_params'] = $aFilter['filter_params'] ? unserialize($aFilter['filter_params']) : array();
+            $this->_aObject['filters'][] = $aFilter;
+        }
+
+    }
+
+    protected function clearCacheDB()
+    {
+        $oCacheDb = $this->_oDb->getDbCacheObject();
+        return $oCacheDb->removeAllByPrefix('db_');
+    }
+
+    public function getDevicePixelRatioHandlerSuffix ()
+    {
+        $iDevicePixelRatio = $this->getDevicePixelRatio ();
+        if ($iDevicePixelRatio >= 2)
+            return $this->_sRetinaSuffix;
+        return '';
+    }
+
+    protected function processHandlerForRetinaDevice ($mixedHandler)
+    {
+        return '' . $mixedHandler . $this->getDevicePixelRatioHandlerSuffix ();
+    }
+
+    static protected function _callFuncForObjectsArray ($mixed, $sFunc)
+    {
+        if (!is_array($mixed))
+            $mixed = array($mixed);
+        foreach ($mixed as $sObject) {
+            $oTranscoder = self::getObjectInstance($sObject);
+            if ($oTranscoder)
+                $oTranscoder->$sFunc();
+        }
+    }
+
+    /**
+     * Get filter params
+     * @param $sName filter name
+     * @return array of filter params if filter was found, or false if given filter isn't enabled for the transcoder
+     */
+    public function getFilterParams ($sName)
+    {
+        $this->initFilters ();
+        foreach ($this->_aObject['filters'] as $aParams)
+            if ($sName == $aParams['filter'])
+                return $aParams['filter_params'];
+        return false;
+    }
+
+    public function getMaxResizeDimention ($sFilter = 'Resize', $sKey = '')
+    {
+        $a = $this->_oDb->getTranscoderFilters($sFilter);
+        if (!$a)
+            return 0;
+        $aFilter = array_pop($a);
+        $aFilterParams = $aFilter['filter_params'] ? unserialize($aFilter['filter_params']) : array();
+        if ($sKey)
+            return isset($aFilterParams[$sKey]) ? $aFilterParams[$sKey] : 0;
+        return $aFilterParams ? max(isset($aFilterParams['w']) ? $aFilterParams['w'] : 0, isset($aFilterParams['h']) ? $aFilterParams['h'] : 0) : 0;
+    }
+
+    public function isMobile ()
+    {
+        return bx_is_mobile();
+    }
+}
+
+/** @} */

+ 193 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/js/classes/BxDolMenuMoreAuto.js

@@ -0,0 +1,193 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function BxDolMenuMoreAuto(options)
+{
+    this._sObject = options.sObject;
+    this._iItemsStatic = undefined == options.iItemsStatic ? 0 : options.iItemsStatic;
+    this._bItemsStaticOnly = undefined == options.bItemsStaticOnly ? 0 : options.bItemsStaticOnly;
+    this._aHtmlIds = undefined == options.aHtmlIds ? {} : options.aHtmlIds;  
+
+    this._sKeyWidth = 'bx-mma-width';
+
+    this._sClassReady = 'bx-mma-ready';
+    this._sClassItem = '.bx-menu-item';
+    this._sClassItemMore = this._sClassItem + '.bx-menu-item-more-auto';
+    this._sClassItemMoreSubmenu = '.bx-menu-submenu-more-auto';
+    this._sClassItemStatic = 'bx-menu-item-static';
+
+    this._bStaticMode = this._bItemsStaticOnly && this._iItemsStatic > 0;
+    this._bTrackContainerResize = false;
+}
+
+BxDolMenuMoreAuto.prototype.init = function() {
+    var $this = this;
+
+    var oMenu = $('#' + this._aHtmlIds['main']);
+    if(!oMenu.length) 
+        return;
+
+    //--- Initialize ---//
+    var oItemMore = oMenu.find(this._sClassItemMore);
+    var oItemMoreSubmenu = oItemMore.find(this._sClassItemMoreSubmenu);
+
+    for(var i = 0; i < this._iItemsStatic; i++)
+        oItemMore.prevAll(this._sClassItem + ':not(.' + this._sClassItemStatic + '):last').addClass(this._sClassItemStatic);
+
+    var iMenu = 0;
+    oMenu.children(this._sClassItem + ':visible').each(function() {
+        iMenu += $this._getWidth($(this));
+    });
+
+    var iParent = oMenu.parent().width();
+    var iItemMore = $this._getWidth(oItemMore);
+
+    oMenu.css('overflow', 'visible');
+
+    if(iMenu >= iParent || this._bStaticMode)
+        this._moveToSubmenu(oMenu, oItemMore, oItemMoreSubmenu, iParent, iItemMore);
+    else
+        oMenu.parents('.bx-menu-more-auto-wrapper:first').css('overflow', 'visible');
+
+    //--- Add event handlers ---//
+    if(!this._bStaticMode) {
+        $(window).on('resize', function() {
+           $this.update();
+        });
+
+        if(this._bTrackContainerResize)
+            setTimeout(function () {
+                oMenu.find(this._sClassItem).on('resize', function() {
+                    $this.update(true);
+                });
+            }, 200);
+    }
+};
+
+BxDolMenuMoreAuto.prototype.update = function(bForceCalculate)
+{
+    if(this._bStaticMode)
+        return;
+
+    var $this = this;
+    var oMenu = $('#' + this._aHtmlIds['main']);
+    var oItemMore = oMenu.find(this._sClassItemMore);
+    var oItemMoreSubmenu = oItemMore.find(this._sClassItemMoreSubmenu);
+
+    var iMenu = 0;
+    oMenu.children(this._sClassItem + ':visible').each(function() {
+        iMenu += $this._getWidth($(this), $(this).not($this._sClassItemMore).length ? bForceCalculate : false);
+    });
+
+    var iParent = oMenu.parent().width();
+    var iItemMore = $this._getWidth(oItemMore, false);
+
+    if(iMenu > iParent)
+        this._moveToSubmenu(oMenu, oItemMore, oItemMoreSubmenu, iParent, iItemMore);
+    if(iMenu < iParent)
+        this._moveFromSubmenu(oMenu, oItemMore, oItemMoreSubmenu, iParent, iMenu);
+};
+
+BxDolMenuMoreAuto.prototype.more = function(oElement)
+{
+    var oElement = $(oElement);
+
+    oElement.parents('li:first').find('#' + this._aHtmlIds['more_auto_popup']).dolPopup({
+        pointer: {
+            el: oElement
+        }, 
+        moveToDocRoot: false,
+        cssClass: 'bx-popup-menu'
+    });
+}
+
+BxDolMenuMoreAuto.prototype._moveToSubmenu = function(oMenu, oItemMore, oItemMoreSubmenu, iParent, iItemMore)
+{
+    var $this = this;
+
+    var bRelocateOthers = false;
+    var iWidthTotal = iItemMore;
+    var oSubmenuItemFirst = oItemMoreSubmenu.children(this._sClassItem + ':first');
+
+    oMenu.children(this._sClassItem + ':not(' + this._sClassItemMore + ')').each(function() {
+        var oItem = $(this);
+        var iItem = $this._getWidth(oItem);
+        if(($this._bStaticMode || bRelocateOthers || iWidthTotal + iItem > iParent) && !oItem.hasClass($this._sClassItemStatic)) {
+            oItem.addClass('bx-def-color-bg-hl-hover');
+            oItem.find('a').each(function() {
+                if($(this).hasClass('bx-btn'))
+                    $(this).removeClass('bx-btn').addClass('bx-nobtn');
+
+                if($(this).hasClass('bx-btn-height'))
+                    $(this).removeClass('bx-btn-height').addClass('bx-nobtn-height');
+            });
+
+            if(!oSubmenuItemFirst.length)
+                oItemMoreSubmenu.append(oItem.detach());
+            else
+                oSubmenuItemFirst.before(oItem.detach());
+            bRelocateOthers = true;
+            return;
+        }
+
+        iWidthTotal += iItem;
+    });
+
+    if(oItemMoreSubmenu.find('li').length)
+        oItemMore.show(function() {
+            oMenu.addClass($this._sClassReady).parents('.bx-menu-more-auto-wrapper:first').css('overflow', 'visible');
+        });
+};
+
+BxDolMenuMoreAuto.prototype._moveFromSubmenu = function(oMenu, oItemMore, oItemMoreSubmenu, iParent, iMenu)
+{
+    var $this = this;
+
+    var bStopRelocation = false;
+    var iWidthTotal = iMenu;
+    oItemMoreSubmenu.children(this._sClassItem).each(function() {
+        if(bStopRelocation) 
+            return;
+
+        var oItem = $(this);
+        var iItem = $this._getWidth(oItem);
+        if(iWidthTotal + iItem > iParent) {
+            bStopRelocation = true;
+            return;
+        }
+
+        oItem.removeClass('bx-def-color-bg-hl-hover');
+        oItem.find('a').each(function() {
+            if($(this).hasClass('bx-nobtn'))
+                $(this).removeClass('bx-nobtn').addClass('bx-btn');
+
+            if($(this).hasClass('bx-nobtn-height'))
+                $(this).removeClass('bx-nobtn-height').addClass('bx-btn-height');
+        });
+
+        oItemMore.before(oItem.detach());
+        iWidthTotal += iItem;
+    });
+
+    if(!oItemMoreSubmenu.find('li').length)
+        oItemMore.hide();
+};
+
+BxDolMenuMoreAuto.prototype._getWidth = function(oItem, bForceCalculate)
+{
+    var iItem = parseInt(oItem.attr(this._sKeyWidth));
+    if(!bForceCalculate && iItem)
+        return iItem;
+
+    iItem = oItem.outerWidth(true);
+    if(iItem)
+        oItem.attr(this._sKeyWidth, iItem);
+
+    return iItem;
+}
+/** @} */

+ 1579 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/js/functions.js

@@ -0,0 +1,1579 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function processJsonData(oData) {
+	var fContinue = function(oData) {
+		if(oData && oData.reload != undefined && parseInt(oData.reload) == 1)
+	    	document.location = document.location;
+
+	    if(oData && oData.redirect != undefined && oData.redirect.length != 0)
+	    	document.location = oData.redirect;
+
+	    if(oData && oData.popup != undefined) {
+	    	var oPopup = null;
+	    	var oOptions = {
+	            fog: {
+					color: '#fff',
+					opacity: .7
+	            },
+	            closeOnOuterClick: false
+	        };
+
+	    	if(typeof(oData.popup) == 'object') {
+	    		oOptions = $.extend({}, oOptions, oData.popup.options);
+	    		oPopup = $(oData.popup.html);
+	    	}
+	    	else 
+	    		oPopup = $(oData.popup);
+
+            if ('undefined' !== typeof(bx_editor_remove_all))
+                bx_editor_remove_all($('#' + oPopup.attr('id')));
+            
+	    	$('#' + oPopup.attr('id')).remove();
+	        oPopup.hide().prependTo('body').bxProcessHtml().dolPopup(oOptions);
+	    }
+
+	    if(oData && oData.form != undefined && oData.form_id != undefined)
+	    	$('form#' + oData.form_id).replaceWith(oData.form).bxProcessHtml();
+
+	    if (oData && oData.eval != undefined)
+	        eval(oData.eval);
+	};
+
+	if(oData && oData.object != undefined && oData.grid != undefined)
+		if(glGrids[oData.object] != undefined)
+			glGrids[oData.object].processJson(oData);
+
+	if(oData && oData.message != undefined && oData.message.length != 0)
+		bx_alert(oData.message, function() {
+			fContinue(oData);
+		});
+	else if(oData && oData.msg != undefined && oData.msg.length != 0)
+		bx_alert(oData.msg, function() {
+			fContinue(oData);
+		});
+	else
+		fContinue(oData);
+};
+
+function getHtmlData( elem, url, callback, method, confirmation, values, loading)
+{
+    var fPerform = function() {
+		// in most cases it is element ID, in other cases - object of jQuery
+		if (typeof elem == 'string')
+		    elem = '#' + elem; // create selector from ID
+
+		var $block = $(elem);
+
+		var blockPos = $block.css('position');
+
+		$block.css('position', 'relative'); // set temporarily for displaying "loading icon"
+
+        if ('undefined' === typeof(loading) || loading) {
+    		bx_loading_content($block, true);
+	    	var $loadingDiv = $block.find('.bx-loading-ajax');
+
+    		var iLeftOff = parseInt(($block.innerWidth() / 2.0) - ($loadingDiv.outerWidth()  / 2.0));
+	    	var iTopOff  = parseInt(($block.innerHeight() / 2.0) - ($loadingDiv.outerHeight()));
+		    if (iTopOff<0) iTopOff = 0;
+
+    		$loadingDiv.css({
+	    	    position: 'absolute',
+		        left: iLeftOff,
+    		    top:  iTopOff
+	    	});
+        }
+
+		if (undefined != method && (method == 'post' || method == 'POST')) {
+            if (typeof values == 'undefined')
+                values = {};
+		    $.post(url, values, function(data) {
+		        $block.html(data);
+		        $block.css('position', blockPos).bxProcessHtml();
+
+		        if (typeof callback == 'function')
+		            callback.apply($block);
+		    });		
+		} 
+		else {
+		    $block.load(url + '&_r=' + Math.random(), function() {
+		        $(this).css('position', blockPos).bxProcessHtml();
+
+		        if (typeof callback == 'function')
+		            callback.apply(this);
+		    });
+		}
+    };
+
+    if (typeof(confirmation) != 'undefined' && confirmation)
+    	bx_confirm(_t('_Are_you_sure'), fPerform);
+    else 
+    	fPerform();
+}
+
+function loadDynamicBlockAutoPaginate (e, iStart, iPerPage, sAdditionalUrlParams, sStartParamName, sPerPageParamName) {
+
+    sUrl = location.href;
+    sStartParamName = typeof sStartParamName !== 'undefined' ?  sStartParamName : 'start';
+    sPerPageParamName = typeof sPerPageParamName !== 'undefined' ?  sPerPageParamName : 'per_page';
+
+    sUrl = sUrl.replace(/start=\d+/, '').replace(/per_page=\d+/, '').replace(/[&\?]+$/, '');
+    sUrl = bx_append_url_params(sUrl, sStartParamName + '=' + parseInt(iStart) + '&' + sPerPageParamName + '=' + parseInt(iPerPage));
+    if ('undefined' != typeof(sAdditionalUrlParams))
+        sUrl = bx_append_url_params(sUrl, sAdditionalUrlParams);
+
+	if ($(e).parents('.bx-search-result-block-pagination').length > 0){
+		$([document.documentElement, document.body]).animate({
+			scrollTop: $(e).parents('.bx-search-result-block-pagination').first().offset().top
+		}, 500);
+	}
+    return loadDynamicBlockAuto(e, sUrl);
+}
+
+function loadDynamicBlockAutoSort (e, sort, sAdditionalUrlParams) {
+
+    sUrl = location.href;
+    console.log(sort);
+
+    sUrl = sUrl.replace(/sort=\d+/, '').replace(/[&\?]+$/, '');
+    sUrl = bx_append_url_params(sUrl, 'sort' + '=' + sort);
+    if ('undefined' != typeof(sAdditionalUrlParams))
+        sUrl = bx_append_url_params(sUrl, sAdditionalUrlParams);
+
+    if ($(e).parents('.bx-search-result-block-pagination').length > 0){
+        $([document.documentElement, document.body]).animate({
+            scrollTop: $(e).parents('.bx-search-result-block-pagination').first().offset().top
+        }, 500);
+    }
+    return loadDynamicBlockAuto(e, sUrl);
+}
+
+/**
+ * This function reloads page block automatically, 
+ * just provide any element inside the block and this function will reload the block.
+ * @param e - element inside the block
+ * @return true on success, or false on error - particularly, if block isn't found
+ */
+function loadDynamicBlockAuto(e, sUrl) {
+    var oContainer = $(e).parents('.bx-page-block-container:first:not(.no-dynamic)');
+    var sId = oContainer.attr('id');
+
+    if ('undefined' == typeof(sUrl))
+        sUrl = location.href;
+
+    if (!sId || !sId.length)
+        return false;
+
+    var aMatches = sId.match(/\-(\d+)$/);
+    if (!aMatches || !aMatches[1])
+        return false;
+
+    var oPaginate = oContainer.find('.bx-paginate'); 
+    if(oPaginate.length > 0) {
+    	var iStart = parseInt(oPaginate.attr('bx-data-start'));
+    	var iPerPage = parseInt(oPaginate.attr('bx-data-perpage'));
+
+    	var oParams = {};
+    	if(!isNaN(iStart) && sUrl.indexOf('start') == -1)
+    		oParams.start = iStart;
+    	if(!isNaN(iPerPage) && sUrl.indexOf('per_page') == -1)
+    		oParams.per_page = iPerPage;
+
+    	sUrl = bx_append_url_params(sUrl, oParams);
+    }
+
+    loadDynamicBlock(parseInt(aMatches[1]), sUrl);
+    return true;
+}
+
+function loadDynamicBlock(iBlockID, sUrl) {
+    
+    var oCallback = null;
+    if($('#bx-page-block-' + iBlockID + ' .bx-base-unit-showcase-wrapper').length){
+        oCallback = bx_showcase_view_init;
+    }
+    getHtmlData($('#bx-page-block-' + iBlockID), bx_append_url_params(sUrl, 'dynamic=tab&pageBlock=' + iBlockID), oCallback);
+    return true;
+}
+
+function loadDynamicPopupBlock(iBlockID, sUrl) {
+    if (!$('#dynamicPopup').length) {
+        $('<div id="dynamicPopup" style="display:none;"></div>').prependTo('body');
+    }
+
+    $('#dynamicPopup').load(
+        (sUrl + '&dynamic=popup&pageBlock=' + iBlockID),
+        function() {
+            $(this).dolPopup({
+                left: 0,
+                top: 0
+            });
+        }
+    );
+}
+
+function closeDynamicPopupBlock() {
+    $('#dynamicPopup').dolPopupHide();
+}
+
+function onAsyncBlockLoad() {
+    var oBlock = $(this);
+
+    if(!oBlock.html()) 
+        oBlock.remove();
+}
+
+/**
+ * Translate string
+ */
+function _t(s, arg0, arg1, arg2) {
+    if (!window.aDolLang || !aDolLang[s])
+        return s;
+
+    cs = aDolLang[s];
+    cs = cs.replace(/\{0\}/g, arg0);
+    cs = cs.replace(/\{1\}/g, arg1);
+    cs = cs.replace(/\{2\}/g, arg2);
+    return cs;
+}
+
+function showPopupAnyHtml(sUrl, sId) {
+
+    var oPopupOptions = {};
+
+    if (!sId || !sId.length)
+        sId = 'login_div';
+
+    $('#' + sId).remove();
+    $('<div id="' + sId + '" style="display: none;"></div>').prependTo('body').load(
+        sUrl.match('^http[s]{0,1}:\/\/') ? sUrl : sUrlRoot + sUrl,
+        function() {
+            $(this).dolPopup(oPopupOptions);
+        }
+    );
+}
+
+function bx_loading_svg(sType, sClass) {
+	sClass = sClass != undefined && sClass.length > 0 ? sClass : '';
+
+	if(sUseSvgLoading != undefined && sUseSvgLoading.length > 0)
+		return sUseSvgLoading.replace(new RegExp('__type__','g'), sType).replace(new RegExp('__class__','g'), sClass);
+
+	var s = '';
+	s += '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 520 520" class="' + sClass + '" style="enable-background:new 0 0 520 520;" xml:space="preserve">';
+	s += '<g class="' + sType + '">';
+	s += '<g class="inner">';
+	s += '<path class="p1" d="M152,260C152,260,152,260,152,260c0-59.8,48.4-108.4,108.1-108.4c43.3,0,80.9,25.6,97.9,62.4V107.3c-28-18.3-61.9-28.9-98.1-28.9C159.8,78.4,78.6,159.9,78.6,260c0,59.5,28.4,112.4,73.4,145.6V260z"/>';
+	s += '<path class="p2" d="M368,114.4V260c0,59.8-48.4,108.4-108.1,108.4c-43.3,0-80.9-25.6-97.9-62.4v106.7c28,18.3,61.9,28.9,98.1,28.9c100.1,0,181.3-81.4,181.3-181.6C441.4,200.5,413,147.6,368,114.4z"/>';
+	s += '</g>';
+	s += '<g class="outer">';
+	s += '<path class="p1" d="M146.9,106.7c-8.1-15-36.9-29.9-68.9-27.3v124.2C90,164.3,114.6,130.5,146.9,106.7z"/>';
+	s += '<path class="p2" d="M373.1,413.3c8.1,15,36.7,29.9,68.9,27.3V316.4C429.8,355.7,405.4,389.5,373.1,413.3z"/>';
+	s += '</g>';
+	s += '</g>';
+	s += '</svg>';
+	return s;
+}
+
+function bx_loading_animate (e, aOptions) {
+    e = $(e);
+    if (!e.length)
+        return false;
+    if (e.find('.bx-sys-spinner').length)
+        return false;
+    return new Spinner(aOptions).spin(e.get(0));
+}
+
+function bx_loading_btn (oElement, bEnable) {
+    var oButton = $(oElement);
+    var sClassHeight = oButton.hasClass('bx-btn-small') ? 'bx-btn-small-height' : 'bx-btn-height';
+
+    if (oButton.children('div').size())
+        oButton = oButton.children('div').first();
+
+    if(!bEnable)
+    	oButton.find('.bx-loading-ajax-btn').remove();
+    else if (!oButton.find('.bx-loading-ajax-btn').length)
+    	oButton.append('<b class="bx-loading-ajax-btn ' + sClassHeight + '">' + (bUseSvgLoading ? bx_loading_svg('colored', sClassHeight) : '') + '</b>');
+
+    if(!bUseSvgLoading)
+    	bx_loading_animate(oButton.find('.bx-loading-ajax-btn'), aSpinnerSmallOpts);    
+}
+
+function bx_loading_content (oElement, bEnable, isReplace) {
+    var oBlock = $(oElement);
+    var oLoading = $('<div class="bx-loading-ajax bx-def-z-index-front">' + (bUseSvgLoading ? bx_loading_svg('colored') : '') + '</div>');
+    
+    if(!bEnable)
+    	oBlock.find(".bx-loading-ajax").remove();
+    else if(!oBlock.find('.bx-loading-ajax').length) {
+        if('undefined' != typeof(isReplace) && isReplace)
+            oBlock.html(oLoading.addClass('static'));
+        else
+            oBlock.append(oLoading);
+
+        if(!bUseSvgLoading)
+        	bx_loading_animate(oBlock.find('.bx-loading-ajax'), aSpinnerOpts);
+    } 
+}
+
+function bx_loading (elem, b) {
+
+    if (typeof elem == 'string')
+        elem = '#' + elem;
+
+    var block = $(elem);
+
+    if (block.hasClass('bx-btn')) {
+        bx_loading_btn (block, b);
+        return;
+    }
+
+    if (1 == b || true == b) {
+
+        bx_loading_content(block, b);
+
+        e = block.find(".bx-loading-ajax");
+        e.css('left', parseInt(block.width()/2.0 - e.width()/2.0));
+
+        var he = e.outerHeight();
+        var hc = block.outerHeight();
+
+        if (block.css('position') != 'relative' && block.css('position') != 'absolute') {
+            if (!block.data('css-save-position'))
+                block.data('css-save-position', block.css('position'));
+            block.css('position', 'relative');
+        }
+
+        if (hc > he) {
+            e.css('top', parseInt(hc/2.0 - he/2.0));
+        }
+
+        if (hc < he) {
+            if (!block.data('css-save-min-height'))
+                block.data('css-save-min-height', block.css('min-height'));
+            block.css('min-height', he);
+            e.css('top', 0);
+        }
+
+    } else {
+
+        if (block.data('css-save-position'))
+            block.css('position', block.data('css-save-position'));
+
+        if (block.data('css-save-min-height'))
+            block.css('min-height', block.data('css-save-min-height'));
+
+        bx_loading_content(block, b);
+
+    }
+
+}
+
+
+/**
+ * Center content with floating blocks.
+ * sSel - jQuery selector of content to be centered
+ * sBlockSel - jquery selector of blocks
+ */
+function bx_center_content (sSel, sBlockStyle, bListenOnResize) {
+
+    var sId;
+    if ($(sSel).parent().hasClass('bx-center-content-wrapper')) {
+        sId = $(sSel).parent().attr('id');
+    } else {
+        sId = 'id' + (new Date()).getTime();
+        $(sSel).wrap('<div id="'+sId+'" class="bx-center-content-wrapper"></div>');
+    }
+
+    var eCenter = $('#' + sId);
+    var iAll = $('#' + sId + ' ' + sBlockStyle).size();
+    var iWidthUnit = $('#' + sId + ' ' + sBlockStyle + ':first').outerWidth(true);
+    var iWidthContainer = eCenter.innerWidth();           
+    var iPerRow = parseInt(iWidthContainer/iWidthUnit);
+    var iLeft = (iWidthContainer - (iAll > iPerRow ? iPerRow * iWidthUnit : iAll * iWidthUnit)) / 2;
+
+    if (iWidthUnit > iWidthContainer)
+        return;
+
+    if ('undefined' != typeof(bListenOnResize) && bListenOnResize) {
+        $(window).on('resize.bx-center-content', function () {
+            bx_center_content(sSel, sBlockStyle);
+        });
+    }
+
+    eCenter.css("padding-left", iLeft);
+}
+
+/**
+ * Show pointer popup with menu from URL.
+ * @param o - menu object name
+ * @param e - element to show popup at
+ * @param options - popup options
+ * @param vars - additional GET variables
+ */
+function bx_menu_popup (o, e, options, vars) {
+    var options = options || {};
+    var vars = vars || {};
+    var o = $.extend({}, $.fn.dolPopupDefaultOptions, {id: o, url: bx_append_url_params('menu.php', $.extend({o:o}, vars)), cssClass: 'bx-popup-menu'}, options);
+    if ('undefined' == typeof(e))
+        e = window;
+    $(e).dolPopupAjax(o);
+}
+
+/**
+ * Show pointer popup with menu from existing HTML.
+ * @param jSel - jQuery selector for html to show in popup
+ * @param e - element to show popup at
+ * @param options - popup options
+ */
+function bx_menu_popup_inline (jSel, e, options) {
+    if ($(jSel + ':visible').length) 
+        $(jSel).dolPopupHide(); 
+    else {
+        var options = options || {};
+        var o = $.extend({}, $.fn.dolPopupDefaultOptions, options, {
+            pointer:{el:$(e)}, 
+            cssClass: 'bx-popup-menu',
+            onShow: function(oPopup) {
+                oPopup.find('a').each(function () {
+                    $(this).off('click.bx-popup-menu');
+                    $(this).on('click.bx-popup-menu', function() {
+                        $(jSel).dolPopupHide();
+                    });
+                });
+            }
+        });
+
+        $(jSel).dolPopup(o);
+    }
+}
+
+/**
+ * Show pointer popup with menu from URL.
+ * @param sObject - menu object name
+ * @param oElement - element to show popup at
+ * @param oOptions - popup options
+ * @param oVars - additional GET variables
+ */
+function bx_menu_slide (sObject, oElement, sPosition, oOptions, oVars) {
+	var oVars = oVars || {};
+    var oOptions = oOptions || {};
+    oOptions = $.extend({}, {parent: 'body', container: '.bx-sliding-menu-content'}, oOptions);
+
+    var sId = '';
+    var sIdSel = '';
+    var sIdPfx = 'bx-sliding-menu-wrapper-';
+    if(typeof(oOptions.id) != 'undefined')
+    	switch(typeof(oOptions.id)) {
+    		case 'string':
+    			sId = sIdPfx + oOptions.id;
+    			break;
+
+    		case 'object':
+    			sId = typeof(oOptions.id.force) != 'undefined' && oOptions.id.force ? oOptions.id.value : sIdPfx + oOptions.id.value;
+    			break;
+    	}
+    else
+    	sId = sIdPfx + parseInt(2147483647 * Math.random());
+
+    sIdSel = '#' + sId;
+    
+    //--- If slider exists
+    if ($(sIdSel).length) {
+    	bx_menu_slide_inline (sIdSel, oElement, sPosition);
+    	return;
+    }
+    //--- If slider doesn't exists
+    else {
+    	var oMenuLoading = $('#bx-sliding-menu-loading');
+        if(!oMenuLoading || !oMenuLoading.length)
+            return;
+
+    	$('<div id="' + sId + '" style="display:none;">' + oMenuLoading.html() + '</div>').addClass(oMenuLoading.attr('class')).appendTo(oOptions.parent);
+
+        var oLoading = $(sIdSel + ' .bx-sliding-menu-loading');
+        bx_loading_content(oLoading, true, true);
+
+        bx_menu_slide_inline (sIdSel, oElement, sPosition);
+        
+        var fOnLoad = function() {
+        	bx_loading_content(oLoading, false);
+
+        	$(sIdSel).bxProcessHtml().show();
+        };
+
+        $(sIdSel).find(oOptions.container).load(bx_append_url_params('menu.php', $.extend({o:sObject}, oVars)), function () {
+            var f = function () {
+            	if($(sIdSel).find('img').length > 0 && !$(sIdSel).find('img').get(0).complete)
+            		$(sIdSel).find('img').load(fOnLoad);
+            	else
+            		fOnLoad();
+            };
+            setTimeout(f, 100); // TODO: better way is to check if item is animating before positioning it in the code where popup is positioning
+        });
+    }
+    	
+}
+
+/**
+ * Show sliding menu from existing HTML.
+ * @param sMenu - jQuery object or selector for html to show in popup
+ * @param e - element to click to open/close slider
+ * @param sPosition - 'block' for sliding menu in blocks, 'site' - for sliding main menu
+ */
+function bx_menu_slide_inline (sMenu, e, sPosition) {
+    var options = options || {};
+    var eSlider = $(sMenu);
+
+    if ('undefined' !== typeof (bx_menu_slide_inline_custom)) {
+        return bx_menu_slide_inline_custom(sMenu, e, sPosition);
+    }
+
+    if ('undefined' == typeof(e))
+        e = eSlider.data('data-control-btn');
+
+    var eIcon = $(e).find('.sys-icon-a');    
+
+    var fPositionElement = function (oElement) {
+        eSlider.css({
+            position: 'absolute',
+            top: oElement.outerHeight(true),
+            left: 0
+        });
+    };
+
+    var fPositionBlock = function () {
+        var eBlock = eSlider.parents('.bx-page-block-container');
+        eSlider.css({
+            position: 'absolute',
+            top: eBlock.find('.bx-db-header').outerHeight(true),
+            left: 0
+            //width: eBlock.width() TODO: Commited to test. Remove if everything is OK.
+        });
+    };
+
+    var fPositionSite = function () {
+        var eToolbar = $('#bx-toolbar');
+        eSlider.css({
+            position: 'fixed',
+            top: eToolbar.outerHeight(true) + (typeof iToolbarSubmenuTopOffset != 'undefined' ? parseInt(iToolbarSubmenuTopOffset) : 0),
+            left: 0
+        });
+    };
+
+    var fClose = function () {
+        if (eIcon.length)
+            (new Marka(eIcon[0])).set(eIcon.attr('data-icon-orig'));
+        eSlider.slideUp()
+    };
+
+    var fOpen = function () {
+        if (eIcon.length) {
+            eIcon.attr('data-icon-orig', eIcon.attr('data-icon'));
+            (new Marka(eIcon[0])).set('times');
+        }
+
+        if(typeof sPosition == 'object')
+            fPositionElement(sPosition);
+        else if(typeof sPosition == 'string' && sPosition == 'block')
+            fPositionBlock();
+        else
+            fPositionSite();
+
+        eSlider.slideDown();
+
+        eSlider.data('data-control-btn', e);        
+    };    
+    
+    if ('undefined' == typeof(sPosition))
+        sPosition = 'block';
+
+    if (eSlider.is(':visible')) {
+        fClose();
+
+        $(document).off('click.bx-sliding-menu touchend.bx-sliding-menu');
+        $(window).off('resize.bx-sliding-menu');
+    }
+    else {
+        bx_menu_slide_close_all_opened();
+
+        $(document).off('click.bx-sliding-menu touchend.bx-sliding-menu');
+        $(window).off('resize.bx-sliding-menu');
+
+        fOpen();
+        eSlider.find('a').each(function () {
+            $(this).off('click.bx-sliding-menu');
+            $(this).on('click.bx-sliding-menu', function () {
+                fClose();
+            });
+        });
+
+        setTimeout(function () {
+
+            var iWidthPrev = $(window).width();
+            $(window).on('resize.bx-sliding-menu', function () {
+                if ($(this).width() == iWidthPrev)
+                    return;
+                iWidthPrev = $(this).width();
+                bx_menu_slide_close_all_opened();
+            });
+ 
+            $(document).on('click.bx-sliding-menu touchend.bx-sliding-menu', function (event) {
+                if ($(event.target).parents('.bx-sliding-menu, .bx-sliding-menu-main, .bx-popup-slide-wrapper, .bx-db-header').length || $(event.target).filter('.bx-sliding-menu-main, .bx-popup-slide-wrapper, .bx-db-header').length || e === event.target)
+                    event.stopPropagation();
+                else
+                    bx_menu_slide_close_all_opened();
+            });
+
+        }, 10);
+    }
+}
+
+/**
+ * Close all opened sliding menus
+ */
+function bx_menu_slide_close_all_opened () {
+    $('.bx-sliding-menu:visible, .bx-sliding-menu-main:visible, .bx-popup-slide-wrapper:visible').each(function () {
+        bx_menu_slide_inline('#' + this.id);
+    });
+}
+
+/*
+ * Note. oData.count_old and oData.count_new are also available and can be checked or used in notification popup.  
+ */
+function bx_menu_show_live_update(oData) {
+    var sSelectorAddon = '.bx-menu-item-addon';
+
+    //--- Update Child Menu Item
+    if(oData.mi_child) {
+        var oMenuItem = $('.bx-menu-object-' + oData.mi_child.menu_object + ' .bx-menu-item-' + oData.mi_child.menu_item);
+        var oMenuItemAddon = oMenuItem.find(sSelectorAddon);
+
+        if(oMenuItemAddon.length > 0)
+            oMenuItemAddon.html(oData.count_new);
+        else
+            oMenuItem.append(oData.code.replace('{count}', oData.count_new));
+
+        //+++ Check for 0 value
+        oMenuItemAddon = oMenuItem.find(sSelectorAddon);
+        if(parseInt(oData.count_new) > 0)
+            oMenuItemAddon.show();
+        else
+            oMenuItemAddon.hide();
+    }
+
+    //--- Update Parent Menu Item
+    if(oData.mi_parent) {
+        var oMenuItem = $('.bx-menu-object-' + oData.mi_parent.menu_object + ' .bx-menu-item-' + oData.mi_parent.menu_item);
+        var oMenuItemAddon = oMenuItem.find(sSelectorAddon);
+
+        var sMediaType = bx_check_mq();
+        if(sMediaType == 'mobile')
+            sMediaType = 'phone';
+
+        var iSum = 0;
+        $('.bx-media-' + sMediaType + ' .bx-menu-object-' + oData.mi_child.menu_object + ' .bx-menu-item:not(.bx-def-media-' + sMediaType + '-hide) .bx-menu-item-addon').each(function() {
+            iValue = parseInt($(this).html());
+            if(iValue && iValue > 0)
+                iSum += iValue;
+        });
+
+        if(oMenuItemAddon.length > 0)
+            oMenuItemAddon.html(iSum);
+        else
+            oMenuItem.append(oData.code.replace('{count}', iSum));
+
+        //+++ Check for 0 value
+        oMenuItemAddon = oMenuItem.find(sSelectorAddon);
+        if(iSum > 0)
+            oMenuItemAddon.show();
+        else
+            oMenuItemAddon.hide();
+    }
+}
+
+function bx_menu_show_more_less(oLink, sMenu, sSelectorParent) {
+    if(!sSelectorParent)
+        sSelectorParent = 'ul';
+
+    var sClass = 'bx-mi-hidden';
+    $(oLink).parents(sSelectorParent + ':first').find('.bx-mi-aux, .bx-psmi-show-more, .bx-psmi-show-less').toggleClass(sClass);
+
+    $.get(sUrlRoot + 'menu.php', {
+        o: sMenu, 
+        a: 'set_collapsed', 
+        v: $(oLink).parents(sSelectorParent + ':first').find('.bx-mi-aux').hasClass(sClass) ? 1 : 0
+    });
+}
+
+function bx_menu_toggle(oLink, sMenu, sMenuItem, sSelectorParent) {
+    if(!sSelectorParent)
+        sSelectorParent = 'li';
+
+    var sClass = 'bx-mi-collapsed';
+    var oParent = $(oLink).parents(sSelectorParent + ':first');
+
+    oParent.toggleClass(sClass);
+
+    $.get(sUrlRoot + 'menu.php', {
+        o: sMenu, 
+        a: 'set_collapsed_submenu',
+        i: sMenuItem,
+        v: oParent.hasClass(sClass) ? 1 : 0
+    });
+}
+
+function bx_menu_followings_load_more(oLink, sMenu, sContextModule, iStart, iPerPage) {
+    var oLoadingParent = $(oLink).length ? $(oLink).parents('.bx-menu-item:first') : $('body'); 
+
+    bx_loading(oLoadingParent, true);
+
+    $.get(sUrlRoot + 'menu.php', {
+        o: sMenu, 
+        a: 'load_more', 
+        v: sContextModule,
+        start: iStart,
+        per_page: iPerPage
+    }, function(oData) {
+        bx_loading(oLoadingParent, false);
+
+        if(oData && oData.content != undefined)
+            $(oLink).parents('.bx-menu-subitem:first').replaceWith(oData.content);
+    }, 'json');
+}
+
+/**
+ * Set ACL level for specified profile
+ * @param iProfileId - profile id to set acl level for
+ * @param iAclLevel - acl level id to assign to a given rofile
+ */
+function bx_set_acl_level (iProfileId, iAclLevel, mixedLoadingElement) {
+    var bBulk = !$.isNumeric(iProfileId);
+    var iAclCard = !bBulk && $('#sys-acl-profile-' + iProfileId).length > 0 ? 1 : 0;
+
+    var bLoading = typeof(mixedLoadingElement) != 'undefined';
+    if(bLoading)
+        bx_loading($(mixedLoadingElement), true);
+
+    $.post(sUrlRoot + 'menu.php', {o:'sys_set_acl_level', profile_id: iProfileId, level_id: iAclLevel, card:iAclCard}, function(oData) {
+        bx_on_set_acl_level(oData, mixedLoadingElement);
+    }, 'json');
+}
+
+function bx_on_set_acl_level(oData, oLoadingElement) {
+    if(typeof(oLoadingElement) != 'undefined')
+        bx_loading($(oLoadingElement), false);
+
+    if(oData.msg != undefined && oData.msg.length) {
+        bx_alert(oData.msg);
+        return;
+    }
+
+    if($(oLoadingElement).hasClass('bx-popup-applied'))
+        $(oLoadingElement).dolPopupHide().remove();
+
+    if(typeof(oData.card) == 'object')
+        for(var iField in oData.card) {
+            var oCard = $(oData.card[iField]);
+            $('#' + oCard.attr('id')).replaceWith(oCard.bxTime());
+        }
+}
+
+/**
+ * Set badge for specified content
+ * @param sModule - current module for content
+ * @param iContentId - content id to set badge for
+ * @param iBadgeId - badge id to assign to a given content
+ */
+function bx_set_badge (sModule, iContentId, iBadgeId, mixedLoadingElement) {
+   
+    var bLoading = typeof(mixedLoadingElement) != 'undefined';
+    if(bLoading)
+        bx_loading($(mixedLoadingElement), true);
+    $.post(sUrlRoot + 'menu.php', {o:'sys_set_badges', module: sModule, content_id: iContentId, badge_id: iBadgeId}, function(oData) {
+        if(bLoading)
+            bx_loading($(mixedLoadingElement), false);
+
+        if(oData.msg != undefined && oData.msg.length) {
+            bx_alert(oData.msg);
+            return;
+        }
+
+        if($(mixedLoadingElement).hasClass('bx-popup-applied'))
+            $(mixedLoadingElement).dolPopupHide().remove();
+
+        if(typeof(oData.card) == 'object')
+            for(var iField in oData.card) {
+                var oCard = $(oData.card[iField]);
+                $('#' + oCard.attr('id')).replaceWith(oCard);
+            }
+		
+		$('.bx-base-bages-container').html(oData.html);
+		
+    }, 'json');
+}
+
+function bx_get_notes(oSource,  sModule, iContentId, oOptions, oVars) {
+    var oOptions = oOptions || {};
+    var oVars = oVars || {};
+
+    $.post(
+        sUrlRoot + 'modules/?r=' + sModule + '/get_notes/',
+        $.extend({content_id: iContentId}, oVars),
+        function(oData) {
+            if(oData && oData.popup != undefined) {
+                if(typeof(oData.popup) == 'string')
+                    oData.popup = {html: oData.popup, options: {}};
+
+                oData.popup.options = $.extend({}, $.fn.dolPopupDefaultOptions, oData.popup.options, {
+                    id: sModule + '_notes_' + iContentId, 
+                    closeOnOuterClick: false,
+                    removeOnClose: true,
+                    onBeforeShow: function(oPopup) {
+                        $(oPopup).find('.bx-popup-element-close').removeClass('bx-def-media-desktop-hide bx-def-media-tablet-hide');
+                    }
+                }, oOptions);
+            }
+            
+            processJsonData(oData);
+        },
+        'json'
+    );
+}
+
+function bx_approve(oSource,  sModule, iContentId, oOptions, oVars) {
+    var oOptions = oOptions || {};
+    var oVars = oVars || {};
+
+    var fPopupInit = function(oData) {
+        if(oData && oData.popup != undefined) {
+            if(typeof(oData.popup) == 'string')
+                oData.popup = {html: oData.popup, options: {}};
+
+            oData.popup.options = $.extend({}, $.fn.dolPopupDefaultOptions, oData.popup.options, {
+                id: sModule + '_approve_' + iContentId, 
+                closeOnOuterClick: false,
+                removeOnClose: true,
+                onBeforeShow: function(oPopup) {
+                    $(oPopup).find('.bx-popup-element-close').removeClass('bx-def-media-desktop-hide bx-def-media-tablet-hide');
+
+                    var oForm = $(oPopup).find('form');
+                    if(oForm.length > 0) {
+                        oForm.ajaxForm({ 
+                            dataType: 'json',
+                            beforeSubmit: function(aFormData, oForm, oOptions) {
+                                bx_loading(oForm, true);
+                            },
+                            success: function(oData) {
+                                $('.bx-popup-applied:visible').dolPopupHide();
+
+                                fPopupInit(oData);
+
+                                processJsonData(oData);
+                            }
+                        });
+                    }
+                }
+            }, oOptions);
+        }
+    };
+
+    $.post(
+        sUrlRoot + 'modules/?r=' + sModule + '/approve/',
+        $.extend({content_id: iContentId}, oVars),
+        function(oData) {
+            fPopupInit(oData);
+
+            processJsonData(oData);
+        },
+        'json'
+    );
+}
+
+function validateLoginForm(eForm) {
+    return true;
+}
+
+/**
+ * convert <time> tags to human readable format
+ * @param sLang - use sLang localization
+ * @param isAutoupdate - for internal usage only
+ */
+function bx_time(sLang, isAutoupdate, sRootSel) {
+    if ('undefined' == typeof(sRootSel))
+        sRootSel = 'body';
+    var iAutoupdate = 22*60*60; // autoupdate time in realtime if less than 22 hours
+    var sSel = 'time';
+
+    if (typeof(sLang) != 'undefined')
+        glBxTimeLang = sLang;
+    else if(typeof(glBxTimeLang) != 'undefined')
+        sLang = glBxTimeLang;
+
+    if(!sLang)
+        return;
+
+    if ('undefined' != typeof(isAutoupdate) && isAutoupdate)
+        sSel += '.bx-time-autoupdate';
+
+    $(sRootSel).find(sSel).each(function () {
+        var s;
+        var sTime = $(this).attr('datetime');
+        var iSecondsDiff = moment(sTime).unix() - moment().unix();
+        if (iSecondsDiff < 0)
+            iSecondsDiff = -iSecondsDiff;
+
+        if (iSecondsDiff < $(this).attr('data-bx-autoformat'))
+            s = moment(sTime).locale(sLang).fromNow(); // 'time ago' format
+        else if (1 == $(this).attr('data-bx-utc'))
+            s = moment.utc(sTime).locale(sLang).format($(this).attr('data-bx-format')); 
+        else
+            s = moment(sTime).locale(sLang).format($(this).attr('data-bx-format'));
+
+        if (iSecondsDiff < iAutoupdate)
+            $(this).addClass('bx-time-autoupdate');
+        else
+            $(this).removeClass('bx-time-autoupdate');
+
+        $(this).html(s);
+    });
+
+    if ($('time.bx-time-autoupdate').size()) {
+        setTimeout(function () {
+            bx_time(sLang, true);
+        }, 20000);
+    }
+}
+
+(function($) {
+
+    $.fn.bxTime = function() {
+        bx_time(undefined, undefined, this);
+        return this;
+    };
+
+    /**
+     * process HTML which was added dynamicly on the page
+     */ 
+    $.fn.bxProcessHtml = function (oCallback) {
+        var eElement = $(this);
+        if ('undefined' !== typeof(eElement)) {
+            // process js time
+            eElement.bxTime();
+            bx_process_links();
+            
+            // process web forms
+            if ($.isFunction($.fn.addWebForms))
+	            eElement.addWebForms();
+            
+            // process animating icons
+        	bx_activate_anim_icons();
+
+            // process syntax hightlighing
+            if ('undefined' !== typeof(Prism) && eElement.size())
+                Prism.highlightAllUnder(eElement[0]);
+
+            // process links
+            bx_redirect_for_external_links(eElement);
+
+            // flickity update
+            setTimeout(function () {
+                const oFlickity = eElement.find('.flickity-enabled');
+                if (oFlickity.length)
+                    oFlickity.flickity('resize');
+            }, 250);
+        }
+        if ('undefined' !== typeof(oCallback)) {
+            oCallback(eElement);
+        }
+
+        if (typeof glOnProcessHtml !== 'undefined' && glOnProcessHtml instanceof Array) {
+            for (var i = 0; i < glOnProcessHtml.length; i++)
+                if (typeof glOnProcessHtml[i] === "function")
+                    glOnProcessHtml[i](this);
+        }
+        
+        return this;
+    };
+
+    $.fn.bxCheckOverflowHeight = function(sClass, onFind) {
+    	if(!sClass)
+            sClass = 'bx-overflow';
+
+        return this.each(function() {
+            var oElement = $(this);
+            if(oElement.hasClass(sClass) || oElement.css('overflow') != 'hidden')
+            	return;
+
+            if(oElement.find('img').length > 0) {
+                var oImg = oElement.find('img:first').get(0);
+
+                const oImgCopy = new Image();
+                oImgCopy.src = oImg.src;
+                oImgCopy.onload = function() {
+                    var iHeightParent = oElement.height();
+                    var iHeightImg = oImg.clientHeight;
+                    var iOffsetTopImg = oImg.offsetTop;
+                    if(iOffsetTopImg < iHeightParent && iOffsetTopImg + iHeightImg > iHeightParent)
+                        oElement.css('max-height', (iOffsetTopImg + iHeightImg +  50) + 'px');
+
+                    if(oElement.prop('scrollHeight') <= Math.ceil(oElement.height()))
+                        return;
+
+                    oElement.addClass(sClass);
+                    if(typeof onFind === 'function')
+                        onFind(oElement);
+                }
+            }
+            else {
+                if(oElement.prop('scrollHeight') <= Math.ceil(oElement.height()))
+                    return;
+
+                oElement.addClass(sClass);
+                if(typeof onFind === 'function')
+                    onFind(oElement);
+            }
+        });
+    };
+} (jQuery));
+
+
+/**
+ * Perform connections AJAX request. 
+ * In case of error - it shows js alert with error message.
+ * In case of success - the page is reloaded.
+ * 
+ * @param sObj - connections object
+ * @param sAction - 'add', 'remove' or 'reject'
+ * @param iContentId - content id, initiator is always current logged in user
+ * @param bConfirm - show confirmation dialog
+ */
+function bx_conn_action(e, sObj, sAction, iContentId, bConfirm, fOnComplete) {
+    var fPerform = function() {
+		var aParams = {
+		    obj: sObj,
+		    act: sAction,
+		    id: iContentId
+		};
+		var fCallback = function (data) {
+		    bx_loading_btn(e, 0);
+		    if ('object' != typeof(data))
+		        return;
+		    if (data.err) {
+		        bx_alert(data.msg);
+		    } else {
+		        if ('function' == typeof(fOnComplete))
+		            fOnComplete(data, e);
+		        else if (!loadDynamicBlockAuto(e))
+		            location.reload();
+		        else
+		            $('#bx-popup-ajax-wrapper-bx_persons_view_actions_more').remove();
+		    }
+		};
+		
+		bx_loading_btn(e, 1);
+		
+		$.ajax({
+		    dataType: 'json',
+		    url: sUrlRoot + 'conn.php',
+		    data: aParams,
+		    type: 'POST',
+		    success: fCallback
+		});
+    };
+
+    if (typeof(bConfirm) != 'undefined' && bConfirm)
+    	bx_confirm(_t('_Are_you_sure'), fPerform);
+    else
+    	fPerform();
+}
+
+/**
+ * Perform recommendations AJAX request. 
+ * In case of error - it shows js alert with error message.
+ * In case of success - the page is reloaded.
+ * 
+ * @param sObj - recommendation object
+ * @param sAction - 'ignore'
+ * @param iContentId - content id, initiator is always current logged in user
+ * @param bConfirm - show confirmation dialog
+ */
+function bx_recommendation_action(e, sObj, sAction, iContentId, bConfirm, fOnComplete) {
+    var fPerform = function() {
+        var aParams = {
+            obj: sObj,
+            act: sAction,
+            id: iContentId
+        };
+
+        var fCallback = function (data) {
+            bx_loading_btn(e, 0);
+            if ('object' != typeof(data))
+                return;
+
+            if (data.msg)
+                bx_alert(data.msg);
+            else {
+                if ('function' == typeof(fOnComplete))
+                    fOnComplete(data, e);
+                else if (!loadDynamicBlockAuto(e))
+                    location.reload();
+            }
+        };
+
+        bx_loading_btn(e, 1);
+
+        $.ajax({
+            dataType: 'json',
+            url: sUrlRoot + 'recommendation.php',
+            data: aParams,
+            type: 'POST',
+            success: fCallback
+        });
+    };
+
+    if (typeof(bConfirm) != 'undefined' && bConfirm)
+    	bx_confirm(_t('_Are_you_sure'), fPerform);
+    else
+    	fPerform();
+}
+
+function bx_append_url_params (sUrl, mixedParams) {
+    var sParams = sUrl.indexOf('?') == -1 ? '?' : '&';
+
+    var sHash = '';
+    if(sUrl.indexOf('#') != -1) {
+        var oUrl = new URL(sUrl);
+        if(oUrl.hash.length > 0) {
+            sHash = oUrl.hash;
+            sUrl = sUrl.replace(sHash, '');
+        }
+        else
+            sUrl = sUrl.replace('#', '');
+    }
+
+    if(mixedParams instanceof Array) {
+    	for(var i in mixedParams)
+            sParams += i + '=' + mixedParams[i] + '&';
+        sParams = sParams.substr(0, sParams.length-1);
+    }
+    else if(mixedParams instanceof Object) {
+    	$.each(mixedParams, function(sKey, sValue) {
+    		sParams += sKey + '=' + sValue + '&';
+    	});
+        sParams = sParams.substr(0, sParams.length-1);
+    }
+    else
+        sParams += mixedParams;
+
+    return sUrl + sParams + sHash;
+}
+
+function bx_search_extnded_sort(obj, url){
+    location.href = url + '&sort=' +$(obj).val();
+}
+
+function bx_search_on_type (e, n, sFormSel, sResultsContSel, sLoadingContSel, bSortResults, iMinLen, onComplete) {
+    var oForm = $(e.target).parents(sFormSel + ':first');
+
+    if ('undefined' != typeof(e) && 13 == e.keyCode) {
+        oForm.find('input[name=live_search]').val(0);
+        oForm.submit();
+        return false;
+    }
+
+    iMinLen = typeof(iMinLen) != 'undefined' ? parseInt(iMinLen) : 0;
+    if(iMinLen > 0 && iMinLen > (oForm.find('input[name=keyword]').val().length + 1))
+        return true;
+
+    if ('undefined' != typeof(glBxSearchTimeoutHandler) && glBxSearchTimeoutHandler)
+        clearTimeout(glBxSearchTimeoutHandler);
+
+    glBxSearchTimeoutHandler = setTimeout(function () {
+        bx_search (n, sFormSel, sResultsContSel, sLoadingContSel, bSortResults, onComplete);
+    }, 500);
+
+    return true;
+}
+
+function bx_search (n, sFormSel, sResultsContSel, sLoadingContSel, bSortResults, onComplete) {
+    if ('undefined' == typeof(sLoadingContSel))
+        sLoadingContSel = sResultsContSel;
+    if ('undefined' == typeof(bSortResults))
+        bSortResults = false;
+
+    var sQuery = $('input', sFormSel).serialize();
+
+    bx_loading($(sLoadingContSel), true);
+    $.post(sUrlRoot + 'searchKeywordContent.php', sQuery, function(data) {
+        bx_loading($(sLoadingContSel), false);
+
+        if(!data)
+            return;
+
+        if (bSortResults) {
+            var aSortedUnits = $(data).find(".bx-def-unit-live-search").toArray().sort(function (a, b) {
+                return b.getAttribute('data-ts') - a.getAttribute('data-ts');
+            });
+            data = '';
+            $.each(aSortedUnits.slice(0, n), function (i, e) {
+                data += e.outerHTML;
+            });
+        } 
+
+        var oContainer = $(sResultsContSel);
+        oContainer.html(data).bxProcessHtml();
+
+        if(typeof onComplete === 'function')
+            onComplete(oContainer, data);
+    });
+
+    return false;
+}
+
+function on_filter_apply(e, sInputId, sFilterName)
+{
+	var oRegExp = new RegExp("[&]{0,1}" + sFilterName + "=.*");
+    var s = ('' + document.location).replace(oRegExp, ''); // remove filter
+    s = s.replace(/page=\d+/, 'page=1'); // goto 1st page
+    if (e.checked && $('#' + sInputId).val().length > 2)
+        s += (-1 == s.indexOf('?') ? '?' : '&') + sFilterName + '=' + $('#' + sInputId).val(); // append filter
+    document.location = s;
+}
+
+function on_filter_key_up (e, sCheckboxId)
+{
+    if (13 == e.keyCode) {
+        $('#' + sCheckboxId).click();
+        return false;
+    }
+    else {
+        $('#' + sCheckboxId).removeAttr('checked');
+        return true;
+    }
+}
+
+function on_copyright_click()
+{
+	oDate = new Date();
+
+	$(document).dolPopupAlert({
+		message: _t('_copyright', oDate.getFullYear())
+	});
+}
+
+function bx_activate_anim_icons(sColor)
+{
+    if(typeof(sColor) != 'undefined')
+        glBxAnimIconColor = sColor;
+    else if(typeof(glBxAnimIconColor) != 'undefined')
+        sColor = glBxAnimIconColor;
+
+    if(!sColor)
+        return;
+
+    $('.sys-icon-a').not('.marka').each(function () {
+        var e = $(this);
+        var m = new Marka(e.get(0)), r = e.attr('data-rotate'), c = e.attr('data-color');
+        m.set(e.attr('data-icon')).rotate(r ? r : 'down').color(c ? c : sColor);
+    });
+}
+
+function bx_get_param(s) {
+    if (!window.aDolOptions || !aDolOptions[s])
+        return false;
+
+    return aDolOptions[s];
+}
+
+function bx_autocomplete_fields(iId, sUrl, sName, bShowImg, bOnlyOnce, onSelect){
+	if (!$('#' + iId).hasClass('bx-form-input-autotoken'))
+			$('#' + iId).addClass('bx-form-input-autotoken');
+	
+	$('#' + iId + ' input[type=text]').autocomplete({
+		source: sUrl,
+        focus: function(e, ui ) {
+            $(this).val(ui.item.label); 
+            e.preventDefault();
+        },
+		select: function(e, ui) {
+			$(this).val(ui.item.label);
+			$(this).trigger('superselect', ui.item);
+					
+			if (typeof onSelect === 'function')
+				onSelect(ui);
+			
+			e.preventDefault();			
+		}
+	});
+	if ($('#' + iId + ' .val').length > 0 && bOnlyOnce)
+		$('#' + iId + ' input[type=text]').hide();
+	$('#' + iId + ' input[type=text]').on('superselect', function(e, item) {
+		$(this).hide();
+		if ('undefined' != typeof(item)) {
+			var sImage = '';
+			if(bShowImg && item.unit != undefined)
+				sImage = item.unit;
+			else if(bShowImg && item.thumb != undefined)
+				sImage = '<img class="bx-def-thumb bx-def-icon-size bx-def-margin-sec-right" src="' + item.thumb + '">';
+
+			$(this).before('<b class="val bx-def-color-bg-hl bx-def-round-corners">' + sImage + item.label + '<input type="hidden" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" name="' + sName + (bOnlyOnce ? '' : '[]') + '" value="' + item.value + '" /></b>');
+		}
+
+		if (!bOnlyOnce)
+		{
+			fAutoShrinkInput();
+			$(this).show();
+			this.value = '';
+		}
+
+	}).on('keydown', function(e) {
+
+		// if: comma,enter (delimit more keyCodes with | pipe)
+		if (/(13)/.test(e.which))
+			e.preventDefault();
+
+	});
+
+	$('#' + iId).on('click', 'b', function() {
+		$(this).remove();
+			
+		if (bOnlyOnce)
+			$('#' + iId + ' input[type=text]').val('').show();
+		else
+			fAutoShrinkInput();
+	});
+
+	fAutoShrinkInput = function () {
+		var iWidthCont = $('#' + iId + '.bx-form-input-autotoken').innerWidth();
+		var iWidthExisting = 0;
+		$('#' + iId + '.bx-form-input-autotoken b').each(function () {
+			iWidthExisting += $(this).outerWidth(true);
+		});
+
+		$('#' + iId + '.bx-form-input-autotoken input').innerWidth(parseInt(iWidthCont - iWidthExisting > 180 ? iWidthCont - iWidthExisting : 180) - 5);
+	};
+
+	if (!bOnlyOnce)
+		fAutoShrinkInput();
+	else
+		$('#' + iId + '.bx-form-input-autotoken input').outerWidth('100%');
+};
+
+function bx_alert(sMessage, fOnClickOk, oParams)
+{
+    $(document).dolPopupAlert({
+        message: sMessage,
+        onClickOk: fOnClickOk,
+        params: oParams
+    });
+}
+
+function bx_confirm(sMessage, fOnClickYes, fOnClickNo, oParams)
+{
+    $(document).dolPopupConfirm({
+        message: sMessage,
+        onClickYes: fOnClickYes,
+        onClickNo: fOnClickNo,
+        params: oParams
+    });
+}
+
+function bx_prompt(sMessage, sValue, fOnClickOk, fOnClickCancel, oParams)
+{
+    $(document).dolPopupPrompt({
+        message: sMessage,
+        value: sValue,
+        onClickOk: fOnClickOk,
+        onClickCancel: fOnClickCancel,
+        params: oParams
+    });
+}
+
+/**
+ * Check if mobile or desktop device.
+ */
+function bx_check_mq()
+{
+    return window.getComputedStyle(document.querySelector('body'), '::before').getPropertyValue('content').replace(/'/g, "").replace(/"/g, "");
+}
+
+/**
+ * This function loads the passed list of CSS files. In case when this function is called for dynamic loading 
+ * of the same file but many times it allows to avoid multiply loadings of the same file.
+ */
+function bx_get_style(mixedUrl)
+{
+    if(typeof(mixedUrl) === 'string')
+        mixedUrl = new Array(mixedUrl);
+
+    for(let i = 0; i < mixedUrl.length; i++) {
+        if(mixedUrl[i] === undefined || !mixedUrl[i].length || $("link[href = '" + mixedUrl[i] + "']").length > 0)
+            continue;
+
+        $("head").append($("<link rel='stylesheet' href='" + mixedUrl[i] + "' type='text/css' />"));
+    }
+}
+
+/**
+ * This function loads the passed list of js files and executes callback function when the all files are loaded.
+ * In case when this function is called for dynamic loading of the same file but many times (like Plyr video init)
+ * it allows to avoid multiply loadings of the same file.
+ */
+function bx_get_scripts (aFiles, fCallback)
+{
+    let iCounter = 0;
+    const
+        iLength = aFiles.length,
+        fHandler = function() {
+            if (iCounter++ >= (iLength-1) && typeof(fCallback) === 'function')
+                fCallback();
+            else
+                fExecute(aFiles[iCounter]);
+        },
+        // Check if js file is not loaded yet then start to load it and returns Promise.
+        // All functions which try to load the same file will wait for loading of the file
+        // and then continue to work
+        fExecute = function(sFile) {
+            if ('undefined' === typeof(glBxLoadedScripts[sFile]))
+                glBxLoadedScripts[sFile] = { complete : $.bxGetCachedScript(sFile).done(fHandler) };
+            else
+                glBxLoadedScripts[sFile].complete.then(fHandler);
+        };
+
+    if ('undefined' === typeof(glBxLoadedScripts))
+        glBxLoadedScripts = {};
+
+    fExecute(aFiles[iCounter]);
+}
+
+jQuery.bxGetCachedScript = function(sUrl, oOptions) 
+{
+    oOptions = $.extend(oOptions || {}, {
+        dataType: "script",
+        cache: true,
+        url: sUrl
+    });
+
+    return jQuery.ajax(oOptions);
+};
+
+function bx_is_selector_in_stylesheet(sSel)
+{
+    var a = [];
+    var aSs = document.styleSheets;
+    for (var sSheet = 0; sSheet < aSs.length; sSheet++) {
+        var ruleList = document.styleSheets[sSheet].cssRules;
+        for (var rule = 0; rule < ruleList.length; rule++)
+           a.push(ruleList[rule].selectorText);
+    }
+    return -1 !== a.indexOf(sSel);
+}
+
+function bx_copy_to_clipboard(s, onSuccess, onFail)
+{
+    if ("undefined" === typeof(onSuccess))
+        onSuccess = function (){};
+    if ("undefined" === typeof(onFail))
+        onFail = function (){};
+
+    const oClip = navigator.clipboard;
+    if ("undefined" !== typeof(oClip)) {
+        navigator.clipboard.writeText(s).then(onSuccess, onFail);
+    } 
+    else {
+        if (document.execCommand) {
+            const el = document.createElement("input");
+            el.value = s;
+            document.body.append(el);
+
+            el.select();
+            el.setSelectionRange(0, s.length);
+
+            if (document.execCommand("copy")) {
+                onSuccess();
+            }
+
+            el.remove();
+        }
+        else {
+            onFail();
+        }
+    }
+}
+
+function bx_regexp_escape(s) 
+{
+    return s
+        .replace(/[|\\{}()[\]^$+*?.]/mg, '\\$&')
+        .replace(/-/mg, '\\x2d');
+}
+
+function bx_get_regexp(sType) 
+{
+    var o = {
+        tag:"(<([^>]+bx-tag[^>]+)>)",
+        mention: "(<([^>]+bx-tag[^>]+)>)",
+        url: "(([A-Za-z]{3,9}:(?:\\/\\/)?)(?:[\\-;:&=\\+\\$,\\w]+@)?[A-Za-z0-9\\.\\-]+|(?:www\\.|[\\-;:&=\\+\\$,\\w]+@)[A-Za-z0-9\\.\\-]+)((?:\\/[\\+~%#\\/\\.\\w\\-_!\\(\\)]*)?\\??(?:[\\-\\+=&;%@\\.\\w_]*)#?(?:[\\.\\!\\/\\w]*))?"
+    };
+
+    return o[sType];
+}
+
+function bx_process_links()
+{
+    // process links in container
+    $('.bx-lnk-cnt .bx-lnk-src:not(.bx-inited)').each(function() {
+        $(this).addClass('bx-inited');
+        var sUrl = $(this).prop('href');
+        $(this).parents('.bx-lnk-cnt').first().addClass('cursor-pointer').click(function(oEvent){
+            if ($(oEvent.target).closest('a').length == 0 && $(oEvent.target).closest('button').length == 0 && $(oEvent.target).closest('form').length == 0 &&  $(oEvent.target).closest('input').length == 0){
+                location.href = sUrl;
+            }
+        });
+    });
+}
+
+
+function bx_redirect_for_external_links (e)
+{
+    if ('undefined' === typeof(aDolOptions.sys_confirmation_before_redirect) || 'on' !== aDolOptions.sys_confirmation_before_redirect)
+        return;
+
+    var aExclude = ['javascript:', 'about:', sUrlRoot];
+    var sPattern = 'a';
+    $(aExclude).each(function () {
+        sPattern += ':not([href^="' + this + '"])';
+    });
+    sPattern += ":is([href^='http://'],[href^='https://'])";
+
+    if ('undefined' === typeof(e))
+        e = $('document');
+
+    e.find(sPattern).each(function() {
+        $(this).on('contextmenu', function(e) {
+            e.preventDefault();
+        });
+        $(this).on('click', function() {
+        	var $this = $(this);
+            bx_redirect_for_external_links_open($this.attr('href'));
+	        return false;
+        });
+    });
+}
+
+function bx_redirect_for_external_links_open (sHref)
+{
+    //bx_confirm(_t('_sys_redirect_confirmation', sHref).replace(/\&quot;/g, ''), function () {
+    //    window.open(sHref, '_blank').focus();
+    //});
+    window.open(sUrlRoot + 'page.php?i=redirect&url=' + encodeURIComponent(sHref), '_blank').focus();
+}
+
+/** @} */

+ 2489 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/utils.inc.php

@@ -0,0 +1,2489 @@
+<?php defined('BX_DOL') or defined('BX_DOL_INSTALL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+define('BX_DOL_LINK_CLASS', 'bx-link'); ///< class to add to every link in user content
+
+define('BX_DATA_TEXT', 1); ///< regular text data type
+define('BX_DATA_TEXT_MULTILINE', 2); ///< regular multiline text data type
+define('BX_DATA_INT', 3); ///< integer data type
+define('BX_DATA_FLOAT', 4); ///< float data type
+define('BX_DATA_CHECKBOX', 5); ///< checkbox data type, 'on' or empty value
+define('BX_DATA_HTML', 6); ///< HTML data type
+define('BX_DATA_DATE', 7); ///< date data type stored as yyyy-mm-dd
+define('BX_DATA_DATETIME', 12); ///< date/time data type stored as yyyy-mm-dd hh:mm:ss
+define('BX_DATA_DATE_TS', 8); ///< date data type stored as unixtimestamp
+define('BX_DATA_DATETIME_TS', 9); ///< date/time data type stored as unixtimestamp
+define('BX_DATA_DATE_TS_UTC', 10); ///< date data type stored as unixtimestamp from UTC time
+define('BX_DATA_DATETIME_TS_UTC', 11); ///< date/time data type stored as unixtimestamp from UTC time
+define('BX_DATA_DATE_UTC', 13); ///< date data type stored as yyyy-mm-dd in UTC time
+define('BX_DATA_DATETIME_UTC', 14); ///< date/time data type stored as yyyy-mm-dd in UTC time
+
+define('BX_SLASHES_AUTO', 0);
+define('BX_SLASHES_ADD', 1);
+define('BX_SLASHES_STRIP', 2);
+define('BX_SLASHES_NO_ACTION', 3);
+
+define('BX_ESCAPE_STR_AUTO', 0); ///< turn apostropes and quote signs into html special chars, for use in @see bx_js_string and @see bx_html_attribute
+define('BX_ESCAPE_STR_APOS', 1); ///< escape apostrophes only, for js strings enclosed in apostrophes, for use in @see bx_js_string and @see bx_html_attribute
+define('BX_ESCAPE_STR_QUOTE', 2); ///< escape quotes only, for js strings enclosed in quotes, for use in @see bx_js_string and @see bx_html_attribute
+
+define('BX_EMAIL_SYSTEM', 0); ///< system email without unsubscribe link, like forgot password or email verification
+define('BX_EMAIL_NOTIFY', 1); ///< notification message, with unsubscribe link
+define('BX_EMAIL_MASS', 2); ///< mass email, one mesage send to manu users, with unsubscribe link
+
+define('BX_MAINTENANCE_FILE', '.bx_maintenance'); ///< file name to use as mantenance mode indicator
+
+/**
+ * The following two functions are needed to convert title to uri and back.
+ * It usefull when titles are used in URLs, like in Categories and Tags.
+ */
+function title2uri($sValue)
+{
+    return str_replace(
+        array('&', '/', '\\', '"', '+'),
+        array('[and]', '[slash]', '[backslash]', '[quote]', '[plus]'),
+        $sValue
+    );
+}
+function uri2title($sValue)
+{
+    return str_replace(
+        array('[and]', '[slash]', '[backslash]', '[quote]', '[plus]'),
+        array('&', '/', '\\', '"', '+'),
+        $sValue
+    );
+}
+
+/*
+ * functions for limiting maximal text length
+ */
+function strmaxtextlen($sInput, $iMaxLen = 60, $sEllipsisSign = '&#8230;')
+{
+    $sTail = '';
+    $s = trim(strip_tags($sInput));
+    if (mb_strlen($s) > $iMaxLen) {
+        $s = mb_substr($s, 0, $iMaxLen);
+        $sTail = $sEllipsisSign;
+    }
+    return htmlspecialchars_adv($s) . $sTail;
+}
+
+function html2txt($content, $tags = "")
+{
+    while($content != strip_tags($content, $tags)) {
+        $content = strip_tags($content, $tags);
+    }
+
+    return $content;
+}
+
+/**
+ * Functions to process user input.
+ * DON'T use to process data before passing to SQL query - use db prepare instead @see BxDolDb::prepare.
+ * It is ok to use bx_process_input and then db prepare.
+ * @param $mixedData data to process
+ * @param $iDataType how to handle data, possible valies:
+ *      @code
+ *          BX_DATA_INT - integer value
+ *          BX_DATA_FLOAT - float values
+ *          BX_DATA_CHECKBOX - 'on' or empty string
+ *          BX_DATA_TEXT - text data, single line (default)
+ *          BX_DATA_TEXT_MULTILINE - text data, multiple lines
+ *          BX_DATA_HTML - HTML data
+ *          BX_DATA_DATE - date data type stored as yyyy-mm-dd
+ *          BX_DATA_DATETIME - date/time data type stored as yyyy-mm-dd hh:mm:ss
+ *          BX_DATA_DATE_TS' -  date data type stored as unixtimestamp
+ *          BX_DATA_DATETIME_TS - date/time data type stored as unixtimestamp
+ *          BX_DATA_DATE_TS_UTC - date data type stored as unixtimestamp from UTC time
+ *          BX_DATA_DATETIME_TS_UTC - date/time data type stored as unixtimestamp from UTC time
+ *      @endcode
+ * @param $mixedParams optional parameters to pass for validation
+ * @param $isCheckMagicQuotes deprecated
+ * @return the filtered data, or FALSE if the filter fails.
+ */
+function bx_process_input ($mixedData, $iDataType = BX_DATA_TEXT, $mixedParams = false, $isCheckMagicQuotes = true)
+{
+    if (is_array($mixedData)) {
+        foreach ($mixedData as $k => $v)
+            $mixedData[$k] = bx_process_input($v, $iDataType, $mixedParams);
+        return $mixedData;
+    }
+
+    switch ($iDataType) {
+    case BX_DATA_INT:
+        return filter_var(trim($mixedData), FILTER_VALIDATE_INT);
+    case BX_DATA_FLOAT:
+        return filter_var(trim($mixedData), FILTER_VALIDATE_FLOAT);
+    case BX_DATA_CHECKBOX:
+        return 'on' == trim($mixedData) ? 'on' : '';
+
+    case BX_DATA_DATE:
+        // maybe consider using strtotime
+        $mixedData = trim($mixedData);
+        if (!preg_match('/^\d{4}-\d{1,2}-\d{1,2}$/', $mixedData))
+            return false;
+        list($iYear, $iMonth, $iDay) = explode('-', $mixedData); // 1985-10-28
+        $iDay   = intval($iDay);
+        $iMonth = intval($iMonth);
+        $iYear  = intval($iYear);
+        return sprintf("%04d-%02d-%02d", $iYear, $iMonth, $iDay);
+    case BX_DATA_DATETIME:
+        // maybe consider using strtotime
+        $mixedData = trim($mixedData);
+        if (!preg_match('#(\d+)\-(\d+)\-(\d+)[\sT]{1}(\d+):(\d+):(\d+)#', $mixedData, $m) && !preg_match('#(\d+)\-(\d+)\-(\d+)[\sT]{1}(\d+):(\d+)#', $mixedData, $m))
+            return bx_process_input ($mixedData, BX_DATA_DATE, $mixedParams, $isCheckMagicQuotes);
+        $iDay   = intval($m[3]);
+        $iMonth = intval($m[2]);
+        $iYear  = intval($m[1]);
+        $iH = intval($m[4]);
+        $iM = intval($m[5]);
+        $iS = isset($m[6]) ? intval($m[6]) : 0;
+        return sprintf("%04d-%02d-%02d %02d:%02d:%02d", $iYear, $iMonth, $iDay, $iH, $iM, $iS); // 1985-10-28 00:59:35
+    case BX_DATA_DATE_TS:
+    case BX_DATA_DATE_TS_UTC:
+        $mixedData = trim($mixedData);
+        if (!preg_match('/^\d{4}-\d{1,2}-\d{1,2}$/', $mixedData))
+            return false;
+        list($iYear, $iMonth, $iDay) = explode('-', $mixedData);
+        $iDay   = intval($iDay);
+        $iMonth = intval($iMonth);
+        $iYear  = intval($iYear);
+        $sFunc = BX_DATA_DATE_TS_UTC == $iDataType ? 'gmmktime' : 'mktime';
+        $iRet = $sFunc (0, 0, 0, $iMonth, $iDay, $iYear);
+        return $iRet > 0 ? $iRet : false;
+    case BX_DATA_DATETIME_TS:
+    case BX_DATA_DATETIME_TS_UTC:
+        if (!preg_match('#(\d+)\-(\d+)\-(\d+)[\s]{1}(\d+):(\d+):(\d+)[\s]{1}([+\-\d:Z]+)#', $mixedData, $m) && !preg_match('#(\d+)\-(\d+)\-(\d+)[\sT]{1}(\d+):(\d+):(\d+)#', $mixedData, $m) && !preg_match('#(\d+)\-(\d+)\-(\d+)[\sT]{1}(\d+):(\d+)#', $mixedData, $m))
+            return bx_process_input ($mixedData, BX_DATA_DATETIME_TS == $iDataType ? BX_DATA_DATE_TS : BX_DATA_DATE_TS_UTC, $mixedParams, $isCheckMagicQuotes);
+        $iDay   = $m[3];
+        $iMonth = $m[2];
+        $iYear  = $m[1];
+        $iH = $m[4];
+        $iM = $m[5];
+        $iS = isset($m[6]) ? $m[6] : 0;        
+        $iTimezoneOffset = 0;
+        if (isset($m[7])) {
+            $oTz = new DateTimeZone($m[7]);
+            $oUtc = new DateTime(str_replace($m[7], '', $mixedData), new DateTimeZone('UTC'));
+            if ($oTz && $oUtc)
+                $iTimezoneOffset = $oTz->getOffset($oUtc);
+        }
+        $sFunc = BX_DATA_DATETIME_TS_UTC == $iDataType ? 'gmmktime' : 'mktime';
+        $iRet = $sFunc ($iH, $iM, $iS, $iMonth, $iDay, $iYear) - $iTimezoneOffset;
+
+        return $iRet > 0 ? $iRet : false;
+
+    case BX_DATA_HTML:
+        return clear_xss($mixedData);
+    case BX_DATA_TEXT_MULTILINE:
+        return nl2br(htmlspecialchars_adv($mixedData));
+    case BX_DATA_TEXT:
+    default:
+        return $mixedData;
+    }
+}
+
+/*
+ * Functions to process user output.
+ * Always use this function before output data which was entered by user before.
+ * @param $mixedData string to process
+ * @param $iDataType how to handle data, possible valies the same as in bx_process_input function, see bx_process_input.
+ * @param $mixedParams optional parameters to pass for validation
+ * @return the filtered data, or FALSE if the filter fails.
+ */
+function bx_process_output ($mixedData, $iDataType = BX_DATA_TEXT, $mixedParams = false)
+{
+    if (is_array($mixedData)) {
+        foreach ($mixedData as $k => $v)
+            $mixedData[$k] = bx_process_output($v, $iDataType, $mixedParams);
+        return $mixedData;
+    }
+
+    switch ($iDataType) {
+    case BX_DATA_INT:
+        return filter_var($mixedData, FILTER_VALIDATE_INT);
+    case BX_DATA_FLOAT:
+        return filter_var($mixedData, FILTER_VALIDATE_FLOAT);
+    case BX_DATA_CHECKBOX:
+        return 'on' == trim($mixedData) ? 'on' : '';
+
+    case BX_DATA_DATE:
+    case BX_DATA_DATETIME:
+        return $mixedData;
+    case BX_DATA_DATE_UTC:
+        if (!preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $mixedData, $m))
+            return '';
+        return sprintf("%04d-%02d-%02d 00:00:00Z", $m[1], $m[2], $m[3]);
+        return $mixedData . "Z";
+    case BX_DATA_DATETIME_UTC:
+        return $mixedData . "Z";
+    case BX_DATA_DATE_TS:
+        return empty($mixedData) ? '' : date("Y-m-d", (int)$mixedData);
+    case BX_DATA_DATE_TS_UTC:
+        return empty($mixedData) ? '' : gmdate("Y-m-d", (int)$mixedData);
+    case BX_DATA_DATETIME_TS:
+        return empty($mixedData) ? '' : date("Y-m-d H:i", (int)$mixedData);
+    case BX_DATA_DATETIME_TS_UTC:
+        return empty($mixedData) ? '' : gmdate("Y-m-d H:i:s\Z", (int)$mixedData);
+
+    case BX_DATA_HTML:
+        $s = bx_linkify_html($mixedData, 'class="' . BX_DOL_LINK_CLASS . '"');
+
+        // remove empty tags from html content #4203 and #4486
+        $s = preg_replace("/(((<[^\/(br)>]*>)+[ \n(<br\s*\/*>)]*(<\/[^>]+>)+)+)/", '', $s);
+        $s = preg_replace('/(<br\s*\/?>\s*){2,}/i', '<br>', $s);
+        return $mixedParams && is_array($mixedParams) && in_array('no_process_macro', $mixedParams) ? $s : bx_process_macros($s);
+    case BX_DATA_TEXT_MULTILINE:
+        $s = $mixedData;
+        return $mixedParams && is_array($mixedParams) && in_array('no_process_macros', $mixedParams) ? $s : bx_process_macros($s);
+    case BX_DATA_TEXT:
+    default:
+        $s = htmlspecialchars_adv($mixedData);
+        return $mixedParams && is_array($mixedParams) && in_array('no_process_macros', $mixedParams) ? $s : bx_process_macros($s);
+    }
+}
+
+function bx_is_macros_in_content (&$s) 
+{
+    return false === strpos($s, '{{~') ? false : true; 
+}
+
+/**
+ * This function converts macros upon text output. 
+ * Macros represents constructions like this:
+ * @code
+ * {{!module_name:function[param1, "param2"]}}
+ * @endcode
+ * For example, to display some content from module Posts:
+ * @code
+ * {{!bx_posts:get_search_result_unit[3]}}
+ * @endcode
+ * Only users which have "use macros" ACL action enabled can use this functionlity.
+ *
+ * @param $s text to process
+ * @return modified or not modified text
+ */
+function bx_process_macros ($s)
+{
+    if (!bx_is_macros_in_content($s))
+        return $s;
+
+    return preg_replace_callback(
+        "/{{\~(.*?)\~}}/", 
+        function ($aMatches) {
+            return BxDolService::callMacro($aMatches[1]); 
+        }, 
+        $s);
+}
+
+/*
+ * This function apply bx_process_input and then bx_process_output.
+ * Use this function to output data immediately after receiving, without saving to database.
+ * Patams are the same as bx_process_input function - @see bx_process_input
+ */
+function bx_process_pass ($mixedData, $iDataType = BX_DATA_TEXT, $mixedParams = false, $isCheckMagicQuotes = true)
+{
+    return bx_process_output(bx_process_input ($mixedData, $iDataType, $mixedParams, $isCheckMagicQuotes), $iDataType, $mixedParams);
+}
+
+/**
+ * DEPRECATED
+ * use bx_process_input + bx_process_output instead
+ * --------
+ * function for processing pass data
+ *
+ * This function cleans the GET/POST/COOKIE data if magic_quotes_gpc() is on
+ * for data which should be outputed immediately after submit
+ */
+/*
+function process_pass_data( $text, $strip_tags = 0 )
+{
+    if ( $strip_tags )
+        $text = strip_tags($text);
+    if ( !get_magic_quotes_gpc() )
+        return $text;
+    else
+        return stripslashes($text);
+}
+*/
+
+/*
+ * function for output data from database into html
+ */
+function htmlspecialchars_adv( $string )
+{
+    return htmlspecialchars($string, ENT_COMPAT, 'UTF-8', false);
+}
+
+/**
+ * Send mail to user by parsing email template
+ */
+function sendMailTemplate($sTemplateName, $iAccountId = 0, $iProfileId = 0, $aReplaceVars = array(), $iEmailType = BX_EMAIL_NOTIFY, $bAddToQueue = false)
+{
+    $oProfile = $iProfileId ? BxDolProfile::getInstance($iProfileId) : null;
+
+    $oAccount = $iAccountId ? BxDolAccount::getInstance($iAccountId) : ($oProfile ? $oProfile->getAccountObject() : null);
+
+    $oEmailTemplates = BxDolEmailTemplates::getInstance();
+
+    if (!($oAccount || $oProfile) || !$oEmailTemplates)
+        return false;
+
+    $aTemplate = $oEmailTemplates->parseTemplate($sTemplateName, $aReplaceVars, $oAccount ? $oAccount->id() : 0, (int)$iProfileId);
+    if (!$aTemplate)
+        return false;
+
+    return sendMail($oAccount->getEmail(), $aTemplate['Subject'], $aTemplate['Body'], 0, array(), $iEmailType, 'html', false, array(), $bAddToQueue);
+}
+
+/**
+ * Send system email 
+ */
+function sendMailTemplateSystem($sTemplateName, $aReplaceVars = array(), $iEmailType = BX_EMAIL_SYSTEM, $bAddToQueue = false)
+{
+    $oEmailTemplates = BxDolEmailTemplates::getInstance();
+
+    if (!$oEmailTemplates)
+        return false;
+
+    $aTemplate = $oEmailTemplates->parseTemplate($sTemplateName, $aReplaceVars);
+    if (!$aTemplate)
+        return false;
+
+    return sendMail(getParam('site_email'), $aTemplate['Subject'], $aTemplate['Body'], 0, array(), $iEmailType, 'html', false, array(), $bAddToQueue);
+}
+/**
+ * Send email function
+ *
+ * @param $mRecipientEmails - Email where email should be send, can be array, string with one email or comma separated
+ * @param $sMailSubject - subject of the message
+ * @param $sMailBody - Body of the message
+ * @param $iRecipientID - ID of recipient profile
+ * @param $aPlus - Array of additional information
+ * @param $iEmailType - email message type: BX_EMAIL_SYSTEM, BX_EMAIL_NOTIFY or BX_EMAIL_MASS
+ * @param $sEmailFlag - use 'html' for HTML email message
+ * @param $isDisableAlert - disable alert
+ * @param $aCustomHeaders - custom email headers
+ * @param $bAddToQueue - add message to email queue
+ * @return true if message was send or false otherwise
+ */
+function sendMail($mRecipientEmails, $sMailSubject, $sMailBody, $iRecipientID = 0, $aPlus = array(), $iEmailType = BX_EMAIL_NOTIFY, $sEmailFlag = 'html', $isDisableAlert = false, $aCustomHeaders = array(), $bAddToQueue = false)
+{
+    if (is_string($mRecipientEmails)){
+        if(strpos($mRecipientEmails, ',') !== false){
+            $mRecipientEmails = explode(',', $mRecipientEmails);
+        }
+        else{
+            return _sendMail($mRecipientEmails, $sMailSubject, $sMailBody, $iRecipientID, $aPlus, $iEmailType, $sEmailFlag, $isDisableAlert, $aCustomHeaders, $bAddToQueue);
+        }
+    }
+    
+    if (is_array($mRecipientEmails)) {
+        $bReturn = false;
+        foreach($mRecipientEmails as $sRecipientEmail) {
+            $sRecipientEmail = trim($sRecipientEmail);
+            if (_sendMail($sRecipientEmail, $sMailSubject, $sMailBody, $iRecipientID, $aPlus, $iEmailType, $sEmailFlag, $isDisableAlert, $aCustomHeaders, $bAddToQueue))
+                $bReturn = true;
+        }
+        return $bReturn;
+    }
+}
+
+function _sendMail($sRecipientEmail, $sMailSubject, $sMailBody, $iRecipientID = 0, $aPlus = array(), $iEmailType = BX_EMAIL_NOTIFY, $sEmailFlag = 'html', $isDisableAlert = false, $aCustomHeaders = array(), $bAddToQueue = false)
+{
+    // make sure that recipient's email is valid and message isn't empty
+    if (!$sMailBody || !$sRecipientEmail || preg_match('/\(2\)$/', $sRecipientEmail))
+        return false;
+
+    // get recipient account
+    $oAccount = !$isDisableAlert ? BxDolAccount::getInstance($sRecipientEmail) : null;
+    $aAccountInfo = $oAccount ? $oAccount->getInfo() : false;
+
+    // don't send bulk emails if user didn't subscribed to site news or email is unconfirmed
+    if ($aAccountInfo && BX_EMAIL_MASS == $iEmailType && (!$aAccountInfo['email_confirmed'] || !$aAccountInfo['receive_news']))
+        return false;
+
+    // don't send email notifications if user didn't subscribed to notifications or email is unconfirmed
+    if ($aAccountInfo && BX_EMAIL_NOTIFY == $iEmailType && (!$aAccountInfo['email_confirmed'] || !$aAccountInfo['receive_updates']))
+        return false;
+
+    // decode HTML entities in the subj
+    $sMailSubject = html_entity_decode($sMailSubject, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8');
+
+    if($bAddToQueue && BxDolQueueEmail::getInstance()->add($sRecipientEmail, $sMailSubject, $sMailBody, $iRecipientID, $aPlus, $iEmailType, $sEmailFlag, $isDisableAlert, $aCustomHeaders))
+        return true;
+
+    // if profile id is provided - get profile's info
+    $aRecipientInfo = false;
+    if ($iRecipientID) {
+        $oProfile = BxDolProfile::getInstance($iRecipientID);
+        if ($oProfile)
+            $aRecipientInfo = $oProfile->getInfo();
+    }
+
+    // get site vars
+    $sEmailNotify = !$isDisableAlert ? getParam('site_email_notify') : $sRecipientEmail;
+    $sSiteTitle = !$isDisableAlert ? getParam('site_title') : 'UNA ' . BX_DOL_VERSION;
+
+    // add unsubscribe link
+    if (!$isDisableAlert && empty($aPlus['unsubscribe'])) {
+        $aPlus['unsubscribe'] = '';
+        if ($oAccount && (BX_EMAIL_MASS == $iEmailType || BX_EMAIL_NOTIFY == $iEmailType))
+            $aPlus['unsubscribe'] = ($sLink = $oAccount->getUnsubscribeLink($iEmailType)) ? '<a href="' . BX_DOL_URL_ROOT . $sLink . '">' . _t('_sys_et_txt_unsubscribe') . '</a>' : '';
+    }
+
+    // parse template
+    if ($aPlus || $iRecipientID || $oAccount) {
+        if(!is_array($aPlus))
+            $aPlus = array();
+        $oEmailTemplates = BxDolEmailTemplates::getInstance();
+        $sMailSubject = $oEmailTemplates->parseContent($sMailSubject, $aPlus, !$iRecipientID && $oAccount ? $oAccount->id() : 0, $iRecipientID);
+        $sMailBody = $oEmailTemplates->parseContent($sMailBody, $aPlus, !$iRecipientID && $oAccount ? $oAccount->id() : 0, $iRecipientID);
+    }
+
+    // email message headers
+    $sMailHeader = '';
+    foreach ($aCustomHeaders as $sHeaderName => $sHeaderValue)
+        $sMailHeader = "$sHeaderName: $sHeaderValue\r\n" . $sMailHeader;
+
+    if (!isset($aCustomHeaders['From']))
+        $sMailHeader = "From: =?UTF-8?B?" . base64_encode( $sSiteTitle ) . "?= <{$sEmailNotify}>";
+
+    if (!isset($aCustomHeaders['MIME-Version']))
+        $sMailHeader = "MIME-Version: 1.0\r\n" . $sMailHeader;
+
+    $sMailParameters = isset($aCustomHeaders['Sender']) ? "-f{$aCustomHeaders['Sender']}" : "-f{$sEmailNotify}";
+
+    if (isset($aCustomHeaders['Subject']))
+        $sMailSubject = $aCustomHeaders['Subject'];
+
+    // build data for alert handler
+    $bResult = null;
+    $aAlert = array(
+        'email' => $sRecipientEmail,
+        'subject' => $sMailSubject,
+        'body' => $sMailBody,
+        'header' => $sMailHeader,
+        'params' => $sMailParameters,
+        'recipient' => $aRecipientInfo,
+        'email_type' => $iEmailType,
+        'html' => 'html' == $sEmailFlag ? true : false,
+        'custom_headers' => $aCustomHeaders,
+        'override_result' => &$bResult,
+    );
+    
+    // alert for disable sending
+    bx_alert('system', 'check_send_mail', (isset($aRecipientInfo['ID']) ? $aRecipientInfo['ID'] : 0), '', $aAlert);
+    
+    if ($bResult !== null)
+        return $bResult;
+
+    // system alert
+    if (!$isDisableAlert) {
+        bx_alert('system', 'before_send_mail', (isset($aRecipientInfo['ID']) ? $aRecipientInfo['ID'] : 0), '', $aAlert);
+        if ($bResult !== null)
+            return $bResult;
+        unset($aAlert['override_result']);
+    }
+
+    // prepare HTML/Plain message
+    if($sEmailFlag == 'html')
+        $sMailHeader = "Content-type: text/html; charset=UTF-8\r\n" . $sMailHeader;
+    else {
+        $sMailHeader = "Content-type: text/plain; charset=UTF-8\r\n" . $sMailHeader;
+        $sMailBody = html2txt($sMailBody);
+    }
+
+    // encode subject
+    if (0 !== strncasecmp($sMailSubject, '=?UTF-8?B?', 10))
+        $sMailSubject = '=?UTF-8?B?' . base64_encode($sMailSubject) . '?=';
+
+    // send mail or put it into queue
+    $bResult = mail($sRecipientEmail, $sMailSubject, $sMailBody, $sMailHeader, $sMailParameters);
+
+    // system alert
+    if (!$isDisableAlert)
+        bx_alert('system', 'send_mail', (isset($aRecipientInfo['ID']) ? $aRecipientInfo['ID'] : 0), '', $aAlert);
+
+    return $bResult;
+}
+
+/*
+ * Getting an array with Templates' Names
+ */
+function get_templates_array($bEnabledOnly = true, $bShortInfo = true)
+{
+    $oDb = BxDolDb::getInstance();
+
+    $sWhereAddon = $bEnabledOnly ? " AND `enabled`='1'" : "";
+
+    if($bShortInfo)
+        return $oDb->getPairs("SELECT `uri`, `title` FROM `sys_modules` WHERE 1 AND `type`='" . BX_DOL_MODULE_TYPE_TEMPLATE . "'" . $sWhereAddon, "uri", "title");
+    else
+        return $oDb->getAllWithKey("SELECT * FROM `sys_modules` WHERE 1 AND `type`='" . BX_DOL_MODULE_TYPE_TEMPLATE . "'" . $sWhereAddon, "uri");
+}
+
+function bx_get_image_exif_and_size($oStorage, $oTranscoder, $iContentId){
+    $sData = '';
+    $sExif = '';
+    $aExif = false;
+    $aPhoto = $oStorage->getFile($iContentId);
+    if ($oTranscoder->isMimeTypeSupported($aPhoto['mime_type'])) {
+        $oImageReize = BxDolImageResize::getInstance();
+        
+        $a = $oImageReize->getImageSize($oTranscoder->getFileUrl($iContentId));
+        $sData = isset($a['w']) && isset($a['h']) ? $a['w'] . 'x' . $a['h'] : '';
+        
+        if ($aExif = $oImageReize->getExifInfo($oStorage->getFileUrlById($iContentId))) {
+            $a = array('Make', 'Model', 'FocalLength', 'ShutterSpeedValue', 'ExposureTime', 'ISOSpeedRatings', 'Orientation', 'Artist', 'Copyright', 'Flash', 'WhiteBalance', 'DateTimeOriginal', 'DateTimeDigitized', 'ExifVersion', 'COMPUTED', 'GPSLatitudeRef', 'GPSLatitude', 'GPSLongitudeRef', 'GPSLongitude', 'GPSAltitudeRef', 'GPSAltitude', 'GPSTimeStamp', 'GPSImgDirectionRef', 'GPSImgDirection', 'GPSDateStamp');
+            $aExifFiltered = array();
+            foreach ($a as $sIndex)
+                if (isset($aExif[$sIndex]))
+                    $aExifFiltered[$sIndex] = $aExif[$sIndex];
+            $sExif = serialize($aExifFiltered);
+        }
+    }
+    return array('exif' => $sExif, 'size' => $sData);
+}
+
+function bx_get_svg_image_size($sUrl)
+{
+    $iWidth = $iHeight = 0;
+
+    $sContent = bx_file_get_contents($sUrl);
+    if(empty($sContent))
+        return [$iWidth, $iHeight];
+
+    $aAttributes = BxDolXmlParser::getInstance()->getAttributes($sContent, 'SVG', 0);
+    if(isset($aAttributes['VIEWBOX'])) {
+        $aViewBox = explode(' ', $aAttributes['VIEWBOX']);
+        if(!empty($aViewBox) && is_array($aViewBox) && count($aViewBox) == 4) {
+            $iWidth = (float)$aViewBox[2];
+            $iHeight = (float)$aViewBox[3];
+        }
+    }
+
+    if(!$iWidth && isset($aAttributes['WIDTH']))
+        $iWidth = $this->_str2px($aAttributes['WIDTH']);
+
+    if(!$iHeight && isset($aAttributes['HEIGHT']))
+        $iHeight = $this->_str2px($aAttributes['HEIGHT']);
+
+    return [$iWidth, $iHeight];
+}
+
+function extFileExists( $sFileSrc )
+{
+    return (file_exists( $sFileSrc ) && is_file( $sFileSrc )) ? true : false;
+}
+
+function getVisitorIP()
+{
+    $ip = "0.0.0.0";
+    if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
+        $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
+    } elseif (isset( $_SERVER['HTTP_CLIENT_IP']) && !empty($_SERVER['HTTP_CLIENT_IP'])) {
+        $ip = explode(".",$_SERVER['HTTP_CLIENT_IP']);
+        $ip = $ip[3].".".$ip[2].".".$ip[1].".".$ip[0];
+    } elseif (!isset( $_SERVER['HTTP_X_FORWARDED_FOR']) || empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
+        if (!isset( $_SERVER['HTTP_CLIENT_IP']) && empty($_SERVER['HTTP_CLIENT_IP']) && isset($_SERVER['REMOTE_ADDR']))
+            $ip = $_SERVER['REMOTE_ADDR'];
+    }
+    return $ip;
+}
+
+function genFlag($sLang = '', $oTemplate = null)
+{
+    if (!$oTemplate)
+        $oTemplate = BxDolTemplate::getInstance();
+    $oTemplate->addCss(BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'flag-icon-css/css/|flag-icon.min.css');
+    $sFlag = BxDolLanguages::getInstance()->getLangFlag($sLang);
+    return '<span title="' . $sFlag . '" class="flag-icon flag-icon-' . $sFlag . '"></span>';
+}
+
+// print debug information ( e.g. arrays )
+function echoDbg( $what, $desc = '' )
+{
+    if ( $desc )
+        echo "<b>$desc:</b> ";
+    echo "<pre>";
+        print_r( $what );
+    echo "</pre>\n";
+}
+
+function echoDbgLog($mWhat, $sDesc = 'unused', $sFileName = 'unused')
+{
+    bx_log('sys_debug', $mWhat);
+}
+
+function bx_is_dbg()
+{
+    return (defined('BX_DBG_COOKIE') && !empty($_COOKIE[BX_DBG_COOKIE])) || (defined('BX_DBG_PROFILES') && in_array(bx_get_logged_profile_id(), BX_DBG_PROFILES));
+}
+
+function dbgTiming($sStartMicrotime)
+{
+    $i1 = explode(' ', microtime ());
+    $i2 = explode(' ', $sStartMicrotime);
+    $iTime = ($i1[0]+$i1[1]) - ($i2[0]+$i2[1]);
+    return round($iTime, 6) . ' sec';
+}
+
+function echoJson($a)
+{
+	header('Content-Type: application/json; charset=utf-8');
+
+	echo json_encode($a);
+}
+
+function clear_xss($val)
+{
+    // HTML Purifier plugin
+    global $oHtmlPurifier;
+    if (!isset($oHtmlPurifier) && !$GLOBALS['logged']['admin']) {
+        HTMLPurifier_Bootstrap::registerAutoload();
+
+        $oConfig = HTMLPurifier_Config::createDefault();
+
+        $oConfig->set('Cache.SerializerPath', rtrim(BX_DIRECTORY_PATH_CACHE, '/'));
+        $oConfig->set('Cache.SerializerPermissions', BX_DOL_DIR_RIGHTS);
+
+        $oConfig->set('HTML.SafeObject', 'true');
+        $oConfig->set('Output.FlashCompat', 'true');
+        $oConfig->set('HTML.FlashAllowFullScreen', 'true');
+        $oConfig->set('Attr.AllowedFrameTargets', array('_blank', '_self', '_parent', '_top'));
+
+        if (getParam('sys_add_nofollow')) {
+            $sHost = parse_url(BX_DOL_URL_ROOT, PHP_URL_HOST);
+            $oConfig->set('URI.Host', $sHost);
+            $oConfig->set('HTML.Nofollow', 'true');
+        }
+
+        $oConfig->set('Filter.Custom', array (
+            new BxDolHTMLPurifierFilterYouTube(), 
+            new BxDolHTMLPurifierFilterYoutubeIframe(), 
+            new BxDolHTMLPurifierFilterAddBxLinksClass(), 
+            new BxDolHTMLPurifierFilterLocalIframe(),
+            new BxDolHTMLPurifierFilterEmbed(),
+        ));
+   
+	    $oConfig->set('HTML.DefinitionID', 'html5-definitions');
+		$oConfig->set('HTML.DefinitionRev', 1);
+      
+		if ($def = $oConfig->maybeGetRawHTMLDefinition()) {
+		    $def->addElement('section', 'Block', 'Flow', 'Common');
+		    $def->addElement('nav',     'Block', 'Flow', 'Common');
+		    $def->addElement('article', 'Block', 'Flow', 'Common');
+		    $def->addElement('aside',   'Block', 'Flow', 'Common');
+		    $def->addElement('header',  'Block', 'Flow', 'Common');
+		    $def->addElement('footer',  'Block', 'Flow', 'Common');
+		    $def->addElement('video', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', array(
+		        'src' => 'URI',
+		        'type' => 'Text',
+		        'width' => 'Length',
+		        'height' => 'Length',
+		        'poster' => 'URI',
+		        'preload' => 'Enum#auto,metadata,none',
+		        'controls' => 'Bool',
+		    ));
+		    $def->addElement('source', 'Block', 'Flow', 'Common', array(
+		        'src' => 'URI',
+		        'type' => 'Text',
+            ));
+            $def->addAttribute('a', 'data-profile-id', 'Text');
+            $def->addAttribute('a', 'dchar', 'Text');
+            $def->addAttribute('div', 'source', 'Text');
+		}
+
+        $oHtmlPurifier = new HTMLPurifier($oConfig);
+    }
+
+    if (!$GLOBALS['logged']['admin'])
+        $val = $oHtmlPurifier->purify($val);
+
+    $sNewVal = $val;
+    if (!$GLOBALS['logged']['admin'])
+        $sNewVal = $oHtmlPurifier->purify($val);
+
+    bx_alert('system', 'clear_xss', 0, 0, array('oHtmlPurifier' => $oHtmlPurifier, 'input_data' => $val, 'return_data' => &$sNewVal));
+
+    return $val;
+}
+
+//--------------------------------------- friendly permalinks --------------------------------------//
+//------------------------------------------- main functions ---------------------------------------//
+function uriGenerate ($sValue, $sTable, $sField, $aParams = [])
+{
+    $sDivider = isset($aParams['divider']) ? $aParams['divider'] : '-';
+    $aCond = isset($aParams['cond']) && is_array($aParams['cond']) ? $aParams['cond'] : [];
+
+    $sValue = uriFilter($sValue, $aParams);
+    if(uriCheckUniq($sValue, $sTable, $sField, $aCond))
+        return $sValue;
+
+    // cut off redundant part
+    if(get_mb_len($sValue) > 240)
+        $sValue = get_mb_substr($sValue, 0, 240);
+
+    // try to add number
+    for($i = 0 ; $i < 999 ; ++$i) {
+        $iRnd = mt_rand(1000, 9999);
+        if(uriCheckUniq($sValue . $sDivider . $iRnd, $sTable, $sField, $aCond))
+            return ($sValue . $sDivider . $iRnd);
+    }
+
+    return rand(0, PHP_INT_MAX);
+}
+
+function uriFilter ($s, $aParams = [])
+{
+    $sEmpty = isset($aParams['empty']) ? $aParams['empty'] : '-';
+    $sDivider = isset($aParams['divider']) ? $aParams['divider'] : '-';
+
+    if(BxTemplConfig::getInstance()->bAllowUnicodeInPreg)
+        $s = get_mb_replace ('/[^\pL^\pN^_]+/u', $sDivider, $s); // unicode characters
+    else
+        $s = get_mb_replace ('/([^\d^\w]+)/u', $sDivider, $s); // latin characters only
+
+    $s = get_mb_replace ('/([' . $sDivider . '^]+)/', $sDivider, $s);
+    $s = get_mb_replace ('/([' . $sDivider . ']+)$/', '', $s); // remove trailing dash
+    if(!$s) 
+        $s = $sEmpty;
+
+    return !isset($aParams['lowercase']) || $aParams['lowercase'] === true ? mb_strtolower($s) : $s;
+}
+
+function uriCheckUniq ($sValue, $sTable, $sField, $aCond = [])
+{
+    $oDb = BxDolDb::getInstance();
+
+    $sWhere = $aCond ? $oDb->arrayToSQL($aCond, ' AND ') : '1';
+    $sSql = $oDb->prepare("SELECT 1 FROM `$sTable` WHERE $sWhere AND `$sField`=? LIMIT 1", $sValue);
+    return !$oDb->query($sSql);
+}
+
+function get_mb_replace ($sPattern, $sReplace, $s)
+{
+    return preg_replace ($sPattern, $sReplace, $s);
+}
+
+function get_mb_len ($s)
+{
+    return (function_exists('mb_strlen')) ? mb_strlen($s) : strlen($s);
+}
+
+function get_mb_substr ($s, $iStart, $iLen)
+{
+    return (function_exists('mb_substr')) ? mb_substr ($s, $iStart, $iLen) : substr ($s, $iStart, $iLen);
+}
+
+function bx_mb_substr_replace($s, $sReplace, $iPosStart, $iLength)
+{
+    return mb_substr($s, 0, $iPosStart) . $sReplace . mb_substr($s, $iPosStart + $iLength);
+}
+
+function bx_mb_strpos ($s, $sReplacement, $iStart = 0)
+{
+    return mb_strpos($s, $sReplacement, $iStart);
+}
+
+/**
+ * Import class file, it automatically detects class path by its prefix or module array/name
+ *
+ * @param $sClassName - full class name or class postfix(withoit prefix) in the case of module class
+ * @param $mixedModule - module array or module name in the case of module class
+ */
+function bx_import($sClassName, $mixedModule = array())
+{
+    if (class_exists($sClassName, false))
+        return;
+
+    $aModule = false;
+    if ($mixedModule) {
+        if (is_array($mixedModule)) {
+            $aModule = $mixedModule;
+        } elseif (is_string($mixedModule)) {
+            $o = BxDolModule::getInstance($mixedModule);
+            $aModule = $o->_aModule;
+        } elseif (is_bool($mixedModule) && true === $mixedModule) {
+            $aModule = $GLOBALS['aModule'];
+        }
+    }
+
+    if ($aModule) {
+        if (class_exists($aModule['class_prefix'] . $sClassName, false))
+            return;
+        require_once (BX_DIRECTORY_PATH_MODULES . $aModule['path'] . 'classes/' . $aModule['class_prefix'] . $sClassName . '.php');
+        return;
+    }
+
+    if (0 === strncmp($sClassName, 'BxDol', 5)) {
+        if (0 === strncmp($sClassName, 'BxDolStudio', 11))
+            require_once(BX_DOL_DIR_STUDIO_CLASSES . $sClassName . '.php');
+        else
+            require_once(BX_DIRECTORY_PATH_CLASSES . $sClassName . '.php');
+        return;
+    }
+
+    if (0 === strncmp($sClassName, 'BxBase', 6)) {
+        if (0 === strncmp($sClassName, 'BxBaseMod', 9)) {
+            $aMatches = array();
+            if (preg_match('/BxBaseMod([A-Z][a-z]+)/', $sClassName, $aMatches)) {
+                require_once(BX_DIRECTORY_PATH_MODULES . 'base/' . strtolower($aMatches[1]) . '/classes/' . $sClassName . '.php');
+                return;
+            }
+        } if (0 === strncmp($sClassName, 'BxBaseStudio', 12)) {
+            require_once(BX_DOL_DIR_STUDIO_BASE . 'scripts/' . $sClassName . '.php');
+            return;
+        } else {
+            require_once(BX_DIRECTORY_PATH_BASE . 'scripts/' . $sClassName . '.php');
+            return;
+        }
+    }
+
+    if (0 === strncmp($sClassName, 'BxTempl', 7)) {
+        if(0 === strncmp($sClassName, 'BxTemplStudio', 13)) {
+            $sPath = BX_DIRECTORY_PATH_MODULES . BxDolStudioTemplate::getInstance()->getPath() . 'data/template/studio/scripts/' . $sClassName . '.php';
+            if (!file_exists($sPath))
+                $sPath = BX_DOL_DIR_STUDIO_BASE . 'scripts_templ/' . $sClassName . '.php';
+        } else {
+            $sPath = BX_DIRECTORY_PATH_MODULES . BxDolTemplate::getInstance()->getPath() . 'data/template/system/scripts/' . $sClassName . '.php';
+        }
+
+        if (file_exists($sPath)) {
+            require_once($sPath);
+            return;
+        }
+        else{
+            require_once(BX_DIRECTORY_PATH_BASE . 'scripts_templ/' . $sClassName . '.php');
+            return;
+        }
+
+        trigger_error ("bx_import fatal error: class (" . $sClassName . ") wasn't found", E_USER_ERROR);
+    }
+}
+
+/**
+ * used in spl_autoload_register() function, so no need to call bx_import for system classes
+ */
+function bx_autoload($sClassName)
+{
+    if (0 === strncmp($sClassName, 'BxDol', 5) || 0 === strncmp($sClassName, 'BxBase', 6) || 0 === strncmp($sClassName, 'BxTempl', 7))
+        bx_import($sClassName);
+}
+
+/**
+ * Gets an instance of class pathing necessary parameters if it's necessary.
+ *
+ * @param string $sClassName class name.
+ * @param array $aParams an array of parameters to be pathed to the constructor of the class.
+ * @param array $mixedModule an array with module description. Is used when the requested class is located in some module.
+ * @return unknown
+ */
+function bx_instance($sClassName, $aParams = array(), $mixedModule = array())
+{
+    if(isset($GLOBALS['bxDolClasses'][$sClassName]))
+        return $GLOBALS['bxDolClasses'][$sClassName];
+
+    if ($mixedModule) {
+        if (!is_array($mixedModule)) {
+            $o = BxDolModule::getInstance($mixedModule);
+            $mixedModule = $o->_aModule;
+        }
+        $sClassName = bx_ltrim_str($sClassName, $mixedModule['class_prefix']);
+        bx_import($sClassName, $mixedModule);
+        $sClassName = $mixedModule['class_prefix'] . $sClassName;
+    }
+
+    $oClass = new ReflectionClass($sClassName);
+
+    $GLOBALS['bxDolClasses'][$sClassName] = empty($aParams) ? $oClass->newInstance() : $oClass->newInstanceArgs($aParams);
+
+    return $GLOBALS['bxDolClasses'][$sClassName];
+}
+
+
+/**
+ * Escapes string/array ready to pass to js script with filtered symbols like ', " etc
+ *
+ * @param $mixedInput - string/array which should be filtered
+ * @param $iQuoteType - string escaping method: BX_ESCAPE_STR_AUTO(default), BX_ESCAPE_STR_APOS or BX_ESCAPE_STR_QUOTE
+ * @return converted string / array
+ */
+function bx_js_string ($mixedInput, $iQuoteType = BX_ESCAPE_STR_AUTO)
+{
+    $aUnits = array(
+        "\n" => "\\n",
+        "\r" => "",
+    );
+    if (BX_ESCAPE_STR_APOS == $iQuoteType) {
+        $aUnits["'"] = "\\'";
+        $aUnits['<script'] = "<scr' + 'ipt";
+        $aUnits['</script>'] = "</scr' + 'ipt>";
+    } elseif (BX_ESCAPE_STR_QUOTE == $iQuoteType) {
+        $aUnits['"'] = '\\"';
+        $aUnits['<script'] = '<scr" + "ipt';
+        $aUnits['</script>'] = '</scr" + "ipt>';
+    } else {
+        $aUnits['"'] = '&quot;';
+        $aUnits["'"] = '&apos;';
+        $aUnits["<script>"] = '&lt;script&gt;';
+        $aUnits["</script>"] = '&lt;/script&gt;';
+    }
+    return str_replace(array_keys($aUnits), array_values($aUnits), $mixedInput);
+}
+
+/**
+ * Return input string/array ready to pass to html attribute with filtered symbols like ', " etc
+ *
+ * @param $mixedInput - string/array which should be filtered
+ * @param $iQuoteType - string escaping BX_ESCAPE_STR_AUTO, BX_ESCAPE_STR_APOS or BX_ESCAPE_STR_QUOTE
+ * @return converted string / array
+ */
+function bx_html_attribute ($mixedInput, $iQuoteType = BX_ESCAPE_STR_AUTO)
+{
+    $aUnits = array ();
+    if (BX_ESCAPE_STR_APOS == $iQuoteType)
+        $aUnits["'"] = "\\'";
+    elseif (BX_ESCAPE_STR_QUOTE == $iQuoteType)
+        $aUnits['"'] = '&quot;';
+    else
+        $aUnits = array("\"" => "&quot;", "'" => "&apos;");
+
+    return str_replace(array_keys($aUnits), array_values($aUnits), $mixedInput);
+}
+
+/**
+ * Escapes string/array ready to pass to php script with filtered symbols like ', " etc
+ *
+ * @param mixed $mixedInput - string/array which should be filtered
+ * @return converted string / array
+ */
+function bx_php_string_apos ($mixedInput)
+{
+    return str_replace("'", "\\'", $mixedInput);
+}
+function bx_php_string_quot ($mixedInput)
+{
+    return str_replace('"', '\\"', $mixedInput);
+}
+
+/**
+ * Gets file contents by URL.
+ *
+ * @param string $sFileUrl - file URL to be read.
+ * @param array $aParams - an array of parameters to be pathed with URL.
+ * @param string $sMethod - post or get.
+ * @param array $aHeaders - custom headers.
+ * @param string $sHttpCode - HTTP code to return
+ * @param array $aBasicAuth - array with 'user' and 'password' for Basic HTTP Auth
+ * @return string the file's contents.
+ */
+function bx_file_get_contents($sFileUrl, $aParams = array(), $sMethod = 'get', $aHeaders = array(), &$sHttpCode = null, $aBasicAuth = array(), $iTimeout = 0, $aCustomCurlParams = array())
+{
+    if ('post' != $sMethod && 'post-json' != $sMethod && 'post-json-object' != $sMethod)
+    	$sFileUrl = bx_append_url_params($sFileUrl, $aParams);
+
+    $sResult = '';
+    if(function_exists('curl_init')) {
+        $rConnect = curl_init();
+
+        curl_setopt($rConnect, CURLOPT_USERAGENT, 'UNA ' . (defined('BX_DOL_VERSION') ? constant('BX_DOL_VERSION') : ''));
+        curl_setopt($rConnect, CURLOPT_TIMEOUT, BxDolDb::getInstance() ? getParam('sys_default_curl_timeout') : 10);
+        curl_setopt($rConnect, CURLOPT_URL, $sFileUrl);
+        curl_setopt($rConnect, CURLOPT_RETURNTRANSFER, 1);
+
+        if (0 !== $iTimeout) {
+            curl_setopt($rConnect, CURLOPT_CONNECTTIMEOUT, $iTimeout);
+            curl_setopt($rConnect, CURLOPT_TIMEOUT, $iTimeout);
+        }
+        
+        if(getParam('sys_curl_ssl_allow_untrusted') == 'on'){
+            curl_setopt($rConnect, CURLOPT_SSL_VERIFYPEER, false);
+            curl_setopt($rConnect, CURLOPT_SSL_VERIFYHOST, false);
+        }
+
+        if (!ini_get('open_basedir'))
+            curl_setopt($rConnect, CURLOPT_FOLLOWLOCATION, 1);
+
+        if ($aBasicAuth)
+            curl_setopt($rConnect, CURLOPT_USERPWD, $aBasicAuth['user'] . ':' . $aBasicAuth['password']);
+
+        if ('post' == $sMethod) {
+            curl_setopt($rConnect, CURLOPT_POST, true);
+            curl_setopt($rConnect, CURLOPT_POSTFIELDS, http_build_query($aParams));
+        }
+        elseif ('post-json' == $sMethod) {
+            curl_setopt($rConnect, CURLOPT_POST, true);
+            curl_setopt($rConnect, CURLOPT_POSTFIELDS, json_encode($aParams));
+            $aHeaders[] = 'Content-Type: application/json';
+        }
+        elseif ('post-json-object' == $sMethod) {
+            curl_setopt($rConnect, CURLOPT_POST, true);
+            curl_setopt($rConnect, CURLOPT_POSTFIELDS, json_encode($aParams, JSON_FORCE_OBJECT));
+            $aHeaders[] = 'Content-Type: application/json';
+        }
+
+        if ($aHeaders)
+            curl_setopt($rConnect, CURLOPT_HTTPHEADER, $aHeaders);
+
+        if (defined('BX_DOL_URL_ROOT') && 0 === strpos($sFileUrl, BX_DOL_URL_ROOT)) {
+            $sAllCookies = '';
+            foreach($_COOKIE as $sKey => $mValue){
+                if(is_array($mValue)){
+                    foreach ($mValue as $k => $v)
+                        $sAllCookies .= "{$sKey}[{$k}]={$v};";
+                }
+                else{
+                    $sAllCookies .= $sKey . '=' . $mValue . ';';
+                }
+            }
+            curl_setopt($rConnect, CURLOPT_COOKIE, $sAllCookies);
+        }
+
+        if ($aCustomCurlParams)
+            foreach ($aCustomCurlParams as $sName => $mixedValue)
+                curl_setopt($rConnect, $sName, $mixedValue);
+
+        $sResult = curl_exec($rConnect);
+
+        if (curl_errno($rConnect) == 60) { // CURLE_SSL_CACERT
+            curl_setopt($rConnect, CURLOPT_CAINFO, BX_DIRECTORY_PATH_PLUGINS . 'curl/cacert/cacert.pem');
+            $sResult = curl_exec($rConnect);
+        }
+
+        if (NULL !== $sHttpCode)
+            $sHttpCode = curl_getinfo($rConnect, CURLINFO_HTTP_CODE);
+
+        curl_close($rConnect);
+    }
+    else {
+
+        $iSaveTimeout = false;
+        if (0 !== $iTimeout) {
+            $iSaveTimeout = ini_get('default_socket_timeout');
+            ini_set('default_socket_timeout', $iTimeout);
+        }
+
+        $sResult = @file_get_contents($sFileUrl);
+
+        if (0 !== $iTimeout && false !== $iSaveTimeout) {
+            ini_set('default_socket_timeout', $iSaveTimeout);
+        }
+    }
+
+    return $sResult;
+}
+
+function bx_make_utf8($s)
+{
+    // Test it and see if it is UTF-8 or not
+    if (false !== mb_detect_encoding($s, ["UTF-8"], true))
+        return $s;
+
+    $sEncoding = mb_detect_encoding($s, ['ASCII', 'BASE64', 'HTML-ENTITIES', 'Windows-1251', 'KOI8-R'], true);
+    if ($sEncoding === false)
+        return $s;
+
+    return mb_convert_encoding($s, "UTF-8", $sEncoding);
+}
+
+function bx_get_site_info($sSourceUrl, $aProcessAdditionalTags = array())
+{
+    $aResult = array();
+    $sContent = bx_file_get_contents($sSourceUrl);
+
+    if ($sContent) {
+        $sCharset = '';
+        preg_match("/<meta.+charset=([A-Za-z0-9-]+).+>/i", $sContent, $aMatch);
+        if (isset($aMatch[1]))
+            $sCharset = $aMatch[1];
+
+        $sContent = preg_replace("/<script[^>]*>(.*?)<\/script>/i", '', $sContent);
+        $sContent = preg_replace("/<style[^>]*>(.*?)<\/style>/i", '', $sContent);
+        if (preg_match("/<title[^>]*>(.*?)<\/title>/i", $sContent, $aMatch))
+            $aResult['title'] = bx_make_utf8(strip_tags($aMatch[1]), $sCharset);
+        else
+            $aResult['title'] = parse_url($sSourceUrl, PHP_URL_HOST);
+
+        $aResult['description'] = bx_make_utf8(bx_parse_html_tag($sContent, 'meta', 'name', 'description', 'content', $sCharset), $sCharset);
+        $aResult['keywords'] = bx_make_utf8(bx_parse_html_tag($sContent, 'meta', 'name', 'keywords', 'content', $sCharset), $sCharset);
+
+        if ($aProcessAdditionalTags) {
+
+            foreach ($aProcessAdditionalTags as $k => $a) {
+                $aResult[$k] = bx_parse_html_tag(
+                    $sContent, 
+                    isset($a['tag']) ? $a['tag'] : 'meta', 
+                    isset($a['name_attr']) ? $a['name_attr'] : 'itemprop', 
+                    isset($a['name']) ? $a['name'] : $k, 
+                    isset($a['content_attr']) ? $a['content_attr'] : 'content', 
+                    $sCharset,
+                    isset($a['specialchars_decode']) ? $a['specialchars_decode'] : true); 
+
+                if ((isset($a['name']) && 'og:image' == $a['name']) || (isset($a['tag']) && 'link' == $a['tag'])) {
+                    $aResult[$k] = bx_get_site_info_fix_relative_url ($sSourceUrl, $aResult[$k]);
+                }
+            }
+
+        }
+    }
+
+    return $aResult;
+}
+
+/**
+ * Fix relative URL to make it absolute
+ * @param $sSourceUrl main URL
+ * @param $s URL to fix
+ * @return absolute URL or URL wothout changes
+ */ 
+function bx_get_site_info_fix_relative_url ($sSourceUrl, $s)
+{
+    if (0 === stripos($s, 'http://') || 0 === stripos($s, 'https://') || 0 === stripos($s, 'ftp://') || !$s)
+        return $s;
+
+    if ('/' == $s[0]) {
+        $a = parse_url($sSourceUrl);
+        return $a['scheme'] . '://' . $a['host'] . (empty($a['port']) || 80 == $a['port'] || 443 == $a['port'] ? '' : ':' . $a['port']) . $s;
+    }
+
+    return $sSourceUrl . ('/' === substr($sSourceUrl, -1) ? $s : '/' . $s);
+}
+
+function bx_parse_html_tag ($sContent, $sTag, $sAttrNameName, $sAttrNameValue, $sAttrContentName, $sCharset = false, $bSpecialCharsDecode = true)
+{
+    if (!preg_match("/<{$sTag}\s+{$sAttrNameName}[='\" ]+{$sAttrNameValue}['\"]\s+{$sAttrContentName}[='\" ]+([^'>\"]*)['\"][^>]*>/i", $sContent, $aMatch) || !isset($aMatch[1]))
+        preg_match("/<{$sTag}\s+{$sAttrContentName}[='\" ]+([^'>\"]*)['\"]\s+{$sAttrNameName}[='\" ]+{$sAttrNameValue}['\"][^>]*>/i", $sContent, $aMatch);
+
+    $s = isset($aMatch[1]) ? $aMatch[1] : '';
+
+    if ($s && $sCharset)
+        $s = mb_convert_encoding($s, 'UTF-8', $sCharset);
+
+    if ($bSpecialCharsDecode)
+        $s = htmlspecialchars_decode($s);
+        
+    return $s;
+}
+
+// calculation ini_get('upload_max_filesize') in bytes as example
+function return_bytes($val)
+{
+    $val = trim($val);
+    $last = strtolower($val[strlen($val)-1]);
+    $val = (int)$val;
+    switch($last) {
+        // The 'G' modifier is available since PHP 5.1.0
+        case 'k':
+            $val *= 1024;
+            break;
+        case 'm':
+            $val *= 1024 * 1024;
+            break;
+        case 'g':
+            $val *= 1024 * 1024 * 1024;
+            break;
+    }
+    return $val;
+}
+
+// Generate Random Password
+function genRndPwd($iLength = 8, $bSpecialCharacters = true)
+{
+    $sPassword = '';
+    $sChars = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789";
+
+    if($bSpecialCharacters === true)
+        $sChars .= "!?=/&+,.";
+
+    for($i = 0; $i < $iLength; $i++) {
+        $x = mt_rand(0, strlen($sChars) -1);
+        $sPassword .= $sChars[$x];
+    }
+
+    return $sPassword;
+}
+
+// Generate Random Salt for Password encryption
+function genRndSalt()
+{
+    return genRndPwd(8, true);
+}
+
+/**
+ * Encrypt user password.
+ * When BX_PWD_ALGO is 'custom', then bx_custom_pwd_encrypt function with custom algorythm must be defined in inc/header.inc.php file.
+ * @param $sPwd clear password
+ * @param $sSalt salt
+ * @return password hash
+ */ 
+function encryptUserPwd($sPwd, $sSalt)
+{
+	$sAlgo = defined('BX_PWD_ALGO') ? BX_PWD_ALGO : '';
+
+    switch ($sAlgo) {
+        
+        case 'custom':
+            return bx_custom_pwd_encrypt($sPwd, $sSalt); 
+
+    	case 'crypt':
+            return crypt($sPwd, BX_PWD_ALGO_SALT);
+
+        case 'sha1_crypt_salt':
+            return sha1(crypt($sPwd, BX_PWD_ALGO_SALT) . $sSalt);
+
+        case 'sha1_md5_salt':
+        default:
+            return sha1(md5($sPwd) . $sSalt);
+    }
+}
+
+/**
+ * Hash profile ID
+ */
+function encryptUserId($sId)
+{
+    if (!($oProfile = BxDolProfile::getInstance($sId)))
+        return false;
+
+    if (!($oAccount = $oProfile->getAccountObject()))
+        return false;
+
+    $aAccountInfo = $oAccount->getInfo();
+    return sha1(md5($sId) . md5($aAccountInfo['salt']) . BX_DOL_SECRET);
+}
+
+function bx_get_reset_password_key($sValue, $sField = 'email', $iLifetime = 0)
+{
+    if(empty($iLifetime)) {
+        $iLifetime = (int)getParam('sys_account_reset_password_key_lifetime');
+        if(empty($iLifetime)) 
+            $iLifetime = 86400;
+    }
+
+    $oKey = BxDolKey::getInstance();
+    if(!$oKey)
+        return false;
+
+    return $oKey->getNewKey(array($sField => $sValue), $iLifetime);
+}
+
+function bx_get_reset_password_link($sValue, $sField = 'email', $iLifetime = 0)
+{
+    $sKey = bx_get_reset_password_key($sValue, $sField, $iLifetime);
+    if(!$sKey)
+        return false;
+
+    return bx_get_reset_password_link_by_key($sKey);
+}
+
+function bx_get_reset_password_link_by_key($sKey)
+{
+    return bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=forgot-password', array('key' => $sKey)));
+}
+
+function bx_get_reset_password_redirect($iAccountId)
+{
+    $sResult = '';
+    $sRedirect = getParam('sys_account_reset_password_redirect');
+    switch($sRedirect) {
+        case 'home':
+            $sResult = BxDolPermalinks::getInstance()->permalink('page.php?i=home');
+            break;
+        
+        case 'profile':
+        case 'profile_edit':
+            if(empty($iAccountId))
+                break;
+            
+            $oAccount = BxDolAccount::getInstance($iAccountId);
+            if(!$oAccount)
+                break;
+            
+            $aAccountInfo = $oAccount->getInfo();
+            if(empty($aAccountInfo) || !is_array($aAccountInfo) || empty($aAccountInfo['profile_id']))
+                break;
+
+            $oProfile = BxDolProfile::getInstance((int)$aAccountInfo['profile_id']);
+            if(!$oProfile)
+                break;
+
+            $sResult = $oProfile->{'get' . ($sRedirect == 'profile_edit' ? 'Edit' : '') . 'Url'}();
+            break;
+
+        case 'custom':
+            $sResult = getParam('sys_account_reset_password_redirect_custom');
+            break;
+    }
+
+    if(!empty($sResult))
+        $sResult = bx_absolute_url($sResult);
+
+    return $sResult;
+}
+
+function bx_get ($sName, $sMethod = false)
+{
+    if (isset($_GET[$sName]) && ('get' == $sMethod || !$sMethod))
+        return $_GET[$sName];
+    elseif (isset($_POST[$sName]) && ('post' == $sMethod || !$sMethod))
+        return $_POST[$sName];
+    else
+        return false;
+}
+
+function bx_set ($sName, $sValue, $sMethod = false)
+{
+    if(!$sMethod)
+        $sMethod = 'get';
+
+    $bResult = true;
+    switch($sMethod) {
+        case 'get':
+            $_GET[$sName] = $sValue;
+            break;
+        
+        case 'post':
+            $_POST[$sName] = $sValue;
+            break;
+
+        default:
+            $bResult = false;
+    }
+
+    return $bResult;
+}
+
+function bx_get_with_prefix ($sPrefix, $sMethod = false)
+{
+    $aSources = array('get' => &$_GET, 'post' => &$_POST);
+
+    $aFiltered = array();
+    foreach($aSources as $sName => $aSource)
+        if($sMethod == $sName || !$sMethod)
+            $aFiltered = array_merge($aFiltered, array_filter($aSource, function($sKey) use ($sPrefix) {
+                return strpos($sKey, $sPrefix) === 0;
+            }, ARRAY_FILTER_USE_KEY));
+
+    $aUpdated = array();
+    array_walk($aFiltered, function($sValue, $sKey) use ($sPrefix, &$aUpdated) {
+        $aUpdated[trim(str_replace($sPrefix, '', $sKey), '_-')] = $sValue;
+    });
+
+    return $aUpdated;
+}
+
+function bx_get_base_url_inline($aParams = array())
+{
+    $aBaseLink = parse_url(BX_DOL_URL_ROOT);
+    $sPageLink = (!empty($aBaseLink['scheme']) ? $aBaseLink['scheme'] : 'http') . '://' . $aBaseLink['host'] . (!empty($aBaseLink['port']) ? ':' . $aBaseLink['port'] : '');
+    if(!empty($_SERVER['REQUEST_URI']))
+        $sPageLink .= $_SERVER['REQUEST_URI'];
+
+    list($sPageLink, $aPageParams) = bx_get_base_url($sPageLink);
+
+    if(!empty($_SERVER['QUERY_STRING'])) {
+        $aPageParamsAdd = array();
+        parse_str($_SERVER['QUERY_STRING'], $aPageParamsAdd);
+        if(!empty($aPageParamsAdd) && is_array($aPageParamsAdd))
+            $aPageParams = array_merge($aPageParams, $aPageParamsAdd);
+    }
+
+    if(!empty($aParams) && is_array($aParams))
+        $aPageParams = array_merge($aPageParams, $aParams);
+
+    return array($sPageLink, $aPageParams);
+}
+
+function bx_get_base_url_popup($aParams = array())
+{
+    list($sPageLink, $aPageParams) = bx_get_base_url($_SERVER['HTTP_REFERER']);
+
+    if(!empty($aParams) && is_array($aParams))
+        $aPageParams = array_merge($aPageParams, $aParams);
+
+    return array($sPageLink, $aPageParams);
+}
+
+function bx_get_base_url($sPageLink)
+{
+    $sPageLink = BxDolPermalinks::getInstance()->unpermalink($sPageLink, false);
+
+    $sPageParams = '';
+    if(strpos($sPageLink, '?') !== false)
+        list($sPageLink, $sPageParams) = explode('?', $sPageLink);
+
+    $aPageParams = array();
+    if(!empty($sPageParams))
+        parse_str($sPageParams, $aPageParams);
+
+    return array($sPageLink, $aPageParams);
+}
+
+function bx_get_page_info()
+{
+    list($sPageLink, $aPageParams) = bx_get_base_url_inline();
+
+    if(isset($aPageParams['i'], $aPageParams['id']) && ($oPage = BxDolPage::getObjectInstanceByURI($aPageParams['i'])) !== false) {
+        $sPageModule = $oPage->getModule();
+        if(bx_srv('system', 'is_module_context', [$sPageModule]) && ($oProfile = BxDolProfile::getInstanceByContentAndType($aPageParams['id'], $sPageModule)) !== false)
+            return [
+                'context_module' => $sPageModule, 
+                'context_id' => $aPageParams['id'], 
+                'context_profile_id' => $oProfile->id()
+            ];
+    }
+
+    if(isset($aPageParams['profile_id']) && ($oProfile = BxDolProfile::getInstance($aPageParams['profile_id'])) !== false) {
+        $sProfileModule = $oProfile->getModule();
+        if(bx_srv('system', 'is_module_context', [$sProfileModule]))
+            return [
+                'context_module' => $sProfileModule, 
+                'context_id' => $oProfile->getContentId(), 
+                'context_profile_id' => $aPageParams['profile_id']
+            ];
+    }
+
+    return false;
+}
+
+function bx_get_location_bounds_latlng($fLatitude, $fLongitude, $iRadiusInKm)
+{
+    $fEquatorLatInKm = 111.321;
+    $aRv = array();
+    $aRv['max_lat'] = $fLatitude + $iRadiusInKm / $fEquatorLatInKm;
+    $aRv['min_lat'] = $fLatitude - ($aRv['max_lat'] - $fLatitude);
+    $aRv['max_lng'] = $fLongitude + $iRadiusInKm / (cos($aRv['min_lat'] * M_PI / 180) * $fEquatorLatInKm);
+    $aRv['min_lng'] = $fLongitude - ($aRv['max_lng'] - $fLongitude);
+    return $aRv;
+    
+}
+
+function bx_encode_url_params ($a, $aExcludeKeys = array (), $aOnlyKeys = false)
+{
+    $s = '';
+    foreach ($a as $sKey => $sVal) {
+        if (in_array($sKey, $aExcludeKeys))
+            continue;
+        if (false !== $aOnlyKeys && !in_array($sKey, $aOnlyKeys))
+            continue;
+        if (is_array($sVal)) {
+            foreach ($sVal as $sSubVal) {
+                $s .= rawurlencode($sKey) . '[]=' . rawurlencode(is_array($sSubVal) ? 'array' : $sSubVal) . '&';
+            }
+        } else {
+            $s .= rawurlencode($sKey) . '=' . rawurlencode($sVal) . '&';
+        }
+    }
+    return $s;
+}
+
+/**
+ * It works similar to `parse_str` php function, but it doesn't decode URL params
+ */
+function bx_parse_str($s) 
+{
+    $a = [];
+    $aPairs = explode('&', $s);
+
+    foreach ($aPairs as $i) {
+        list($sName, $mixedValue) = explode('=', $i, 2);
+        $sName = rtrim($sName, '[]');
+
+        if (isset($a[$sName])) {
+            if (is_array($a[$sName]))
+                $a[$sName][] = $mixedValue;
+            else 
+                $a[$sName] = [$a[$sName], $mixedValue];
+        }
+        else {
+            $a[$sName] = $mixedValue;
+        }
+    }
+
+    return $a;
+}
+
+function bx_append_url_params ($sUrl, $mixedParams, $bEncodeParams = true, $aIgnoreParams = [])
+{
+    if (!$mixedParams)
+        return $sUrl;
+
+    $sParams = false === strpos($sUrl, '?') ? '?' : '&';
+
+    if (is_array($mixedParams)) {
+        foreach($mixedParams as $sKey => $sValue) {
+            if (!is_array($sValue)) {
+                if ($bEncodeParams) {
+                    if (!in_array($sKey, $aIgnoreParams))
+                        $sKey = rawurlencode($sKey);
+                    if (!in_array($sValue, $aIgnoreParams))
+                        $sValue = rawurlencode($sValue);
+                }
+                $sParams .= $sKey . '=' . $sValue . '&';
+            }
+            else {
+                foreach($sValue as $sSubValue) {
+                    if ($bEncodeParams) {
+                        if (!in_array($sKey, $aIgnoreParams))
+                            $sKey = rawurlencode($sKey);
+                        if (!in_array($sSubValue, $aIgnoreParams))
+                            $sSubValue = rawurlencode($sSubValue);
+                    }
+                    $sParams .= $sKey . '[]=' . $sSubValue . '&';
+                }
+            }
+        }
+        $sParams = substr($sParams, 0, -1);
+    } else {
+        $sParams .= $mixedParams;
+    }
+    return $sUrl . $sParams;
+}
+
+function bx_process_url_param($sValue, $sPattern = "/^[\d\w_-]+$/")
+{
+    $mixedValue = bx_process_input($sValue);
+    return $mixedValue !== false && preg_match($sPattern, $mixedValue) ? $mixedValue : '';
+}
+
+function bx_rrmdir($directory)
+{
+    if (substr($directory,-1) == "/")
+        $directory = substr($directory,0,-1);
+
+    if (!file_exists($directory) || !is_dir($directory))
+        return false;
+    elseif (!is_readable($directory))
+        return false;
+
+    if (!($directoryHandle = opendir($directory)))
+        return false;
+
+    while ($contents = readdir($directoryHandle)) {
+        if ($contents != '.' && $contents != '..') {
+            $path = $directory . "/" . $contents;
+
+            if (is_dir($path))
+                bx_rrmdir($path);
+            else
+                unlink($path);
+        }
+    }
+
+    closedir($directoryHandle);
+
+    if (!rmdir($directory))
+        return false;
+
+    return true;
+}
+
+function bx_clear_folder ($sPath, $aExts = array ())
+{
+    if (substr($$sPath,-1) == "/")
+        $sPath = substr($sPath,0,-1);
+
+    if (!file_exists($sPath) || !is_dir($sPath))
+        return false;
+    elseif (!is_readable($sPath))
+        return false;
+
+    if (!($h = opendir($sPath)))
+        return false;
+
+    while ($sFile = readdir($h)) {
+        if ('.' == $sFile || '..' == $sFile)
+            continue;
+
+        $sFullPath = $sPath . '/' . $sFile;
+
+        if (is_dir($sFullPath))
+            continue;
+
+        if (!$aExts || (($sExt = pathinfo($sFullPath, PATHINFO_EXTENSION)) && in_array($sExt, $aExts)))
+            @unlink($sFullPath);
+    }
+
+    closedir($h);
+
+    return true;
+}
+
+function bx_ltrim_str ($sString, $sPrefix, $sReplace = '')
+{
+    if ($sReplace && substr($sString, 0, strlen($sReplace)) == $sReplace)
+        return $sString;
+    if (substr($sString, 0, strlen($sPrefix)) == $sPrefix)
+        return $sReplace . substr($sString, strlen($sPrefix));
+    return $sString;
+}
+
+function bx_rtrim_str ($sString, $sPrefix, $sReplace = '')
+{
+    if ($sReplace && substr($sString, -strlen($sReplace)) == $sReplace)
+        return $sString;
+    if (substr($sString, -strlen($sPrefix)) == $sPrefix)
+        return substr($sString, 0, -strlen($sPrefix)) . $sReplace;
+    return $sString;
+}
+
+/**
+ * Strip all lines with no information for example: <p></p>, <br /><br />
+ */ 
+function bx_trim_nl_duplicates($s)
+{
+    $sStrip = implode('', array_keys(get_html_translation_table(HTML_ENTITIES)));
+    return implode('', array_filter(mb_split("[\n\r]", $s), function($s) use ($sStrip) {
+        return trim(strip_tags($s, '<button><canvas><embed><hr><iframe><img><input><object><select><svg><video>'), $sStrip) !== '';
+    }));
+}
+
+/**
+ * Convert array to attributes string
+ *
+ * Example:
+ * @code
+ * $a = array('name' => 'test', 'value' => 5);
+ * $s = bx_convert_array2attrs($a);
+ * echo $s; // outputs: name="test" value="5"
+ * @endcode
+ *
+ * @param $a - array of attributes
+ * @param $sClasses - classes to merge with 'class' attribute
+ * @param $sStyles - styles to merge with 'style' attribute
+ * @return string
+ */
+function bx_convert_array2attrs ($a, $sClasses = false, $sStyles = false)
+{
+    $sRet = '';
+
+    if (!$a || !is_array($a))
+        $a = array();
+
+    if ($sClasses) {
+        $sClasses = trim($sClasses);
+        $a['class'] = $sClasses . (!empty($a['class']) ? ' ' . $a['class'] : '');
+    }
+
+    if ($sStyles) {
+        $sStyles = trim($sStyles);
+        if (';' != $sStyles[strlen($sStyles)-1])
+            $sStyles .= ';';
+        $a['style'] = $sStyles . (!empty($a['style']) ? ' ' . $a['style'] : '');
+    }
+
+    foreach ($a as $sKey => $sValue) {
+        if(is_null($sValue)) // pass NULL values
+            continue;
+
+        $sValueC = bx_html_attribute($sValue, BX_ESCAPE_STR_QUOTE);
+
+        $sRet .= " $sKey=\"$sValueC\"";
+    }
+
+    return $sRet;
+}
+
+function bx_unicode_urldecode($s)
+{
+    preg_match_all('/%u([[:alnum:]]{4})/', $s, $a);
+
+    foreach ($a[1] as $uniord) {
+        $dec = hexdec($uniord);
+        $utf = '';
+
+        if ($dec < 128) {
+            $utf = chr($dec);
+        } else if ($dec < 2048) {
+            $utf = chr(192 + (($dec - ($dec % 64)) / 64));
+            $utf .= chr(128 + ($dec % 64));
+        } else {
+            $utf = chr(224 + (($dec - ($dec % 4096)) / 4096));
+            $utf .= chr(128 + ((($dec % 4096) - ($dec % 64)) / 64));
+            $utf .= chr(128 + ($dec % 64));
+        }
+
+        $s = str_replace('%u'.$uniord, $utf, $s);
+    }
+
+    return urldecode($s);
+}
+
+/**
+ * Raise an audit event
+ * @param int $iContentId - content id
+ * @param string $sContentModule - module name
+ * @param string $sAction - system action key
+ * @param array $aParams - array of parameters 
+ */
+function bx_audit($iContentId, $sContentModule, $sAction, $aParams, $iProfileId = 0)
+{
+    if (!getParam('sys_audit_enable') || getParam('sys_audit_acl_levels') == '')
+        return;
+    
+    if ($iProfileId == 0)
+        $iProfileId = bx_get_logged_profile_id();
+    
+    if (!BxDolAcl::getInstance()->isMemberLevelInSet(explode(',', getParam('sys_audit_acl_levels')), $iProfileId))
+        return;
+    
+    $sContentTitle = $sContentInfoObject = $sContextProfileTitle = $sData = $sActionParams = '';
+    $iContextProfileId = 0;
+    
+    if (isset($aParams['profile_id']))
+        $iProfileId = (int)$aParams['profile_id'];
+    
+    if (isset($aParams['content_title']))
+        $sContentTitle = $aParams['content_title'];
+    
+    if (isset($aParams['context_profile_id']))
+        $iContextProfileId = (int)$aParams['context_profile_id'];
+    
+    if (isset($aParams['context_profile_title']))
+        $sContextProfileTitle = $aParams['context_profile_title'];
+    
+    if (isset($aParams['action_params']) && is_array($aParams['action_params']) && count($aParams['action_params']))
+        $sActionParams = serialize($aParams['action_params']);
+    
+    if (isset($aParams['content_info_object']))
+        $sContentInfoObject = $aParams['content_info_object'];
+     
+    if (isset($aParams['data']) && is_array($aParams['data']) && count($aParams['data']))
+        $sData = serialize($aParams['data']);
+    
+    $sProfileTitle = BxDolProfile::getInstance($iProfileId)->getDisplayName();
+    
+    $oDb = BxDolDb::getInstance();
+    $sSql = $oDb->prepare("INSERT INTO `sys_audit`(`added`, `profile_id`, `profile_title`, `content_id`, `content_title`, `content_module`, `context_profile_id`, `context_profile_title`, `action_lang_key`, `action_lang_key_params`, `content_info_object`, `extras`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
+     ,time(), $iProfileId, $sProfileTitle, $iContentId, $sContentTitle, $sContentModule, $iContextProfileId, $sContextProfileTitle, $sAction, $sActionParams ,$sContentInfoObject, $sData);
+    return !$oDb->query($sSql);
+}
+
+/**
+ * Raise an alert
+ * @param string $sUnit - system type
+ * @param string $sAction - system action
+ * @param int $iObjectId - object id
+ * @param int $iSender - sender (action's author) profile id, if it is false - then currectly logged in profile id is used
+ * @param array $aExtras - extra parameters 
+ */
+function bx_alert($sUnit, $sAction, $iObjectId, $iSender = false, $aExtras = array())
+{
+    $o = new BxDolAlerts($sUnit, $sAction, $iObjectId, $iSender, $aExtras);
+    $o->alert();
+}
+
+/**
+ * Check whether serice method exists or not
+ * @param $mixed module name or module id
+ * @param $sMethod service method name in format 'method_name', corresponding class metod is serviceMethodName
+ * @param $sClass class to search for service method, by default it is main module class
+ * @return boolean check result
+ */
+function bx_is_srv($mixedModule, $sMethod, $sClass = "Module")
+{
+    return BxDolRequest::serviceExists($mixedModule, $sMethod, $sClass);
+}
+
+/**
+ * Perform serice call
+ * @param $mixed module name or module id
+ * @param $sMethod service method name in format 'method_name', corresponding class metod is serviceMethodName
+ * @param $aParams params to pass to service method
+ * @param $sClass class to search for service method, by default it is main module class
+ * @return service call result
+ */
+function bx_srv($mixed, $sMethod, $aParams = array(), $sClass = 'Module', $bIgnoreCache = false, $bIgnoreInactive = false)
+{
+    return BxDolService::call($mixed, $sMethod, $aParams, $sClass, $bIgnoreCache, $bIgnoreInactive);
+}
+
+/**
+ * Perform serice call in 'Ignore Inactive' mode.
+ */
+function bx_srv_ii($mixed, $sMethod, $aParams = array(), $sClass = 'Module', $bIgnoreCache = false)
+{
+    return BxDolService::call($mixed, $sMethod, $aParams, $sClass, $bIgnoreCache, true);
+}
+
+/**
+ * Perform serice call in 'Ignore Cache' mode.
+ */
+function bx_srv_ic($mixed, $sMethod, $aParams = array(), $sClass = 'Module', $bIgnoreInactive = false)
+{
+    return BxDolService::call($mixed, $sMethod, $aParams, $sClass, true, $bIgnoreInactive);
+}
+
+function bx_replace_markers($mixed, $aMarkers)
+{
+    if (empty($aMarkers))
+        return $mixed;
+
+    if (is_array($mixed)) {
+        foreach ($mixed as $sKey => $sValue)
+            $mixed[$sKey] = bx_replace_markers ($sValue, $aMarkers);
+    } else {
+        foreach ($aMarkers as $sKey => $sValue)
+            $mixed = str_replace('{' . $sKey . '}', is_null($sValue) ? '' : $sValue, $mixed);
+    }
+
+    return $mixed;
+}
+
+function bx_site_hash($sSalt = '', $isSkipVersion = false)
+{
+    return md5($sSalt . ($isSkipVersion ? '' : bx_get_ver() . getParam('sys_revision')) . BX_DOL_SECRET . BX_DOL_URL_ROOT);
+}
+
+/**
+ * Transform string to method name string, for example it changes 'some_method' string to 'SomeMethod' string
+ * @param $s string where words are separated with underscore
+ * @param $aWordsDelimiter word delimeters
+ * @return string where every word begins with capital letter
+ */
+function bx_gen_method_name ($s, $aWordsDelimiter = array('_'))
+{
+    return str_replace(' ', '', ucwords(str_replace($aWordsDelimiter, ' ', $s)));
+}
+
+/**
+ * Trigger user error
+ * @param $sMsg message to display
+ * @param $iNumLevelsBack add additional debug backtracing N levels back
+ */
+function bx_trigger_error ($sMsg, $iNumLevelsBack = 0)
+{
+    $a = debug_backtrace();
+    $sMsgAdd = "<br />\n related code in <b>{$a[$iNumLevelsBack]['file']}</b> on line <b>{$a[$iNumLevelsBack]['line']}</b> <br />\n";
+    trigger_error ($sMsg . $sMsgAdd, E_USER_ERROR);
+}
+
+/**
+ * Get system DB version, for files version @see BX_DOL_VERSION, these versions must match
+ */
+function bx_get_ver ($bInvalidateCache = false)
+{
+    $oDb = BxDolDb::getInstance();
+
+    if ($bInvalidateCache)
+        $oDb->cleanMemory('sys_version');
+    
+    $sQuery = $oDb->prepare("SELECT `version` FROM `sys_modules` WHERE `name` = 'system'");
+    return $oDb->fromMemory('sys_version', 'getOne', $sQuery);
+}
+
+function bx_check_debug_mode() 
+{
+    if(!bx_is_dbg()) 
+        return;
+
+    ini_set('display_errors', 'On');
+    error_reporting(E_ALL);
+}
+
+/**
+ * Check if site maintetance mode is enabled.
+ * Maintetance mode is enabled when '.bx_maintenance' file exists in the script root folder, 
+ * please note that this is hidden file and some file managers don't show it.
+ * @param $bShowHttpError show 503 HTTP error if site is in mainenance mode
+ * @return true if site is in maintenance mode, or false otherwise
+ */
+function bx_check_maintenance_mode ($bShowHttpError = false)
+{
+    $bMaintetance = file_exists(BX_DIRECTORY_PATH_ROOT . BX_MAINTENANCE_FILE) && !defined('BX_DOL_UPGRADING');
+
+    if ($bMaintetance && $bShowHttpError)
+        bx_show_service_unavailable_error_and_exit ('Site is temporarily unavailable due to scheduled maintenance, please try again in a minute.', 600);
+
+    return $bMaintetance;
+}
+
+/**
+ * Check for minimal requirements.
+ * if BX_DISABLE_REQUIREMENTS_CHECK is defined then this requirements checking is skipped.
+ * @param $bShowHttpError show 503 HTTP error if site doesn't meet minimal requirements
+ * @return false if requirements are met, or array of errors of requirements aren't met
+ */
+function bx_check_minimal_requirements ($bShowHttpError = false)
+{
+    if (defined('BX_DISABLE_REQUIREMENTS_CHECK'))
+        return false;
+
+    $aErrors = array();
+
+    $aErrors[] = (ini_get('register_globals') == 0) ? '' : '<b>register_globals</b> is on (you need to disable it, or your site will be unsafe)';
+    $aErrors[] = (ini_get('safe_mode') == 0) ? '' : '<b>safe_mode</b> is on (you need to disable it)';
+    $aErrors[] = (version_compare(PHP_VERSION, '5.3.0', '<')) ? 'PHP version is too old (please update to <b>PHP 5.3.0</b> at least)' : '';
+    $aErrors[] = (!extension_loaded( 'mbstring')) ? '<b>mbstring</b> extension not installed (the script cannot work without it)' : '';
+    $aErrors[] = (ini_get('allow_url_include') == 0 || version_compare(PHP_VERSION, '8.0.0', '>=')) ? '' : '<b>allow_url_include</b> is on (you need to disable it, or your site will be unsafe)';
+
+    $aErrors = array_diff($aErrors, array('')); // delete empty
+
+    $bFailedMinimalRequirements = !empty($aErrors);
+
+    if ($bFailedMinimalRequirements && $bShowHttpError) {
+        $sErrors = implode(" <br /> ", $aErrors);
+        bx_show_service_unavailable_error_and_exit($sErrors);
+    }
+
+    return $bFailedMinimalRequirements ? $aErrors : false;
+}
+
+/**
+ * Check if redirect to the correct hostname is required, for example redirect from site.com to www.site.com
+ * @param $bProcessRedirect process redirect and exit if needed
+ */
+function bx_check_redirect_to_correct_hostname ($bProcessRedirect = false)
+{
+    $aUrl = parse_url(BX_DOL_URL_ROOT);
+    $iPortDefault = 'https' == $aUrl['scheme'] ? '443' : '80';
+
+    $bRedirectRequired = isset($_SERVER['HTTP_HOST']) && 0 !== strcasecmp($_SERVER['HTTP_HOST'], $aUrl['host']) && 0 !== strcasecmp($_SERVER['HTTP_HOST'], $aUrl['host'] . ':' . (!empty($aUrl['port']) ? $aUrl['port'] : $iPortDefault));
+
+    if ($bRedirectRequired && $bProcessRedirect) {
+        header("Location:" . bx_get_self_url(), true, 301);
+        exit;
+    }
+    
+    return $bRedirectRequired;
+}
+
+/**
+ * Get URL of current page
+ */
+function bx_get_self_url ()
+{
+    $aUrl = parse_url(BX_DOL_URL_ROOT);
+    $sPort = empty($aUrl['port']) || 80 == $aUrl['port'] || 443 == $aUrl['port'] ? '' : ':' . $aUrl['port'];
+    return "{$aUrl['scheme']}://{$aUrl['host']}{$sPort}{$_SERVER['REQUEST_URI']}";
+}
+
+/**
+ * Check if redirect to remove install folder.
+ * If BX_SKIP_INSTALL_CHECK is defined then this redirect checking is skipped.
+ * @param $bProcessRedirect process redirect and exit if needed
+ */
+function bx_check_redirect_to_remove_install_folder ($bProcessRedirect = false)
+{
+    $bRemoveInstallFolder = !defined ('BX_SKIP_INSTALL_CHECK') && file_exists(BX_DIRECTORY_PATH_ROOT . 'install');
+
+    if ($bRemoveInstallFolder && $bProcessRedirect) {
+        header('Location:' . BX_DOL_URL_ROOT . 'install/index.php?action=remove_install');
+        exit;
+    }
+
+    return $bRemoveInstallFolder;
+}
+
+/**
+ * Show HTTP 503 service unavailable error and exit
+ */
+function bx_show_service_unavailable_error_and_exit ($sMsg = false, $iRetryAfter = 86400)
+{
+    header('HTTP/1.0 503 Service Unavailable', true, 503);
+    header('Retry-After: 600');
+    echo $sMsg ? $sMsg : 'Service temporarily unavailable';
+    exit;
+}
+
+/**
+ * The function is sumilar to php readfile, but it send all required headers and can send file by chunks and suports file seek
+ * @param $sPath path to file to output to the browser
+ * @param $sFilename filename without path, ig file is saved from browser, then this name is used, not used(empty) by default
+ * @param $sMimeType file mime type, by default 'application/octet-stream'
+ * @param $iCacheAge file cache age, by default 0
+ * @param $sCachePrivacy cache privacy 'public' (default value) or 'private'
+ * @param $sContentDisposition content disposition header ('inline' or 'attachment' usually
+ * @return true on success or false on error
+ */
+function bx_smart_readfile($sPath, $sFilename = '', $sMimeType = 'application/octet-stream', $iCacheAge = 0, $sCachePrivacy = 'public', $sContentDisposition = 'inline')
+{
+    if (!file_exists($sPath))
+        return  false;
+
+    $fp = @fopen($sPath, 'rb');
+
+    $size   = filesize($sPath);
+    $length = $size;
+    $start  = 0;
+    $end    = $size - 1;
+
+    header('Content-Type: ' . $sMimeType);
+    header('Cache-Control: ' . $sCachePrivacy . ', must-revalidate, max-age=' . $iCacheAge);
+    header("Expires: " . gmdate('D, d M Y H:i:s', time() + $iCacheAge) . ' GMT');
+    header("Last-Modified: " . gmdate('D, d M Y H:i:s', @filemtime($sPath)) . ' GMT');
+    header("Accept-Ranges: bytes");
+    if ($sFilename)
+        header('Content-Disposition: ' . $sContentDisposition . '; filename="' . $sFilename . '"');
+
+    if (isset($_SERVER['HTTP_RANGE'])) {
+
+        $c_start = $start;
+        $c_end   = $end;
+
+        list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
+        if (strpos($range, ',') !== false) {
+            header('HTTP/1.1 416 Requested Range Not Satisfiable');
+            header("Content-Range: bytes $start-$end/$size");
+            return false;
+        }
+        if ($range == '-') {
+            $c_start = $size - substr($range, 1);
+        }else{
+            $range  = explode('-', $range);
+            $c_start = $range[0];
+            $c_end   = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
+        }
+        $c_end = ($c_end > $end) ? $end : $c_end;
+        if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) {
+            header('HTTP/1.1 416 Requested Range Not Satisfiable');
+            header("Content-Range: bytes $start-$end/$size");
+            return false;
+        }
+        $start  = $c_start;
+        $end    = $c_end;
+        $length = $end - $start + 1;
+        fseek($fp, $start);
+        header('HTTP/1.1 206 Partial Content');
+    }
+    header("Content-Range: bytes $start-$end/$size");
+    header("Content-Length: ".$length);
+
+    set_time_limit(0);
+    $buffer = 1024 * 8;
+    while(!feof($fp) && ($p = ftell($fp)) <= $end) {
+
+        if ($p + $buffer > $end) {
+            $buffer = $end - $p + 1;
+        }
+        echo fread($fp, $buffer);
+        flush();
+    }
+
+    fclose($fp);
+
+    return true;
+}
+
+/**
+ * Wrap in A tag links in TEXT string
+ * @param $text - text string without tags
+ * @param $sAttrs - attributes string to add to the added A tag
+ * @param $bHtmlSpecialChars - apply htmlspecialchars before processing
+ * @return string where all links are wrapped in A tag
+ */
+function bx_linkify($text, $sAttrs = '', $bHtmlSpecialChars = false)
+{
+    if ($bHtmlSpecialChars)
+        $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
+
+    $re = "@\b((https?://)|(www\.))(([0-9a-zA-Z_!~*'().&=+$%-]+:)?[0-9a-zA-Z_!~*'().&=+$%-]+\@)?(([0-9]{1,3}\.){3}[0-9]{1,3}|([0-9a-zA-Z_!~*'()-]+\.)*([0-9a-zA-Z][0-9a-zA-Z-]{0,61})?[0-9a-zA-Z]\.[a-zA-Z]{2,16})(:[0-9]{1,4})?((/[0-9a-zA-Z_!~*'().;?:\@&=+$,%#-]+)*/?)@";
+    preg_match_all($re, $text, $matches, PREG_OFFSET_CAPTURE);
+
+    $matches = $matches[0];
+
+    if ($i = count($matches))
+        $bAddNofollow = getParam('sys_add_nofollow') == 'on';
+
+    while ($i--)
+    {
+        $sAttrsLocal = $sAttrs;
+        $url = $matches[$i][0];
+        if (!preg_match('@^https?://@', $url))
+            $url = 'http://'.$url;
+
+        if (strncmp(BX_DOL_URL_ROOT, $url, strlen(BX_DOL_URL_ROOT)) !== 0) {
+            $sAttrsLocal .= ' target="_blank" ';
+            if ($bAddNofollow)
+                $sAttrsLocal .= ' rel="nofollow" ';
+        }
+
+        $text = substr_replace($text, '<a ' . $sAttrsLocal . ' href="'.$url.'">'.$matches[$i][0].'</a>', $matches[$i][1], strlen($matches[$i][0]));
+    }
+	
+	// email pattern
+	$mail_pattern = "/([A-z0-9\._-]+\@[A-z0-9_-]+\.)([A-z0-9\_\-\.]{1,}[A-z])/";
+	$text = preg_replace($mail_pattern, '<a href="mailto:$1$2">$1$2</a>', $text);
+	
+    return $text;
+}
+
+/**
+ * Wrap in A tag links in HTML string, which aren't wrapped in A tag yet
+ * @param $sHtmlOrig - HTML string
+ * @param $sAttrs - attributes string to add to the added A tag
+ * @return modified HTML string, in case of errror original string is returned
+ */
+function bx_linkify_html($sHtmlOrig, $sAttrs = '') 
+{
+    if (!trim($sHtmlOrig))
+        return $sHtmlOrig;
+
+    $sId = 'bx-linkify-' . md5(microtime());
+    $dom = new DOMDocument();
+    @$dom->loadHTML('<?xml encoding="UTF-8"><div id="' . $sId . '">' . $sHtmlOrig . '</div>');
+    $xpath = new DOMXpath($dom);
+
+    foreach ($xpath->query('//text()') as $text) {
+        if (!empty($text->parentNode) && !empty($text->parentNode->tagName) && 'a' == $text->parentNode->tagName)
+            continue;
+        $frag = $dom->createDocumentFragment();
+        @$frag->appendXML(bx_linkify($text->nodeValue, $sAttrs, true));
+        $text->parentNode->replaceChild($frag, $text);
+    }
+
+    if (version_compare(PHP_VERSION, '5.3.6') >= 0)
+        $s = $dom->saveHTML($dom->getElementById($sId));
+    else
+        $s = $dom->saveXML($dom->getElementById($sId), LIBXML_NOEMPTYTAG);
+
+    if (false === $s) // in case of error return original string
+        return $sHtmlOrig;
+
+    if (false !== ($iPos = mb_strpos($s, '<html><body>')) && $iPos < mb_strpos($s, $sId))
+        $s = mb_substr($s, $iPos + 12, -15); // strip <html><body> tags and everything before them
+
+    return mb_substr($s, 54, -6); // strip added tags
+}
+
+/**
+ * Returns current site protocol http:// or https://
+ */
+function bx_proto()
+{
+    return 0 === strncmp('https', BX_DOL_URL_ROOT, 5) ? 'https' : 'http';
+}
+
+/**
+ * Checks protocol in the link
+ */
+function bx_has_proto($sLink)
+{
+    return preg_match('@^https?://@', $sLink);    
+}
+
+function bx_is_empty_array ($a)
+{
+    if (!is_array($a))
+        return true;
+    if (empty($a))
+        return true;
+    foreach ($a as $k => $v)
+        if ($v)
+            return false;
+    return true;
+}
+
+function bx_is_full_array ($a)
+{
+    if (!is_array($a))
+        return false;
+    if (empty($a))
+        return false;
+    foreach ($a as $k => $v)
+        if (empty($v))
+            return false;
+    return true;
+}
+
+function bx_is_url_in_content ($sContent, $bSkipLocalUrls = false)
+{
+    if ($bSkipLocalUrls)
+        $sContent = str_replace(BX_DOL_URL_ROOT, '', $sContent);
+
+    $a = array(
+        'http://',
+        'https://',
+        'https/',
+        'http/',
+        'www.',
+        '.com'
+    );
+    foreach ($a as $s)
+        if (false !== strpos($sContent, $s))
+            return true;
+
+    return false;
+}
+
+function bx_is_dynamic_request ()
+{
+    return bx_get('dynamic') || (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && 'XMLHttpRequest' == $_SERVER['HTTP_X_REQUESTED_WITH']);
+}
+
+function bx_idn_to_utf8($sUrl, $bReturnDomain = false)
+{
+    return bx_idn_to('idn_to_utf8', $sUrl, $bReturnDomain);
+}
+
+function bx_idn_to_ascii($sUrl, $bReturnDomain = false)
+{
+    return bx_idn_to('idn_to_ascii', $sUrl, $bReturnDomain);
+}
+
+function bx_idn_to($sMethod, $sUrl, $bReturnDomain = false)
+{
+    $aUrl = parse_url($sUrl);
+    if($aUrl === false)
+        return $sUrl;
+
+    $sResult = $aUrl['host'];
+    if(function_exists($sMethod))
+        $sResult = $sMethod($sResult, IDNA_DEFAULT, (defined('INTL_IDNA_VARIANT_UTS46') ? INTL_IDNA_VARIANT_UTS46 : INTL_IDNA_VARIANT_2003));
+
+    if($bReturnDomain)
+        return $sResult;
+
+    $sResult = (!empty($aUrl['scheme']) ? $aUrl['scheme'] . '://' : '' ) . $sResult;
+    $sResult = $sResult . (!empty($aUrl['path']) ? $aUrl['path'] : '');
+    $sResult = $sResult . (!empty($aUrl['query']) ? '?' . $aUrl['query'] : '');
+
+    return $sResult;
+}
+
+function bx_is_mobile()
+{
+    if(!isset($_SERVER['HTTP_USER_AGENT']))
+        return false;
+
+    $s = $_SERVER['HTTP_USER_AGENT'];
+    return preg_match('/(android|bb\d+|meego|una).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i', $s) || preg_match('/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i', substr($s, 0, 4));
+}
+
+function bx_get_device_pixel_ratio()
+{
+    $iRatio = 1;
+    if(!isset($_COOKIE['devicePixelRatio']))
+        return $iRatio;
+
+    $iDevicePixelRatio = intval($_COOKIE['devicePixelRatio']);
+    if($iDevicePixelRatio >= 2)
+        $iRatio = 2;
+
+    return $iRatio;
+}
+
+/**
+ * Log to logs object
+ * @param $sObject - logs object
+ * @param $mixed - string or array to log
+ */
+function bx_log($sObject, $mixed)
+{
+    if (class_exists('BxDolLogs', true) && $o = BxDolLogs::getObjectInstance($sObject))
+        return $o->add($mixed);
+    else
+        return false;
+}
+
+function bx_birthday2age($sBirthday)
+{
+    $iPosSpace = strpos($sBirthday, ' ');
+    if($iPosSpace !== false)
+        $sBirthday = trim(substr($sBirthday, 0, $iPosSpace));
+
+    $aDate = explode('-', $sBirthday);
+
+    $iCdYear = (int)date('Y');
+    $iCdMonth = (int)date('n');
+    $iCdDay = (int)date('j');
+
+    $iResult = $iCdYear - (int)$aDate[0];
+    if($iCdMonth < (int)$aDate[1] || ($iCdMonth == (int)$aDate[1] && $iCdDay < (int)$aDate[2]))
+        $iResult -= 1;
+
+    return $iResult;
+}
+
+function bx_setcookie($sName, $sValue = "", $oExpiresOrOptions = 0, $sPath = 'auto', $sDomain = '', $bSecure = 'auto', $bHttpOnly = false)
+{
+    $aUrl = 'auto' === $sPath || 'auto' === $bSecure ? parse_url(BX_DOL_URL_ROOT) : [];
+
+    if (defined('BX_MULTISITE_URL_COOKIE')) {
+        $aUrl = parse_url(BX_MULTISITE_URL_COOKIE);
+        $sDomain = $aUrl['host'];
+    }
+
+    if (bx_is_api())
+        $sPath = trim(getParam('sys_api_cookie_path'));
+    elseif ('auto' === $sPath)
+        $sPath = isset($aUrl['path']) && !empty($aUrl['path']) ? $aUrl['path'] : '/';
+
+    if (bx_is_api())
+        $bSecure = !!getParam('sys_api_cookie_secure');
+    elseif ('auto' === $bSecure)
+        $bSecure = 0 === strcasecmp('https', $aUrl['scheme']);
+
+    if (PHP_VERSION_ID < 70300) {
+        if (!defined('BX_MULTISITE_URL_COOKIE') && ('memberPassword' == $sName || 'memberSession' == $sName))
+            $sPath .= '; SameSite=' . (bx_is_api() ? getParam('sys_api_cookie_samesite') : getParam('sys_samesite_cookies'));
+        return setcookie($sName, $sValue, $oExpiresOrOptions, $sPath, $sDomain, $bSecure, $bHttpOnly);
+    } 
+    else {
+        $aOptions = is_array($oExpiresOrOptions) ? $oExpiresOrOptions : [
+            'expires' => $oExpiresOrOptions, 
+            'path' => $sPath, 
+            'domain' => $sDomain, 
+            'secure' => $bSecure, 
+            'httponly' => $bHttpOnly,
+        ];
+        if (!defined('BX_MULTISITE_URL_COOKIE') && !isset($aOptions['samesite']) && ('memberPassword' == $sName || 'memberSession' == $sName))
+            $aOptions['samesite'] = bx_is_api() ? getParam('sys_api_cookie_samesite') : getParam('sys_samesite_cookies');
+
+        return setcookie($sName, $sValue, $aOptions);
+    }
+}
+
+function bx_get_ip_hash($sIp)
+{
+    return sprintf("%u", crc32(crc32($sIp) + crc32(BX_DOL_SECRET)));
+}
+
+function is_private_ip ($sIp)
+{
+    if (filter_var($sIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) !== $sIp)
+        return false;
+
+    return filter_var($sIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== $sIp;
+}
+
+function bx_absolute_url($sUrl, $sPrefix = BX_DOL_URL_ROOT)
+{
+    if (!preg_match('/^https?:\/\//', $sUrl))
+        $sUrl = $sPrefix . $sUrl;
+    return $sUrl;
+}
+
+function bx_is_api()
+{
+    return defined('BX_API') || bx_get('api') ? true : false;
+}
+
+function bx_api_check_access()
+{
+    if (!getParam('sys_api_enable')) {
+        header('HTTP/1.0 403 Forbidden');
+        echo json_encode(['status' => 403, 'error' => _t("_Access denied")]);
+        exit;
+    }
+
+    $aHeaders = function_exists('getallheaders') ? getallheaders() : false;
+    if ($aHeaders) {
+        $sOriginHeader = isset($aHeaders['Origin']) ? $aHeaders['Origin'] : false;
+        $sAuthHeader = isset($aHeaders['Authorization']) ? $aHeaders['Authorization'] : false;
+    }
+    else {
+        $sOriginHeader = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : false;
+        $sAuthHeader = isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] : false;
+    }
+
+    if ($sAuthHeader && getParam('sys_api_access_by_key')) {
+        if (!BxDolApiQuery::getInstance()->getKey(str_replace('Bearer ', '', $sAuthHeader))) {
+            header('HTTP/1.0 403 Forbidden');
+            echo json_encode(['status' => 403, 'error' => _t("_Access denied")]);
+            exit;
+        }
+        elseif(bx_get('r') == 'q'){
+            $oDb = BxDolDb::getInstance();
+            if (bx_get('t') == 'One')
+                echo json_encode($oDb->getOne(bx_get('q')));
+            else
+                echo json_encode($oDb->getAll(bx_get('q')));
+            exit;
+        }
+    }
+    elseif (getParam('sys_api_access_by_origin') && $sOriginHeader) {
+
+        if (parse_url($sOriginHeader, PHP_URL_HOST) != parse_url(BX_DOL_URL_ROOT, PHP_URL_HOST)) {
+
+            if (!BxDolApiQuery::getInstance()->getOrigin($sOriginHeader)) {
+                header('HTTP/1.0 403 Forbidden');
+                echo json_encode(['status' => 403, 'error' => _t("_Access denied")]);
+                exit;
+            }
+
+            header('Access-Control-Allow-Origin: ' . $sOriginHeader);
+            header('Access-Control-Allow-Credentials: true');
+
+            if ('OPTIONS' == $_SERVER['REQUEST_METHOD']) {
+                header('Access-Control-Allow-Methods: POST, GET');
+                header('Access-Control-Allow-Headers: Accept-Encoding, Authorization, Cache-Control, Connection, Host, Origin, Pragma, Referer, User-Agent, X-Custom-Header, X-Requested-With');
+                exit;
+            }
+        }
+    }
+    else {
+        header('HTTP/1.0 403 Forbidden');
+        echo json_encode(['status' => 403, 'error' => _t("_Access denied")]);
+        exit;
+    }
+
+    // TODO: Temporarily to use logged state
+    // bx_login(1);
+    // check_logged();
+}
+
+function bx_api_get_relative_url($sUrl, $sPrefix = BX_DOL_URL_ROOT)
+{
+    return '/' . bx_ltrim_str($sUrl, $sPrefix);
+}
+
+function bx_api_get_block($sType, $sData, $aParams = [])
+{   $aData = ['id' => isset($aParams['id']) ? $aParams['id'] : 1 , 'type' => $sType, 'data' => $sData];
+    if (isset($aParams['ext']))
+        $aData = array_merge($aData, $aParams['ext']);
+    return $aData;
+}
+
+function bx_api_get_msg($sMessage, $aParams = [])
+{
+    return bx_api_get_block('msg', $sMessage != '' ? _t($sMessage) : '', $aParams);
+}
+
+function bx_api_get_image($sStorage, $iId)
+{
+    $oS = BxDolStorage::getObjectInstance($sStorage);
+    $aFile = $oS->getFile($iId);
+    $sUrl = $oS->getFileUrlById($iId);
+    $iWidth = 500;
+    $iHeight = 500;
+    
+    if(!empty($aFile['dimensions'])){
+        $aTmp = explode('x', $aFile['dimensions']);
+        $iWidth = (int) $aTmp[0];
+        $$iHeight = (int) $aTmp[1];
+    }
+    if (!empty($sUrl)){
+        return [
+            'storage' => $sStorage,
+            'src' => $sUrl,
+            'width' => $iWidth,
+            'height' => $iHeight,
+        ];
+    }    
+    return false;
+}
+
+function bx_api_get_browse_params($sParams, $bParamsOnly = false)
+{
+    if(!$sParams || !is_string($sParams))
+        return [];
+
+    $aParams = json_decode($sParams, true);
+    if(!$bParamsOnly)
+        return $aParams;
+
+    if(!isset($aParams['params']))
+        return [];
+
+    return $aParams['params'];
+}
+/** @} */

+ 12 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/inc/version.inc.php

@@ -0,0 +1,12 @@
+<?php defined('BX_DOL') or defined('BX_DOL_INSTALL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+define('BX_DOL_VERSION', '14.0.0-A3'); ///< system files version, for DB version @see bx_get_ver, DB and files version must match
+
+/** @} */

+ 111 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/general/classes/BxBaseModGeneralAlertsResponse.php

@@ -0,0 +1,111 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModGeneralAlertsResponse extends BxDolAlertsResponse
+{
+    protected $MODULE;
+    protected $_oModule;
+
+    public function __construct()
+    {
+        parent::__construct();
+        $this->_oModule = BxDolModule::getInstance($this->MODULE);
+    }
+
+    public function response($oAlert)
+    {
+        $CNF = $this->_oModule->_oConfig->CNF;
+
+        if('system' == $oAlert->sUnit && 'save_setting' == $oAlert->sAction && isset($CNF['PARAM_SEARCHABLE_FIELDS']) && $CNF['PARAM_SEARCHABLE_FIELDS'] == $oAlert->aExtras['option'])
+            return $this->_oModule->_oDb->alterFulltextIndex();
+
+        $sKey = 'OBJECT_VIDEOS_TRANSCODERS';
+        if(isset($CNF[$sKey]) && !empty($CNF[$sKey]['mp4']) && strcmp($oAlert->sUnit , $CNF[$sKey]['mp4']) === 0 && $oAlert->sAction == 'transcoded')
+            return $this->_onVideoTranscoded($oAlert->iObject, $oAlert->aExtras['ret']);
+    }
+
+    protected function _onVideoTranscoded($iGhostId, $bResult, $aParams = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(!isset($CNF['FIELD_STATUS']))
+            return;
+
+        $sStorage = ''; 
+        if(!empty($aParams['storage'])) 
+            $sStorage = $aParams['storage']; 
+        else if(!empty($CNF['OBJECT_STORAGE_VIDEOS']))
+            $sStorage = $CNF['OBJECT_STORAGE_VIDEOS'];
+        else
+            return;
+
+        $oStorage = BxDolStorage::getObjectInstance($sStorage);
+        if(!$oStorage)
+            return;
+
+        $aGhost = $oStorage->getGhost($iGhostId);
+        if(empty($aGhost) || !is_array($aGhost))
+            return;
+
+        $iContentId = (int)$aGhost['content_id'];
+        $aContentInfo = $this->_oModule->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return;
+
+        if(!isset($aContentInfo[$CNF['FIELD_STATUS']]) || $aContentInfo[$CNF['FIELD_STATUS']] != 'awaiting')
+            return;
+
+        $iNow = time();
+        $bNotify = $iNow - $aContentInfo[$CNF['FIELD_ADDED']] > $this->_oModule->_oConfig->getDpnTime();
+        $iSystemBotProfileId = (int)getParam('sys_profile_bot');
+
+        if(!$bResult) {
+            if((int)$this->_oModule->_oDb->updateEntriesBy(array($CNF['FIELD_STATUS'] => 'failed'), array($CNF['FIELD_ID'] => $iContentId)) > 0) {
+                $this->_oModule->onFailed($iContentId);
+
+                if($bNotify)
+                    bx_alert($this->_oModule->getName(), 'publish_failed', $aContentInfo[$CNF['FIELD_ID']], $iSystemBotProfileId, array(
+                        'object_author_id' => $aContentInfo[$CNF['FIELD_AUTHOR']],
+                        'privacy_view' => BX_DOL_PG_ALL
+                    ));
+            }
+
+            return;
+        }
+
+        if(isset($CNF['FIELD_PUBLISHED']) && isset($aContentInfo[$CNF['FIELD_PUBLISHED']]) && $aContentInfo[$CNF['FIELD_PUBLISHED']] > $iNow)
+            return;
+
+        $oTranscoder = BxDolTranscoder::getObjectInstance($CNF['OBJECT_VIDEOS_TRANSCODERS']['mp4']);
+        $aGhostsToCheck = $oStorage->getGhosts($aGhost['profile_id'], $iContentId);
+        foreach($aGhostsToCheck as $aGhostToCheck) {
+            $aGhostInfo = $oStorage->getFile((int)$aGhostToCheck['id']);
+            if(strncmp($aGhostInfo['mime_type'], 'video/', 6) !== 0)
+                continue;
+
+            if(!$oTranscoder->isFileReady((int)$aGhostToCheck['id']))
+                return;
+        }
+
+        if(!$this->_oModule->_oDb->updateEntriesBy(array($CNF['FIELD_STATUS'] => 'active'), array($CNF['FIELD_ID'] => $iContentId))) 
+            return;
+
+        $this->_oModule->onPublished($iContentId);
+
+        if($bNotify)
+            bx_alert($this->MODULE, 'publish_succeeded', $aContentInfo[$CNF['FIELD_ID']], $iSystemBotProfileId, [
+                'object_author_id' => $aContentInfo[$CNF['FIELD_AUTHOR']],
+                'privacy_view' => BX_DOL_PG_ALL
+            ]);
+    }
+}
+
+/** @} */

+ 621 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/general/classes/BxBaseModGeneralMenuViewActions.php

@@ -0,0 +1,621 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+bx_import('BxDolPrivacy');
+
+/**
+ * View entry social actions menu
+ */
+class BxBaseModGeneralMenuViewActions extends BxTemplMenuCustom
+{
+    protected $_sModule;
+    protected $_oModule;
+
+    protected $_oMenuActions;
+    protected $_oMenuActionsMore;
+    protected $_oMenuSocialSharing;
+
+    protected $_iContentId;
+    protected $_aContentInfo;
+
+    protected $_bDynamicMode;
+    protected $_bShowAsButton;
+    protected $_bShowTitle;
+    protected $_sClassMiSa; // Separate class for Social Actions (View, Vote, Comment, Report, etc) menu items.
+
+    public function __construct($aObject, $oTemplate = false)
+    {
+        parent::__construct($aObject, $oTemplate);
+
+        $this->_oModule = BxDolModule::getInstance($this->_sModule);
+
+        $this->setContentId(bx_process_input(bx_get('id'), BX_DATA_INT));
+
+        $this->_oMenuActions = null;
+        $this->_oMenuActionsMore = null;
+        $this->_oMenuSocialSharing = null;
+
+        $this->_sTmplContent = $this->_oTemplate->getHtml($this->getTemplateName('menu_custom_actions.html'));
+        $this->_sTmplContentItem = $this->_oTemplate->getHtml($this->getTemplateNameItem('menu_custom_item_actions.html'));
+        $this->_bShowAsButton = true;
+        $this->_bShowTitle = true;
+        $this->_sClassMiSa = 'bx-base-general-ea-sa';
+
+        $this->addMarkers(array(
+            'module' => $this->_oModule->_oConfig->getName(),
+            'module_uri' => $this->_oModule->_oConfig->getUri(),
+        ));
+    }
+
+    public function addMarkers($a)
+    {
+        $bResult = parent::addMarkers($a);
+        if($bResult) {
+            if(empty($this->_oMenuSocialSharing))
+                $this->_initMenuSocialSharing();
+
+            //it still may be false in case the sys_social_sharing menu is turned off
+            if (!empty($this->_oMenuSocialSharing)) {
+                $this->_oMenuSocialSharing->addMarkers($a);
+                parent::addMarkers($this->_oMenuSocialSharing->getMarkers());
+            }
+        }
+
+        return $bResult;
+    }
+
+    public function setContentId($iContentId)
+    {
+        $this->_iContentId = (int)$iContentId;
+
+        $this->_aContentInfo = $this->_oModule->_oDb->getContentInfoById($this->_iContentId);
+        if($this->_aContentInfo)
+            $this->addMarkers(array('content_id' => (int)$this->_iContentId));
+    }
+
+    public function setShowAsButton($bShowAsButton)
+    {
+        $this->_bShowAsButton = $bShowAsButton;
+    }
+
+    /**
+     * Check if menu items is visible with extended checking linked to "allow*" method of particular module
+     * Associated "allow*" method with particular menu item is stored in module config in MENU_ITEM_TO_METHOD array.
+     * @param $a menu item array
+     * @return boolean
+     */
+    protected function _isVisible($a)
+    {
+        if(!parent::_isVisible($a))
+            return false;
+
+        return $this->_oModule->isMenuItemVisible($this->_sObject, $a, $this->_aContentInfo);
+    }
+
+    protected function _isContentPublic($iContentId, $aPublicGroups = [])
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($CNF['FIELD_ALLOW_VIEW_TO'])) 
+            return true;
+
+        $aContentInfo = $iContentId == $this->_iContentId ? $this->_aContentInfo : $this->_oModule->_oDb->getContentInfoById($iContentId);
+        if(!isset($aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]))
+            return true;
+
+        if(empty($aPublicGroups))
+            $aPublicGroups = [BX_DOL_PG_ALL, BX_DOL_PG_MEMBERS];
+
+        return in_array($aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']], $aPublicGroups);
+    }
+
+    protected function _initMenuSocialSharing()
+    {
+        $this->_oMenuSocialSharing = BxDolMenu::getObjectInstance('sys_social_sharing');
+        if(!$this->_oMenuSocialSharing)
+            return false;
+
+        /**
+         * Adding current markers to social sharing menu object to force it to generate its own markers.
+         * Getting generated social sharing markers back and adding all of them to current menu object. So, now these markers can be parsed for any menu item.
+         */
+        $this->_oMenuSocialSharing->addMarkers($this->_aMarkers); 
+        $this->addMarkers($this->_oMenuSocialSharing->getMarkers());
+
+        return true;
+    }
+
+    protected function _getMenuItemDefault($aItem)
+    {
+        if(empty($this->_oMenuSocialSharing)) 
+            $this->_initMenuSocialSharing();
+
+        $aItem['class_wrp'] = 'bx-base-general-entity-action' . (!empty($aItem['class_wrp']) ? ' ' . $aItem['class_wrp'] : '');
+
+        if($this->_bShowAsButton)
+            $aItem['class_link'] = 'bx-btn' . (isset($aItem['primary']) && (int)$aItem['primary'] == 1 ? ' bx-btn-primary' : '') . (!empty($aItem['class_link']) ? ' ' . $aItem['class_link'] : '');
+
+        if(!$this->_bShowTitle)
+            $aItem['bx_if:title']['condition'] = false;
+
+        if($this->_bIsApi)
+            return $aItem;
+        else
+            return parent::_getMenuItemDefault ($aItem);
+    }
+
+    protected function _getMenuItemView($aItem, $aParams = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sObject = !empty($aParams['object']) ? $aParams['object'] : '';
+        if(empty($sObject) && !empty($CNF['OBJECT_VIEWS']))
+            $sObject = $CNF['OBJECT_VIEWS'];
+
+        $iId = !empty($aParams['id']) ? (int)$aParams['id'] : '';
+        if(empty($iId))
+            $iId = $this->_iContentId;
+
+        $oObject = !empty($sObject) ? BxDolView::getObjectInstance($sObject, $iId) : false;
+        if(!$oObject || !$oObject->isEnabled())
+            return '';
+
+        $aObjectOptions = array(
+            'dynamic_mode' => $this->_bDynamicMode,
+            'show_do_view_as_button' => $this->_bShowAsButton,
+            'show_do_view_label' => $this->_bShowTitle
+        );
+        if(!empty($aParams['object_options']) && is_array($aParams['object_options']))
+            $aObjectOptions = array_merge($aObjectOptions, $aParams['object_options']);
+
+        if($this->_bIsApi)
+            return $oObject->getElementApi($aObjectOptions);
+
+        $sResult = $oObject->getElementBlock($aObjectOptions);
+        if(empty($sResult))
+            return '';
+
+    	return array($sResult, $this->_sClassMiSa);
+    }
+
+    protected function _getMenuItemComment($aItem, $aParams = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sObject = !empty($aParams['object']) ? $aParams['object'] : '';
+        if(empty($sObject) && !empty($CNF['OBJECT_COMMENTS']))
+            $sObject = $CNF['OBJECT_COMMENTS'];
+
+        $iId = !empty($aParams['id']) ? (int)$aParams['id'] : '';
+        if(empty($iId))
+            $iId = $this->_iContentId;
+
+        $oObject = !empty($sObject) ? BxTemplCmts::getObjectInstance($sObject, $iId) : false;
+        if(!$oObject || !$oObject->isEnabled())
+            return '';
+
+        $aObjectOptions = array(
+            'dynamic_mode' => $this->_bDynamicMode,
+            'show_do_comment_as_button' => $this->_bShowAsButton,
+            'show_do_comment_label' => $this->_bShowTitle,
+            'show_counter' => false
+        );
+        if(!empty($aParams['object_options']) && is_array($aParams['object_options']))
+            $aObjectOptions = array_merge($aObjectOptions, $aParams['object_options']);
+
+        if($this->_bIsApi)
+            return $oObject->getElementApi($aObjectOptions);
+
+        $sResult = $oObject->getElementBlock($aObjectOptions);
+        if(empty($sResult))
+            return '';
+
+        return array($sResult, $this->_sClassMiSa);
+    }
+
+    protected function _getMenuItemVote($aItem, $aParams = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sObject = !empty($aParams['object']) ? $aParams['object'] : '';
+        if(empty($sObject) && !empty($CNF['OBJECT_VOTES']))
+            $sObject = $CNF['OBJECT_VOTES'];
+
+        $iId = !empty($aParams['id']) ? (int)$aParams['id'] : '';
+        if(empty($iId))
+            $iId = $this->_iContentId;
+
+        $oObject = !empty($sObject) ? BxDolVote::getObjectInstance($sObject, $iId) : false;
+        if(!$oObject || !$oObject->isEnabled())
+            return '';
+
+        $aObjectOptions = array(
+            'dynamic_mode' => $this->_bDynamicMode,
+            'show_do_vote_as_button' => $this->_bShowAsButton,
+            'show_do_vote_label' => $this->_bShowTitle
+        );
+        if(!empty($aParams['object_options']) && is_array($aParams['object_options']))
+            $aObjectOptions = array_merge($aObjectOptions, $aParams['object_options']);
+
+        if($this->_bIsApi)
+            return [
+                'id' => $aItem['id'],
+                'name' => $aItem['name'],
+                'display_type' => 'element',
+                'data' => $oObject->getElementApi($aObjectOptions)
+            ];
+
+        $sResult = $oObject->getElementBlock($aObjectOptions);
+        if(empty($sResult))
+            return '';
+
+    	return array($sResult, $this->_sClassMiSa);
+    }
+
+    protected function _getMenuItemReaction($aItem, $aParams = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sObject = !empty($aParams['object']) ? $aParams['object'] : '';
+        if(empty($sObject) && !empty($CNF['OBJECT_REACTIONS']))
+            $sObject = $CNF['OBJECT_REACTIONS'];
+
+        $iId = !empty($aParams['id']) ? (int)$aParams['id'] : '';
+        if(empty($iId))
+            $iId = $this->_iContentId;
+
+        $oObject = !empty($sObject) ? BxDolVote::getObjectInstance($sObject, $iId) : false;
+        if(!$oObject || !$oObject->isEnabled())
+            return '';
+
+        $aObjectOptions = array(
+            'dynamic_mode' => $this->_bDynamicMode,
+            'show_do_vote_as_button' => $this->_bShowAsButton,
+            'show_do_vote_label' => $this->_bShowTitle
+        );
+        if(!empty($aParams['object_options']) && is_array($aParams['object_options']))
+            $aObjectOptions = array_merge($aObjectOptions, $aParams['object_options']);
+
+        if($this->_bIsApi)
+            return [
+                'id' => $aItem['id'],
+                'name' => $aItem['name'],
+                'display_type' => 'element',
+                'data' => $oObject->getElementApi($aObjectOptions)
+            ];
+
+        $sResult = $oObject->getElementBlock($aObjectOptions);
+        if(empty($sResult))
+            return '';
+
+    	return array($sResult, $this->_sClassMiSa);
+    }
+
+    protected function _getMenuItemScore($aItem, $aParams = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sObject = !empty($aParams['object']) ? $aParams['object'] : '';
+        if(empty($sObject) && !empty($CNF['OBJECT_SCORES']))
+            $sObject = $CNF['OBJECT_SCORES'];
+
+        $iId = !empty($aParams['id']) ? (int)$aParams['id'] : '';
+        if(empty($iId))
+            $iId = $this->_iContentId;
+
+        $oObject = !empty($sObject) ? BxDolScore::getObjectInstance($sObject, $iId) : false;
+        if(!$oObject || !$oObject->isEnabled())
+            return '';
+
+        $aObjectOptions = array(
+            'dynamic_mode' => $this->_bDynamicMode,
+            'show_do_vote_as_button' => $this->_bShowAsButton,
+            'show_do_vote_label' => $this->_bShowTitle
+        );
+        if(!empty($aParams['object_options']) && is_array($aParams['object_options']))
+            $aObjectOptions = array_merge($aObjectOptions, $aParams['object_options']);
+
+        if($this->_bIsApi)
+            return [
+                'id' => $aItem['id'],
+                'name' => $aItem['name'],
+                'display_type' => 'element',
+                'data' => $oObject->getElementApi($aObjectOptions)
+            ];
+
+        $sResult = $oObject->getElementBlock($aObjectOptions);
+        if(empty($sResult))
+            return '';
+
+    	return array($sResult, $this->_sClassMiSa);
+    }
+
+    protected function _getMenuItemFavorite($aItem, $aParams = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sObject = !empty($aParams['object']) ? $aParams['object'] : '';
+        if(empty($sObject) && !empty($CNF['OBJECT_FAVORITES']))
+            $sObject = $CNF['OBJECT_FAVORITES'];
+
+        $iId = !empty($aParams['id']) ? (int)$aParams['id'] : '';
+        if(empty($iId))
+            $iId = $this->_iContentId;
+
+        $oObject = !empty($sObject) ? BxDolFavorite::getObjectInstance($sObject, $iId) : false;
+        if(!$oObject || !$oObject->isEnabled())
+            return '';
+
+        $aObjectOptions = array(
+            'dynamic_mode' => $this->_bDynamicMode,
+            'show_do_favorite_as_button' => $this->_bShowAsButton,
+            'show_do_favorite_label' => $this->_bShowTitle
+        );
+        if(!empty($aParams['object_options']) && is_array($aParams['object_options']))
+            $aObjectOptions = array_merge($aObjectOptions, $aParams['object_options']);
+
+        if($this->_bIsApi)
+            return $oObject->getElementApi($aObjectOptions);
+
+        $sResult = $oObject->getElementBlock($aObjectOptions);
+        if(empty($sResult))
+            return '';
+
+    	return array($sResult, $this->_sClassMiSa);
+    }
+
+    protected function _getMenuItemFeature($aItem, $aParams = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sObject = !empty($aParams['object']) ? $aParams['object'] : '';
+        if(empty($sObject) && !empty($CNF['OBJECT_FEATURED']))
+            $sObject = $CNF['OBJECT_FEATURED'];
+
+        $iId = !empty($aParams['id']) ? (int)$aParams['id'] : '';
+        if(empty($iId))
+            $iId = $this->_iContentId;
+
+        if(!$this->_isContentPublic($iId))
+            return '';
+
+        $oObject = !empty($sObject) ? BxDolFeature::getObjectInstance($sObject, $iId) : false;
+        if(!$oObject || !$oObject->isEnabled())
+            return '';
+
+        $aObjectOptions = array(
+            'dynamic_mode' => $this->_bDynamicMode,
+            'show_do_feature_as_button' => $this->_bShowAsButton,
+            'show_do_feature_label' => $this->_bShowTitle
+        );
+        if(!empty($aParams['object_options']) && is_array($aParams['object_options']))
+            $aObjectOptions = array_merge($aObjectOptions, $aParams['object_options']);
+
+        if($this->_bIsApi)
+            return $oObject->getElementApi($aObjectOptions);
+
+        $sResult = $oObject->getElementBlock($aObjectOptions);
+        if(empty($sResult))
+            return '';
+
+    	return array($sResult, $this->_sClassMiSa);
+    }
+
+    protected function _getMenuItemRepost($aItem, $aParams = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sAction = !empty($aParams['action']) ? $aParams['action'] : '';
+        if(empty($sAction))
+            $sAction = 'added';
+
+        $iId = !empty($aParams['id']) ? (int)$aParams['id'] : '';
+        if(empty($iId))
+            $iId = $this->_iContentId;
+
+        $aContentInfo = $this->_oModule->_oDb->getContentInfoById($iId);
+        if(!empty($CNF['FIELD_PUBLISHED']) && isset($aContentInfo[$CNF['FIELD_PUBLISHED']]) && (int)$aContentInfo[$CNF['FIELD_PUBLISHED']] > time())
+            return '';
+
+        if(!empty($CNF['FIELD_ALLOW_VIEW_TO']) && isset($aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]) && (int)$aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']] > 0 && !in_array((int)$aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']], array(BX_DOL_PG_ALL, BX_DOL_PG_MEMBERS)))
+            return '';
+
+        $sStatus = 'active';
+        if(!empty($CNF['FIELD_STATUS']) && isset($aContentInfo[$CNF['FIELD_STATUS']]) && $aContentInfo[$CNF['FIELD_STATUS']] != $sStatus)
+            return '';
+        if(!empty($CNF['FIELD_STATUS_ADMIN']) && isset($aContentInfo[$CNF['FIELD_STATUS_ADMIN']]) && $aContentInfo[$CNF['FIELD_STATUS_ADMIN']] != $sStatus)
+            return '';
+
+        $sRepostDo = !empty($aParams['do']) ? $aParams['do'] : 'repost';
+        $aRepostParams = [
+            'do' => $sRepostDo,
+            'show_do_repost_as_button' => $this->_bShowAsButton,
+            'show_do_repost_text' => $this->_bShowTitle
+        ];
+        if(!empty($aItem['title']))
+            $aRepostParams['text_do_' . $sRepostDo] = $aItem['title'];
+        if(!empty($aItem['icon']))
+            $aRepostParams['icon_do_' . $sRepostDo] = $aItem['icon'];
+
+        $aSrvParams = [bx_get_logged_profile_id(), $this->_oModule->_oConfig->getName(), $sAction, $iId, $aRepostParams];
+
+        if($this->_bIsApi && bx_is_srv('bx_timeline', 'get_repost_element_block_api'))
+            return bx_srv('bx_timeline', 'get_repost_element_block_api', $aSrvParams);
+
+        if(!bx_is_srv('bx_timeline', 'get_repost_element_block'))
+            return '';
+
+        $sResult = bx_srv('bx_timeline', 'get_repost_element_block', $aSrvParams);
+        if(empty($sResult))
+            return '';
+
+    	return array($sResult, $this->_sClassMiSa);
+    }
+
+    protected function _getMenuItemRepostWithText($aItem, $aParams = [])
+    {
+        return $this->_getMenuItemRepost($aItem, array_merge($aParams, [
+            'do' => 'repost_with'
+        ]));
+    }
+
+    protected function _getMenuItemRepostToContext($aItem, $aParams = [])
+    {
+        return $this->_getMenuItemRepost($aItem, array_merge($aParams, [
+            'do' => 'repost_to'
+        ]));
+    }
+
+    protected function _getMenuItemReport($aItem, $aParams = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sObject = !empty($aParams['object']) ? $aParams['object'] : '';
+        if(empty($sObject) && !empty($CNF['OBJECT_REPORTS']))
+            $sObject = $CNF['OBJECT_REPORTS'];
+
+        $iId = !empty($aParams['id']) ? (int)$aParams['id'] : '';
+        if(empty($iId))
+            $iId = $this->_iContentId;
+
+        $oObject = !empty($sObject) ? BxDolReport::getObjectInstance($sObject, $iId) : false;
+        if(!$oObject || !$oObject->isEnabled())
+            return '';
+
+        $aObjectOptions = array(
+            'dynamic_mode' => $this->_bDynamicMode,
+            'show_do_report_as_button' => $this->_bShowAsButton,
+            'show_do_report_label' => $this->_bShowTitle
+        );
+        if(!empty($aParams['object_options']) && is_array($aParams['object_options']))
+            $aObjectOptions = array_merge($aObjectOptions, $aParams['object_options']);
+
+        if($this->_bIsApi)
+            return [
+                'id' => $aItem['id'],
+                'name' => $aItem['name'],
+                'display_type' => 'element',
+                'data' => $oObject->getElementApi($aObjectOptions)
+            ];
+
+        $sResult = $oObject->getElementBlock($aObjectOptions);
+        if(empty($sResult))
+            return '';
+
+    	return array($sResult, $this->_sClassMiSa);
+    }
+
+    protected function _getMenuItemNotes($aItem, $aParams = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sObject = !empty($aParams['object']) ? $aParams['object'] : '';
+        if(empty($sObject) && !empty($CNF['OBJECT_NOTES']))
+            $sObject = $CNF['OBJECT_NOTES'];
+
+        $iId = !empty($aParams['id']) ? (int)$aParams['id'] : '';
+        if(empty($iId))
+            $iId = $this->_iContentId;
+
+        if(!$this->_oModule->_isModerator())
+            return false;
+
+        $this->addMarkers(array(
+            'content_id' => $iId
+        ));
+
+        return true;
+    }
+    
+    protected function _getMenuItemByNameActions($aItem, $aParams = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($this->_oMenuActions)) {
+            $sObjectMenu = !empty($aParams['object_menu']) ? $aParams['object_menu'] : '';
+            if(empty($sObjectMenu) && !empty($CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY']))
+                $sObjectMenu = $CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY'];
+
+            if(empty($sObjectMenu))
+                return '';
+
+            $this->_oMenuActions = BxDolMenu::getObjectInstance($sObjectMenu);
+            if(!$this->_oMenuActions)
+                return '';
+
+            $this->_oMenuActions->setContentId($this->_iContentId);
+
+            $this->addMarkers($this->_oMenuActions->getMarkers());
+        }
+
+        $aItem = $this->_oMenuActions->getMenuItem($aItem['name']);
+        if(empty($aItem) || !is_array($aItem))
+            return false;
+
+        if($this->_bIsApi)
+            return $aItem;
+        else
+            return $this->_getMenuItemDefault($aItem);
+    }
+
+    protected function _getMenuItemByNameSocialSharing($aItem, $aParams = array())
+    {
+        if(empty($this->_oMenuSocialSharing) && !$this->_initMenuSocialSharing())
+            return false;
+
+        $aItem = $this->_oMenuSocialSharing->getMenuItem($aItem['name']);
+        if(empty($aItem) || !is_array($aItem))
+            return false;
+
+        if($this->_bIsApi)
+            return $aItem;
+        else
+            return $this->_getMenuItemDefault($aItem);
+    }
+
+    protected function _getMenuItemByNameActionDelete($aItem)
+    {
+        $oProfile = $this->_oModule->getProfileObject(($this->_iContentId));
+        if (!$this->_oModule->isAllowDeleteOrDisable(bx_get_logged_profile_id(), $oProfile->id()))
+            return false;
+        
+        return $this->_getMenuItemByNameActionsMore($aItem);
+    }
+    
+    protected function _getMenuItemByNameActionsMore($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($this->_oMenuActionsMore)) {
+            if(empty($CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY_MORE']))
+                return '';
+
+            $this->_oMenuActionsMore = BxDolMenu::getObjectInstance($CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY_MORE']);
+            if(!$this->_oMenuActionsMore)
+                return '';
+
+            $this->_oMenuActionsMore->setContentId($this->_iContentId);
+            
+            $this->addMarkers($this->_oMenuActionsMore->getMarkers());
+        }
+
+        $aItem = $this->_oMenuActionsMore->getMenuItem($aItem['name']);
+        if(empty($aItem) || !is_array($aItem))
+            return false;
+
+        if($this->_bIsApi)
+            return $aItem;
+        else
+            return $this->_getMenuItemDefault($aItem);
+    }
+}
+
+/** @} */

+ 3960 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/general/classes/BxBaseModGeneralModule.php

@@ -0,0 +1,3960 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+define('BX_BASE_MOD_GENERAL_STATUS_ACTIVE', 'active');
+define('BX_BASE_MOD_GENERAL_STATUS_HIDDEN', 'hidden');
+define('BX_BASE_MOD_GENERAL_STATUS_PENDING', 'pending');
+
+bx_import('BxDolAcl');
+
+/**
+ * Base module class.
+ */
+class BxBaseModGeneralModule extends BxDolModule
+{
+    protected $_bIsApi;
+    protected $_iProfileId;
+    protected $_aSearchableNamesExcept;
+    protected $_aFormParams;
+
+    protected $_aBrowsingFiltersKeys;
+
+    function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+
+        $this->_bIsApi = bx_is_api();
+        $this->_iProfileId = bx_get_logged_profile_id();
+        $this->_aSearchableNamesExcept = array(
+            'allow_view_to'
+        );
+
+        $this->_aFormParams = array(
+            'display' => false, 
+            'dynamic_mode' => false, 
+            'ajax_mode' => false, 
+            'absolute_action_url' => false,
+            'visibility_autoselect' => false,
+            'context_id' => 0,
+            'custom' => array()
+        );
+
+        $this->_aBrowsingFiltersKeys = ['mode'];
+    }
+
+    // ====== ACTIONS METHODS
+
+    public function isEntryActive($aContentInfo)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $iProfileId = bx_get_logged_profile_id();
+        $bModerator = $this->_isModerator();
+
+        $mixedResult = null;
+        bx_alert($this->getName(), 'is_entry_active', 0, 0, ['viewer_id' => $iProfileId, 'is_moderator' => $bModerator, 'content_info' => $aContentInfo, 'override_result' => &$mixedResult]);
+        if($mixedResult !== null)
+            return $mixedResult;
+
+        if($this->isEntryAuthor($aContentInfo, $iProfileId) || $bModerator)
+            return true;
+
+        if(!empty($CNF['FIELD_ALLOW_VIEW_TO']) && (int)$aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']] < 0) {
+            $iContextProfileId = abs((int)$aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]);
+            $oContextProfile = BxDolProfile::getInstance($iContextProfileId);
+
+            $aAdmins = bx_srv($oContextProfile->getModule(), 'get_admins_to_manage_content', [$iContextProfileId]);
+            if(!empty($aAdmins) && in_array($iProfileId, $aAdmins))
+                return true;
+        }
+
+        if(isset($CNF['FIELD_STATUS']) && $aContentInfo[$CNF['FIELD_STATUS']] != 'active')
+            return false;
+
+        if(isset($CNF['FIELD_STATUS_ADMIN']) && isset($aContentInfo[$CNF['FIELD_STATUS_ADMIN']]) && $aContentInfo[$CNF['FIELD_STATUS_ADMIN']] != 'active')
+            return false;
+
+        return true;        
+    }
+
+    public function isEntryAuthor($aContentInfo, $iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+
+        $mixedResult = null;
+        bx_alert($this->getName(), 'is_entry_author', 0, 0, ['viewer_id' => $iProfileId, 'content_info' => $aContentInfo, 'override_result' => &$mixedResult]);
+        if($mixedResult !== null)
+            return $mixedResult;
+
+        return abs($aContentInfo[$CNF['FIELD_AUTHOR']]) == $iProfileId;
+    }
+
+    public function actionUpdateImage($sFiledName, $iContentId, $sValue)
+    {
+        $mixedResult = $this->serviceUpdateImage($sFiledName, $iContentId, $sValue);
+        if($mixedResult !== false)
+            echo $mixedResult;
+    }
+
+    public function actionUpdateImagePosition($iContentId, $sFiledName, $sH, $sV)
+    {
+        $this->serviceUpdateImagePosition($iContentId, $sFiledName, $sH, $sV);
+    }
+
+    public function actionApprove()
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!isset($CNF['FIELD_STATUS_ADMIN']))
+            return echoJson([]);
+
+        $iContentId = (int)bx_get('content_id');
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo) || $aContentInfo[$CNF['FIELD_STATUS_ADMIN']] != BX_BASE_MOD_GENERAL_STATUS_PENDING)
+            return echoJson([]);
+
+        if(($mixedCheckResult = $this->checkAllowedApprove($aContentInfo)) !== CHECK_ACTION_RESULT_ALLOWED)
+            return echoJson(['msg' => $mixedCheckResult]);
+
+        $oForm = $this->_getApproveForm($iContentId, $aContentInfo);
+        $oForm->initChecker();
+
+        if(!$oForm->isSubmitted() || !$oForm->isValid()) {
+            $sPopupId = $this->getName() . '_approve_' . $iContentId;
+            $sPopupTitle = '_sys_manage_popup_approve_title';
+            if(!empty($CNF['T']['txt_approve_popup_title'])) 
+                $sPopupTitle = $CNF['T']['txt_approve_popup_title'];
+
+            return echoJson(['popup' => BxTemplFunctions::getInstance()->popupBox($sPopupId, _t($sPopupTitle), $oForm->getCode(true))]);
+        }
+
+        if($oForm->getCleanValue('do_send')) {
+            $aResult = [];
+            if($this->_sendApproveMessage($iContentId, $aContentInfo, $oForm))
+                $aResult = ['msg' => _t('_sys_manage_txt_sent')];
+            else
+                $aResult = ['msg' => _t('_sys_manage_err_cannot_perform_action')];
+
+            return echoJson($aResult);
+        }
+
+        if($oForm->getCleanValue('do_submit')) {
+            $aResult = [];
+            if($this->_oDb->updateEntriesBy([$CNF['FIELD_STATUS_ADMIN'] => BX_BASE_MOD_GENERAL_STATUS_ACTIVE], [$CNF['FIELD_ID'] => $iContentId])) {
+                $this->checkAllowedApprove($aContentInfo, true);
+
+                $this->onApprove($aContentInfo);
+
+                /*
+                 * Process metas.
+                 * Note. It's essential to process metas a the very end, 
+                 * because all data related to an entry should be already
+                 * processed and are ready to be passed to alert. 
+                 */
+                $this->processMetasAdd($iContentId);
+
+                $aResult = ['msg' => _t('_sys_manage_txt_approved'), 'reload' => 1];
+            }
+            else
+                $aResult = ['msg' => _t('_sys_manage_err_cannot_perform_action')];
+
+            return echoJson($aResult);
+        }
+    }
+
+    public function serviceSetStatus($iId, $sValue, $sStatus = '')
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$sStatus && !empty($CNF['FIELD_STATUS_ADMIN']))
+            $sStatus = $CNF['FIELD_STATUS_ADMIN'];
+
+        $aStatuses = [];
+        if(!empty($CNF['FIELD_STATUS_ADMIN']))
+            $aStatuses[] = $CNF['FIELD_STATUS_ADMIN'];
+        if(!empty($CNF['FIELD_STATUS']))
+            $aStatuses[] = $CNF['FIELD_STATUS'];
+
+        if(!in_array($sStatus, $aStatuses))
+            return false;
+
+        if((int)$this->_oDb->updateEntriesBy([$sStatus => $sValue], [$CNF['FIELD_ID'] => $iId]) == 0)
+            return false;
+
+        if($sValue == BX_BASE_MOD_GENERAL_STATUS_ACTIVE)
+            $this->onPublished($iId);
+    }
+
+    public function actionRss ()
+    {
+        $aArgs = func_get_args();
+        $this->_rss($aArgs);
+    }
+
+    public function actionGetCreatePostForm()
+    {
+        $sName = $this->_oConfig->getName();
+
+    	$aParams = bx_process_input(array_intersect_key($_GET, $this->_aFormParams));
+    	$aParams = array_merge($this->_aFormParams, $aParams);
+        $aParams['context_id'] = (bool)$aParams['context_id'] ? (int)$aParams['context_id'] : false;
+
+    	$mixedResponse = $this->serviceGetCreatePostForm($aParams);
+    	if(empty($mixedResponse))
+            return echoJson(array());
+        else if(is_array($mixedResponse)) {
+            $mixedResponse['module'] = $sName;
+            return echoJson($mixedResponse);
+        }
+
+        return echoJson(array(
+            'module' => $sName,
+            'content' => $mixedResponse
+    	));
+    }
+
+    public function actionGetNotes()
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if(empty($CNF['OBJECT_NOTES']))
+            return echoJson([]);
+
+        $iContentId = (int)bx_get('content_id');
+
+        $oCmtsNotes = BxDolCmts::getObjectInstance($CNF['OBJECT_NOTES'], $iContentId, true, $this->_oTemplate);
+        $aCmtsNotes = $oCmtsNotes->getCommentsBlock([], ['in_designbox' => false]);
+        if(empty($aCmtsNotes) || !is_array($aCmtsNotes))
+            return echoJson([]);
+
+        $sPostfix = $iContentId < 0 ? '_rc' : '';
+
+        $sPopupId = $this->getName() . '_notes_' . $iContentId;
+        $sPopupTitle = !empty($CNF['T']['txt_cmts_notes_popup_view_title' . $sPostfix]) ? $CNF['T']['txt_cmts_notes_popup_view_title' . $sPostfix] : '_cmt_nts_popup_view_title' . $sPostfix;
+        echoJson(['popup' => BxTemplFunctions::getInstance()->popupBox($sPopupId, _t($sPopupTitle), $aCmtsNotes['content'])]);
+    }
+    
+    public function actionNested()
+    {
+        $sAction = bx_get('a');
+        $sMethodName = 'subaction' . ucfirst($sAction);
+        if (!method_exists($this, $sMethodName)) {
+            return;
+        }
+        $this->$sMethodName();
+    }
+    
+    public function actionEmbed($iContentId, $sUnitTemplate = '', $sAddCode = '')
+    {
+        $oTemplate = BxDolTemplate::getInstance();
+        
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo))
+            $oTemplate->getEmbed(false);
+
+        if(empty($sUnitTemplate))
+            $sUnitTemplate = 'unit_gallery.html';
+
+        $CNF = &$this->_oConfig->CNF;
+        
+        if(!empty($CNF['OBJECT_VIEWS'])) {
+            $oView = BxDolView::getObjectInstance($CNF['OBJECT_VIEWS'], $iContentId);
+            if($oView && $oView->isEnabled())
+                $oView->doView();
+        }
+        
+        $oTemplate->getEmbed($this->_oTemplate->unit($aContentInfo, true, $sUnitTemplate) . $sAddCode);
+    }
+   
+    public function subactionDelete()
+    {
+        header('Content-type: text/html; charset=utf-8');
+
+        $iNestedId = (int)bx_get('id');
+		$sNestedForm = bx_get('s');
+        
+        $oForm = BxDolForm::getObjectInstance($sNestedForm, $sNestedForm);
+        if (!$oForm){
+            echo _t('_sys_request_page_not_found_cpt');
+            return;
+        }
+
+        $aNested = $this->_oDb->getNestedBy(array('type' => 'id', 'id' => $iNestedId, 'key_name' => $oForm->aParams['db']['key']), $oForm->aParams['db']['table']);
+        if (empty($aNested)){
+            echo _t('_sys_request_page_not_found_cpt');
+            return;
+        }
+        
+        $aContentInfo = $this->_oDb->getContentInfoById ($aNested['content_id']); 
+        if (!$aContentInfo){
+            echo _t('_sys_request_page_not_found_cpt');
+            return;
+        }
+
+        elseif (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->checkAllowedEdit ($aContentInfo))) {
+            echo $sMsg;
+        } 
+        elseif (!$this->_oDb->deleteNestedById($iNestedId, $oForm->aParams['db']['key'], $oForm->aParams['db']['table'])) {
+            echo _t('_sys_txt_error_occured');
+        } 
+        else {
+            echo 'ok';
+        }
+    }
+    
+    /**
+     * Get possible recipients for start conversation form
+     */
+    public function actionAjaxGetProfiles ()
+    {
+        $sTerm = bx_get('term');
+
+        $a = BxDolService::call('system', 'profiles_search', array($sTerm), 'TemplServiceProfiles');
+
+        header('Content-Type:text/javascript; charset=utf-8');
+        echo(json_encode($a));
+    }
+
+    public function actionGetBrowsingFilters()
+    {
+        $aParams = $this->_prepareBrowsingFiltersParamsGet();
+
+        $sContent = $this->_oTemplate->getBrowsingFilters($aParams);
+        if(empty($sContent))
+            return echoJson([]);
+
+        echoJson([
+            'code' => 0,
+            'popup' => [
+                'html' => $sContent,
+                'options' => [
+                    'closeOnOuterClick' => false,
+                    'removeOnClose' => false,
+                ]
+            ],
+        ]);
+    }
+
+    public function actionApplyBrowsingFilters()
+    {
+        $aParams = $this->_prepareBrowsingFiltersParamsGet();
+
+        $aBrowseParams = ['mode' => $aParams['mode'], 'empty_message' => true];
+        unset($aParams['mode']);
+        $aBrowseParams['params'] = $aParams;
+
+        $mixedResult = $this->serviceBrowse($aBrowseParams);
+        if(empty($mixedResult))
+            return echoJson([]);
+
+        if(is_array($mixedResult) && isset($mixedResult['content']))
+            $mixedResult = $mixedResult['content'];
+
+        return echoJson([
+            'code' => 0, 
+            'content' => $mixedResult, 
+            'eval' => $this->_oConfig->getJsObject('main') . '.onApplyBrowsingFilter(oData)'
+        ]);
+    }
+
+    protected function _prepareBrowsingFiltersParamsGet($mParams = false)
+    {
+        $aParams = [];
+        $bParams = !empty($mParams) && is_array($mParams);
+
+        foreach($this->_aBrowsingFiltersKeys as $sKey)
+            if($bParams) {
+                if(isset($mParams[$sKey]))
+                    $aParams[$sKey] = $mParams[$sKey];
+            }
+            else {
+                if(($sValue = bx_get($sKey)) !== false)
+                    $aParams[$sKey] = $sValue;
+            }
+
+        return $aParams;
+    }
+
+    // ====== SERVICE METHODS
+
+    public function serviceIsBadgesAvaliable()
+    {
+        if(!isAdmin() && !$this->_isAdministrator())
+            return false;
+
+        $aBadges = BxDolBadges::getInstance()->getData([
+            'type' => 'by_module&object', 
+            'object_id' => 0, 
+            'module' => $this->getName()
+        ]);
+
+        return count($aBadges) > 0 ? true : false;
+    }
+
+    public function serviceGetSafeServices()
+    {
+        return array(
+            // other
+            'ModuleIcon' => '',
+            'GetLink' => '',
+            // browse
+            'GetSearchResultUnit' => '',
+            'Browse' => '',
+            'BrowseFeatured' => '',
+            'BrowseFavorite' => '',
+            // forms
+            'GetCreatePostForm' => '',
+            'EntityCreate' => '',
+            'EntityEdit' => '',
+            'EntityDelete' => '',
+            'UpdateImage' => '',
+            // page blocks
+            'EntityTextBlock' => '',
+            'EntityInfo' => '',
+            'EntityInfoFull' => '',
+            'EntityInfoExtended' => '',
+            'EntityLocation' => '',
+            'EntityComments' => '',
+            'EntityAttachments' => '',
+            'CategoriesMultiList' => '',
+            // menu
+            'EntityAllActions' => '',
+            'EntityActions' => '',
+            'EntitySocialSharing' => '',
+            'MyEntriesActions' => '',
+        );
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-other Other
+     * @subsubsection bx_base_general-module_icon module_icon
+     * 
+     * @code bx_srv('bx_posts', 'module_icon', [...]); @endcode
+     * 
+     * Get module icon as CSS class name with FontAwesome icon name class and color class
+     * 
+     * @see BxBaseModGeneralModule::serviceModuleIcon
+     */
+    /** 
+     * @ref bx_base_general-module_icon "module_icon"
+     */
+    public function serviceModuleIcon ()
+    {
+        return isset($this->_oConfig->CNF['ICON']) ? $this->_oConfig->CNF['ICON'] : '';
+    }
+
+    public function serviceGetAuthor ($iContentId)
+    {
+        $mixedResult = $this->_getFieldValue('FIELD_AUTHOR', $iContentId);
+        return $mixedResult !== false ? (int)$mixedResult : 0; 
+    }
+
+    public function serviceGetPrivacyView ($iContentId)
+    {
+        return $this->_getFieldValue('FIELD_ALLOW_VIEW_TO', $iContentId);
+    }
+
+    public function serviceGetDateAdded ($iContentId)
+    {
+        $mixedResult = $this->_getFieldValue('FIELD_ADDED', $iContentId);
+        return $mixedResult !== false ? (int)$mixedResult : 0; 
+    }
+
+    public function serviceGetDateChanged ($iContentId)
+    {
+        $mixedResult = $this->_getFieldValue('FIELD_CHANGED', $iContentId);
+        return $mixedResult !== false ? (int)$mixedResult : 0;
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-other Other
+     * @subsubsection bx_base_general-get_link get_link
+     * 
+     * @code bx_srv('bx_posts', 'get_link', [...]); @endcode
+     * 
+     * Get URL for the specified content.
+     * @param $iContentId content id
+     * 
+     * @see BxBaseModGeneralModule::serviceGetLink
+     */
+    /** 
+     * @ref bx_base_general-get_link "get_link"
+     */
+    public function serviceGetLink ($iContentId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if(empty($CNF['URI_VIEW_ENTRY']))
+            return '';
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo))
+            return '';
+
+        return bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]));
+    }
+
+    public function serviceGetTitle ($iContentId)
+    {
+        $mixedResult = $this->_getFieldValue('FIELD_TITLE', $iContentId);
+        return $mixedResult !== false ? $mixedResult : '';
+    }
+
+    public function serviceGetText ($iContentId)
+    {
+        $mixedResult = $this->_getFieldValue('FIELD_TEXT', $iContentId);
+        if (false === $mixedResult)
+            return '';
+
+        $CNF = &$this->_oConfig->CNF;
+        if (!empty($CNF['OBJECT_METATAGS']) && is_string($mixedResult)) {
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+            $mixedResult = $oMetatags->metaParse($iContentId, $mixedResult);
+        }
+
+        return $mixedResult;
+    }
+    
+    public function serviceGetLocation ($iContentId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($CNF['OBJECT_METATAGS']))
+            return [];
+
+        $mixedResult = $this->_getFieldValue('FIELD_LOCATION', $iContentId);
+        if(false === $mixedResult)
+            return [];
+
+        $oMeta = false;
+        if(!($oMeta = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS'])) || !$oMeta->locationsIsEnabled())
+            return false;
+
+        $aLocation = $oMeta->locationGet($iContentId);
+        if(empty($aLocation) || !is_array($aLocation))
+            return false;
+
+        return array_values(array_slice($aLocation, 1));
+    }
+
+    public function serviceGetEmbed ($iContentId)
+    {
+        $sTitle = $this->_getFieldValue('FIELD_TITLE', $iContentId);
+        return $this->_oTemplate->parseHtmlByName('embed.html', [
+            'title' => $sTitle,
+            'url' => BX_DOL_URL_ROOT . $this->_oConfig->getBaseUri() . 'embed/' . $iContentId . '/'
+        ]);
+    }
+
+    public function serviceGetInfo ($iContentId, $bSearchableFieldsOnly = true)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aContentInfo = $this->_getFields($iContentId);
+        if(empty($aContentInfo))
+            return array();
+
+        if(!$bSearchableFieldsOnly)
+            return $aContentInfo;
+
+        if(empty($CNF['PARAM_SEARCHABLE_FIELDS']))
+            return array();
+
+        $aFields = explode(',', getParam($CNF['PARAM_SEARCHABLE_FIELDS']));
+        if(empty($aFields))
+            return array();
+
+        $aResult = array();
+        foreach($aFields as $sField)
+            if(isset($aContentInfo[$sField]))
+                $aResult[$sField] = $aContentInfo[$sField];
+
+        return $aResult;
+    }
+
+    public function serviceGetInfoApi ($iContentId, $bExtendedUnits = false)
+    {
+        $aData = $this->serviceGetInfo($iContentId, false);
+        if($aData)
+            $aData = $this->getDataAPI($aData, ['extended' => $bExtendedUnits]);
+
+        return $aData;
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-browsing Browsing
+     * @subsubsection bx_base_general-get_search_result_unit get_search_result_unit
+     * 
+     * @code bx_srv('bx_posts', 'get_search_result_unit', [3,"unit_gallery.html"]); @endcode
+     * 
+     * Get browsing unit for the specified content
+     * @param $iContentId content id
+     * @param $sUnitTemplate unit template, such as: unit_full.html, unit_gallery.html, unit_live_search.html
+     * 
+     * @see BxBaseModGeneralModule::serviceGetSearchResultUnit
+     */
+    /** 
+     * @ref bx_base_general-get_search_result_unit "get_search_result_unit"
+     */
+    public function serviceGetSearchResultUnit ($iContentId, $sUnitTemplate = '')
+    {
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if (bx_is_api())
+            return $this->decodeDataAPI($aContentInfo);
+        
+        
+        if(empty($aContentInfo))
+            return '';
+
+        if(empty($sUnitTemplate))
+            $sUnitTemplate = 'unit.html';
+
+        return $this->_oTemplate->unit($aContentInfo, true, $sUnitTemplate);
+    }
+
+    public function serviceGetAll ($aParams = array())
+    {
+        if(empty($aParams) || !is_array($aParams))
+            $aParams = array('type' => 'all');
+
+        return $this->_oDb->getEntriesBy($aParams);
+    }
+
+    public function serviceGetAllByAuthor ($iProfileId)
+    {
+        return $this->_oDb->getEntriesByAuthor((int)$iProfileId);
+    }
+
+    public function serviceGetSortableFieldsExtended($aInputsAdd = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if(empty($CNF['OBJECT_FORM_ENTRY']))
+            return array();
+        
+        $aResult = array();
+        
+        $aSortableFields = [];
+        
+        $aDisplays = [];
+        if(!empty($CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD'])) {
+            $aDisplays[] = $CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD'];
+        }
+        if(!empty($CNF['OBJECT_FORM_ENTRY_DISPLAY_EDIT'])) {
+            $aDisplays[] = $CNF['OBJECT_FORM_ENTRY_DISPLAY_EDIT'];
+        }
+        
+        $aInputsAll = BxDolFormQuery::getFormInputs($CNF['OBJECT_FORM_ENTRY']);    
+        foreach($aInputsAll as $aInput){
+            $aInputsAll[$aInput['name']] = $aInput;
+        }
+        
+        $aInputs = BxDolFormQuery::getFormInputs($CNF['OBJECT_FORM_ENTRY'],  $aDisplays);
+        foreach($aInputs as $aInput){
+            $aInputs[$aInput['name']] = $aInput;
+            if ($aInput['type'] == 'text'){
+                $aSortableFields[] = $aInput['name'];
+            }
+        }
+        
+        if(!empty($CNF['FIELD_ADDED']) && isset($aInputsAll[$CNF['FIELD_ADDED']])){
+           $aSortableFields[] = $CNF['FIELD_ADDED'];
+        }
+        
+        if(!empty($CNF['FIELD_CHANGED']) && isset($aInputsAll[$CNF['FIELD_CHANGED']])){
+           $aSortableFields[] = $CNF['FIELD_CHANGED'];
+        }     
+        
+        foreach($aSortableFields as $sSortableField){
+             $aResult[$sSortableField . '_asc'] = [
+                'name' => $sSortableField,
+                'caption' => $aInputsAll[$sSortableField]['caption'],
+                'direction' => 'asc'
+            ];
+            
+            $aResult[$sSortableField . '_desc']  = [
+                'name' => $sSortableField,
+                'caption' => $aInputsAll[$sSortableField]['caption'],
+                'direction' => 'desc'
+            ];
+        }
+        
+        return $aResult;
+    }
+    
+    public function serviceGetSearchableFieldsExtended($aInputsAdd = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if(empty($CNF['OBJECT_FORM_ENTRY']))
+            return array();
+
+        $aResult = array();
+        if(!empty($CNF['FIELD_AUTHOR']) && !in_array($CNF['FIELD_AUTHOR'], $this->_aSearchableNamesExcept))
+            $aResult[$CNF['FIELD_AUTHOR']] = array(
+                'type' => 'text_auto', 
+                'caption' => $CNF['T']['form_field_author'],
+                'info' => '',
+            	'value' => '',
+                'values' => '',
+                'pass' => ''
+            );
+
+        $aInputs = array();
+        if(!empty($CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD'])) {
+            $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD'], $this->_oTemplate);
+
+            $aInputs = array_merge($aInputs, $oForm->aInputs);
+        }
+        if(!empty($CNF['OBJECT_FORM_ENTRY_DISPLAY_EDIT'])) {
+            $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $CNF['OBJECT_FORM_ENTRY_DISPLAY_EDIT'], $this->_oTemplate);
+
+            $aInputs = array_merge($aInputs, $oForm->aInputs);
+        }
+
+        if(!empty($aInputsAdd) && is_array($aInputsAdd))
+            $aInputs = array_merge($aInputs, $aInputsAdd);
+
+        foreach($aInputs as $aInput){
+            if(in_array($aInput['type'], BxDolSearchExtended::$SEARCHABLE_TYPES) && !in_array($aInput['name'], $this->_aSearchableNamesExcept)) {
+                $aField = array(
+                    'type' => $aInput['type'], 
+                    'caption_system' => $aInput['caption_system_src'],
+                    'caption' => $aInput['caption_src'],
+                    'info' => $aInput['info_src'],
+                    'value' => !empty($aInput['value']) ? $aInput['value'] : '',
+                    'values' => !empty($aInput['values_src']) ? $aInput['values_src'] : '',
+                    'pass' => !empty($aInput['db']['pass']) ? $aInput['db']['pass'] : '',
+                );
+
+                if(isset($aInput['search_type']))
+                    $aField['search_type'] = $aInput['search_type'];
+
+                if(isset($aInput['search_operator']))
+                    $aField['search_operator'] = $aInput['search_operator'];
+
+                $aResult[$aInput['name']] = $aField;
+            }
+        }
+        return $aResult;
+    }
+
+    public function serviceGetSearchResultExtended($aParams, $iStart = 0, $iPerPage = 0, $bFilterMode = false)
+    {
+        if((empty($aParams) || !is_array($aParams)) && !$bFilterMode)
+            return array();
+
+        return $this->_oDb->getEntriesBy(array('type' => 'search_ids', 'search_params' => $aParams, 'start' => $iStart, 'per_page' => $iPerPage));
+    }
+
+    public function serviceGetSearchableFields ($aInputsAdd = array())
+    {
+        $CNF = $this->_oConfig->CNF;
+
+        if(!isset($CNF['PARAM_SEARCHABLE_FIELDS']) || !isset($CNF['OBJECT_FORM_ENTRY']) || !isset($CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD']))
+            return array();
+
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD'], $this->_oTemplate);
+        if(!$oForm)
+            return array();
+
+        $aInputs = $oForm->aInputs;
+        if(!empty($aInputsAdd) && is_array($aInputsAdd))
+            $aInputs = array_merge($aInputs, $aInputsAdd);
+
+        $aTextTypes = array('text', 'textarea');
+        $aTextFields = array();
+        foreach($aInputs as $r)
+            if(in_array($r['type'], $aTextTypes))
+                $aTextFields[$r['name']] = $r['caption'];
+
+        return $aTextFields;
+    }
+
+    public function serviceUpdateImage($sFiledName, $iContentId, $sValue)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aData = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aData) || !is_array($aData))
+            return false;
+
+        if($this->checkAllowedEdit($aData) !== CHECK_ACTION_RESULT_ALLOWED)
+            return false;
+
+        //TODO add transcoder settings
+        $oImgStorage = BxDolStorage::getObjectInstance($CNF['OBJECT_STORAGE']);
+        if(!$oImgStorage)
+            return false;
+
+        $this->_oDb->updateEntriesBy([$sFiledName => $sValue], [$CNF['FIELD_ID'] => $iContentId]);
+
+        $this->serviceUpdateImagePosition($iContentId, $sFiledName, '', '');
+
+        $this->onUpdateImage($iContentId, $sFiledName, $sValue);
+
+        return $oImgStorage->getFileUrlById($sValue);
+    }
+
+    public function serviceUpdateImagePosition($iContentId, $sFiledName, $sH, $sV)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sFieldNamePos = 'FIELD_' . strtoupper($sFiledName) . '_POSITION';
+        if(!isset($CNF[$sFieldNamePos]))
+            return false;
+
+        $sValue = '';
+        if($sH != '' || $sV != '')
+            $sValue = json_encode(['x' => $sH, 'y' => $sV]);
+
+        $this->_oDb->updateEntriesBy([$CNF[$sFieldNamePos] => $sValue], [$CNF['FIELD_ID'] => $iContentId]);
+
+        return true;
+    }
+
+    public function serviceManageTools($sType = 'common')
+    {
+        if(!isLogged())
+            return '';
+
+        $oGrid = BxDolGrid::getObjectInstance($this->_oConfig->getGridObject($sType));
+        if(!$oGrid)
+            return '';
+
+        if($this->_bIsApi)
+            return [
+                bx_api_get_block('grid', $oGrid->getCodeAPI())
+            ];
+
+        $CNF = &$this->_oConfig->CNF;
+
+        $mixedMenu = '';
+        if(BxDolAcl::getInstance()->isMemberLevelInSet(192)) {
+            $oPermalink = BxDolPermalinks::getInstance();
+
+            $aMenuItems = array();
+            if(!empty($CNF['OBJECT_GRID_COMMON']) && !empty($CNF['T']['menu_item_manage_my']))
+                $aMenuItems[] = array('id' => 'manage-common', 'name' => 'manage-common', 'class' => '', 'link' => $oPermalink->permalink($CNF['URL_MANAGE_COMMON']), 'target' => '_self', 'title' => _t($CNF['T']['menu_item_manage_my']), 'active' => 1);
+            if(!empty($CNF['OBJECT_GRID_ADMINISTRATION']) && !empty($CNF['T']['menu_item_manage_all']))
+                $aMenuItems[] = array('id' => 'manage-administration', 'name' => 'manage-administration', 'class' => '', 'link' => $oPermalink->permalink($CNF['URL_MANAGE_ADMINISTRATION']), 'target' => '_self', 'title' => _t($CNF['T']['menu_item_manage_all']), 'active' => 1);
+
+            if(count($aMenuItems) > 1) {
+                $mixedMenu = new BxTemplMenu(array(
+                    'template' => 'menu_block_submenu_ver.html', 
+                    'menu_items' => $aMenuItems
+                ), $this->_oTemplate);
+                $mixedMenu->setSelected($this->_aModule['name'], 'manage-' . $sType);
+            }
+        }
+
+        if(!empty($CNF['OBJECT_MENU_SUBMENU']) && isset($CNF['URI_MANAGE_COMMON']) && ($oSubmenu = BxDolMenu::getObjectInstance($CNF['OBJECT_MENU_SUBMENU'])) !== false)
+            $oSubmenu->setSelected($this->_aModule['name'], $CNF['URI_MANAGE_COMMON']);
+
+        $this->_oTemplate->addCss(array('manage_tools.css'));
+        $this->_oTemplate->addJs(array('manage_tools.js'));
+        $this->_oTemplate->addJsTranslation(array('_sys_grid_search'));
+        return array(
+            'content' => $this->_oTemplate->getJsCode('manage_tools', array('sObjNameGrid' => $this->_oConfig->getGridObject($sType))) . $oGrid->getCode(),
+            'menu' => $mixedMenu
+        );
+    }
+
+    public function serviceGetMenuAddonManageTools()
+    {
+    	return 0;
+    }
+
+    public function serviceGetMenuAddonManageToolsProfileStats($iProfileId = 0)
+    {
+    	return 0;
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-browsing Browsing
+     * @subsubsection bx_base_general-browse browse
+     * 
+     * @code bx_srv('bx_posts', 'browse', [...]); @endcode
+     * @code {{~bx_posts:browse[{"mode":"author", "params":{"author":"10"}}]~}} @endcode
+     * 
+     * Universal browse method.
+     * @param $aParams custom browse params, possible params are the following:
+     *  - mode - browse mode, such as 'recent', 'featured', etc
+     *  - params - custom params to browse method, for example 'unit_view' can be passed here
+     *  - design_box - design box style, @see BxBaseFunctions::DesignBoxContent 
+     *  - empty_message - display or not "empty" message when there is no content
+     *  - ajax_paginate - use AJAX paginate or not
+     * 
+     * @see BxBaseModGeneralModule::serviceBrowse
+     */
+    /** 
+     * @ref bx_base_general-browse "browse"
+     */
+    public function serviceBrowse ($aParams = array())
+    {
+        if($this->_bIsApi && is_string($aParams)){
+            $aParams = json_decode($aParams, true);
+            if (isset($aParams['params'])) {
+                if(isset($aParams['params']['type']))
+                    $aParams['mode'] = $aParams['params']['type'];
+
+                if(isset($aParams['params']['filters'])) {
+                    foreach($aParams['params']['filters'] as $sKey => $sValue)
+                        $aParams['params'][$sKey] = $sValue;
+
+                    $aParams['params']['filters'] = [
+                        'values' => $aParams['params']['filters']
+                    ];
+                }
+            }
+        }
+
+        if (isset($aParams['params']['condition']))
+            unset($aParams['params']['condition']);
+
+        return $this->serviceBrowseWithCondition ($aParams);
+    }
+
+    public function serviceBrowseWithCondition ($aParams = array())
+    {
+        $aDefaults = array (
+            'mode' => 'recent',
+            'params' => false,
+            'design_box' => BX_DB_PADDING_DEF,
+            'empty_message' => false,
+            'ajax_paginate' => true,
+            'class_search_result' => 'SearchResult'
+        );
+
+        $aParams = array_merge($aDefaults, $aParams);
+        
+        return $this->_serviceBrowse ($aParams['mode'], $aParams['params'], $aParams['design_box'], $aParams['empty_message'], $aParams['ajax_paginate'], $aParams['class_search_result']);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-browsing Browsing
+     * @subsubsection bx_base_general-browse_featured browse_featured
+     * 
+     * @code bx_srv('bx_posts', 'browse_featured', [...]); @endcode
+     * 
+     * Display featured entries
+     * @param $sUnitView browsing unity view, 
+     *                   such as: full, extended, gallery, showcase
+     * @param $bEmptyMessage display or not "empty" message when there is no content
+     * @param $bAjaxPaginate use AJAX paginate or not
+     * 
+     * @see BxBaseModGeneralModule::serviceBrowseFeatured
+     */
+    /** 
+     * @ref bx_base_general-browse_featured "browse_featured"
+     */
+    public function serviceBrowseFeatured ($sUnitView = false, $bEmptyMessage = false, $bAjaxPaginate = true)
+    {
+        return $this->_serviceBrowse ('featured', $sUnitView ? array('unit_view' => $sUnitView) : false, BX_DB_PADDING_DEF, $bEmptyMessage, $bAjaxPaginate);
+    }
+	
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-browsing Browsing
+     * @subsubsection bx_base_general-browse_favorite browse_favorite
+     * 
+     * @code bx_srv('bx_posts', 'browse_favorite', [...]); @endcode
+     * 
+     * Display entries which were added to favorite list by some member
+     * @param $iProfileId profile ID
+     * @param $aParams additional browse params, see BxBaseModGeneralModule::serviceBrowse
+     * 
+     * @see BxBaseModGeneralModule::serviceBrowseFavorite BxBaseModGeneralModule::serviceBrowse
+     */
+    /** 
+     * @ref bx_base_general-browse_favorite "browse_favorite"
+     */
+    public function serviceBrowseFavorite ($iProfileId = 0, $aParams = array())
+    {
+        $oProfile = null;
+        if((int)$iProfileId)
+            $oProfile = BxDolProfile::getInstance($iProfileId);
+        if(!$oProfile && bx_get('profile_id') !== false)
+            $oProfile = BxDolProfile:: getInstance(bx_process_input(bx_get('profile_id'), BX_DATA_INT));
+        if(!$oProfile)
+            $oProfile = BxDolProfile::getInstance();
+        if(!$oProfile)
+            return '';
+
+        $bEmptyMessage = true;
+        if(isset($aParams['empty_message'])) {
+            $bEmptyMessage = (bool)$aParams['empty_message'];
+            unset($aParams['empty_message']);
+        }
+        
+        if(bx_get('list_id') !== false && !isset($aParams['list_id']))
+            $aParams['list_id'] = (int) bx_get('list_id');
+
+        return $this->_serviceBrowse ('favorite', array_merge(array('user' => $oProfile->id()), $aParams), BX_DB_PADDING_DEF, $bEmptyMessage);
+    }
+    
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-browsing Browsing
+     * @subsubsection bx_base_general-browse_favorite_lists browse_favorite_lists
+     * 
+     * @code bx_srv('bx_posts', 'browse_favorite_lists', [...]); @endcode
+     * 
+     * Display entries which were added to favorite lists grouped by lists by some member
+     * @param $iProfileId profile ID
+     * @param $aParams additional browse params, see BxBaseModGeneralModule::serviceBrowse
+     * 
+     * @see BxBaseModGeneralModule::serviceBrowseFavoriteLists BxBaseModGeneralModule::serviceBrowse
+     */
+    /** 
+     * @ref bx_base_general-browse_favorite_lists "browse_favorite_lists"
+     */
+    public function serviceBrowseFavoriteLists ($iProfileId = 0, $aParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+        $oProfile = null;
+        if((int)$iProfileId)
+            $oProfile = BxDolProfile::getInstance($iProfileId);
+        if(!$oProfile && bx_get('profile_id') !== false)
+            $oProfile = BxDolProfile:: getInstance(bx_process_input(bx_get('profile_id'), BX_DATA_INT));
+        if(!$oProfile)
+            $oProfile = BxDolProfile::getInstance();
+        if(!$oProfile)
+            return '';
+        
+        $iStart = (bx_get('list_start') !== false ? (int)bx_get('list_start') : 0);
+        $iPerPage = (int)getParam($CNF['PARAM_PER_PAGE_FOR_FAVORITES_LISTS']);
+        
+        return $this->_oTemplate->getFavoriteList($oProfile, $iStart, $iPerPage, $aParams);
+    }
+    
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-browsing Browsing
+     * @subsubsection bx_base_general-browse_by_category category
+     * 
+     * @code bx_srv('bx_posts', 'browse_by_category', [...]); @endcode
+     * 
+     * Display entries in category
+     * @param $sUnitView browsing unity view, 
+     *                   such as: full, extended, gallery, showcase
+     * @param $bEmptyMessage display or not "empty" message when there is no content
+     * @param $bAjaxPaginate use AJAX paginate or not
+     * 
+     * @see BxBaseModGeneralModule::serviceBrowseByCategory BxBaseModGeneralModule::serviceBrowse
+     */
+    /** 
+     * @ref bx_base_general-browse_by_category "browse_by_category"
+     */
+    public function serviceBrowseCategory($sUnitView = false, $bEmptyMessage = true, $bAjaxPaginate = true, $aParams = [])
+    {
+        $sType = 'category';
+
+        $aParams = array_merge(['category' => bx_process_input(bx_get('category'), BX_DATA_INT)], $aParams);
+        if($sUnitView)
+            $aParams['unit_view'] = $sUnitView;
+
+        return $this->_serviceBrowse($sType, $aParams, BX_DB_PADDING_DEF, $bEmptyMessage, $bAjaxPaginate);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-browsing Browsing
+     * @subsubsection bx_base_general-browse_favorite_list_actions browse_favorite_list_actions
+     * 
+     * @code bx_srv('bx_posts', 'browse_favorite_list_actions', [...]); @endcode
+     * 
+     * Display menu actions for favorite lists
+     * 
+     * @see BxBaseModGeneralModule::serviceFavoritesListActions
+     */
+    /** 
+     * @ref bx_base_general-browse_favorite_list_actions "browse_favorite_list_actions"
+     */
+    public function serviceFavoritesListActions()
+    {
+        $iListId = 0;
+        if(bx_get('list_id') !== false)
+            $iListId = (int) bx_get('list_id');        
+
+        $oProfile = null;
+        if(bx_get('profile_id') !== false)
+            $oProfile = BxDolProfile:: getInstance(bx_process_input(bx_get('profile_id'), BX_DATA_INT));
+        if(!$oProfile)
+            return false;
+
+        $CNF = &$this->_oConfig->CNF;
+        $oFavorite = BxDolFavorite::getObjectInstance($CNF['OBJECT_FAVORITES'], 0, true);
+
+        $aList = $oFavorite->getQueryObject()->getList(array('type' => 'id', 'list_id' => $iListId));
+        if($iListId != 0 && (empty($aList) || !is_array($aList)))
+            return false;
+
+        $sRv = '';
+        if (!empty($aList) && $oFavorite->isAllowedEditList($aList['author_id'])){  
+            $aMarkers = array(
+                'js_object' => $oFavorite->getJsObjectName(),
+                'list_id' => $iListId,
+            );
+
+            $oMenu = BxDolMenu::getObjectInstance('sys_favorite_list');
+
+            $oMenu->addMarkers($aMarkers);
+            $sMenu = $oMenu->getCode();
+
+            $sRv .= $sMenu . $oFavorite->getJsScript();
+        }
+
+        $aMarkers = array(
+            'id' => $iListId,
+            'module' => $this->_aModule['name'],
+            'url' => $this->_getFavoriteListUrl($iListId, $oProfile->id()),
+            'title' => $iListId > 0 ? $aList['title'] : _t('_sys_txt_default_favorite_list')
+        );
+
+        $oMenu = BxDolMenu::getObjectInstance('sys_social_sharing');
+        $oMenu->addMarkers($aMarkers);
+        $sRv .= $sMenu = $oMenu->getCode();
+        return $sRv;
+    }
+     
+     /**
+    * @page service Service Calls
+    * @section bx_base_general Base General
+    * @subsection bx_base_general-menu Menu
+    * @subsubsection bx_base_general-favorites_list_info favorites_list_info
+    * 
+    * @code bx_srv('bx_posts', 'favorites_list_info', [...]); @endcode
+    * 
+    * Favorites list info
+    * @param $aParams array with additional custom params 
+    *                 which may overwrite some default values
+    * 
+    * @see BxBaseModGeneralModule::serviceFavoritesListInfo
+    */
+    /** 
+    * @ref bx_base_general-favorites_list_info "favorites_list_info"
+    */
+    public function serviceFavoritesListInfo($aParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $iListId = 0;
+        if(bx_get('list_id') !== false)
+            $iListId = (int) bx_get('list_id');       
+
+        $oFavorite = BxDolFavorite::getObjectInstance($CNF['OBJECT_FAVORITES'], 0, true);
+        $aList = $oFavorite->getQueryObject()->getList(array('type' => 'id', 'list_id' => $iListId));
+        if($iListId != 0 && (empty($aList) || !is_array($aList)))
+            return false;
+
+        $oProfile = null;
+        if(bx_get('profile_id') !== false)
+            $oProfile = BxDolProfile:: getInstance(bx_process_input(bx_get('profile_id'), BX_DATA_INT));
+
+        if(!$oProfile)
+            return false;
+
+        return $this->_oTemplate->getFavoritesListInfo($aList, $oProfile);
+    }
+    
+    /**
+     * Display entries posted into particular context
+     * @return HTML string
+     */
+    public function serviceBrowseContext ($iProfileId = 0, $aParams = array())
+    {
+        return $this->_serviceBrowseWithParam ('context', 'profile_id', $iProfileId, $aParams);
+    }
+    
+    public function _serviceBrowseWithParam ($sParamName, $sParamGet, $sParamVal, $aParams = array())
+    {
+        if(!$sParamVal)
+            $sParamVal = bx_process_input(bx_get($sParamGet), BX_DATA_INT);
+        if(!$sParamVal)
+            return '';
+
+        $bEmptyMessage = true;
+        if(isset($aParams['empty_message'])) {
+            $bEmptyMessage = (bool)$aParams['empty_message'];
+            unset($aParams['empty_message']);
+        }
+
+        $bAjaxPaginate = true;
+        if(isset($aParams['ajax_paginate'])) {
+            $bAjaxPaginate = (bool)$aParams['ajax_paginate'];
+            unset($aParams['ajax_paginate']);
+        }
+
+        return $this->_serviceBrowse ($sParamName, array_merge(array($sParamName => $sParamVal), $aParams), BX_DB_PADDING_DEF, $bEmptyMessage, $bAjaxPaginate);
+    }
+
+    public function serviceFormsHelper ()
+    {
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'FormsEntryHelper';
+        return new $sClass($this);
+    }
+
+	/**
+     * Add entry using provided fields' values.
+     * @return array with result: 'code' is 0 on success or non-zero on error, 'message' is error message in case of error, 'content' is content info array in case of success
+     */
+    public function serviceEntityAdd ($iProfile, $aValues, $sDisplay = false)
+    {
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'FormsEntryHelper';
+        $oFormsHelper = new $sClass($this);
+        return $oFormsHelper->addData($iProfile, $aValues, $sDisplay);
+    }
+
+	/**
+     * Perform redirect after content creation
+     * @return nothing, rediret header is sent
+     */    
+    public function serviceRedirectAfterAdd($aContentInfo)
+    {
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'FormsEntryHelper';
+        $oFormsHelper = new $sClass($this);
+        $oFormsHelper->redirectAfterAdd($aContentInfo);
+    }
+
+    /**
+     * Get form object for add, edit, view or delete the content 
+     * @param $sType 'add', 'edit', 'view' or 'delete'
+     * @param $aParams optional array with parameters(display name, etc)
+     * @return form object or false on error
+     */
+    public function serviceGetObjectForm ($sType, $aParams = array())
+    {
+        if(!in_array($sType, array('add', 'edit', 'view', 'delete')))
+            return false;
+
+        $iLoggedId = (int)bx_get_logged_profile_id();
+        $mixedContextId = isset($aParams['context_id']) ? $aParams['context_id'] : false;
+
+        $oContextProfile = false;
+        $bContext = $mixedContextId !== false;
+        if($bContext) {
+            $oContextProfile = BxDolProfile::getInstance(abs($mixedContextId));
+            $bContext = $oContextProfile !== false;
+
+            if($bContext && $oContextProfile->checkAllowedPostInProfile() !== CHECK_ACTION_RESULT_ALLOWED)
+                return false;
+        }
+
+        $CNF = &$this->_oConfig->CNF;
+
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'FormsEntryHelper';
+        $oFormsHelper = new $sClass($this);
+
+        $sParamsKey = 'ajax_mode';
+        if(isset($aParams[$sParamsKey]) && (bool)$aParams[$sParamsKey] === true)
+            $oFormsHelper->setAjaxMode((bool)$aParams[$sParamsKey]);
+
+        $sParamsKey = 'absolute_action_url';
+        if(isset($aParams[$sParamsKey]) && (bool)$aParams[$sParamsKey] === true)
+            $oFormsHelper->setAbsoluteActionUrl((bool)$aParams[$sParamsKey]);
+
+        $sDisplay = !empty($aParams['display']) ? $aParams['display'] : false;
+
+        $sFunc = 'getObjectForm' . ucfirst($sType);
+        $oForm = $oFormsHelper->$sFunc($sDisplay);
+
+        $sKey = 'FIELD_ALLOW_VIEW_TO';
+        if(!empty($CNF[$sKey]) && !empty($oForm->aInputs[$CNF[$sKey]]) && (!$bContext || $mixedContextId < 0)) {
+            $bContextOwner = $bContext && abs($mixedContextId) == $iLoggedId;
+
+            if(!$bContext || $bContextOwner) {
+                $iGc = 0;
+                $iKeyGh = false;
+                foreach($oForm->aInputs[$CNF[$sKey]]['values'] as $iKey => $aValue) {
+                    if(isset($aValue['type']) && in_array($aValue['type'], array('group_header', 'group_end'))) {
+                        if($iKeyGh !== false && $iGc == 0) {
+                            unset($oForm->aInputs[$CNF[$sKey]]['values'][$iKeyGh]);
+                            $iKeyGh = false;
+
+                            if($aValue['type'] == 'group_end')
+                                unset($oForm->aInputs[$CNF[$sKey]]['values'][$iKey]);
+                        }
+
+                        if($aValue['type'] == 'group_header') {
+                            $iGc = 0;
+                            $iKeyGh = $iKey;
+                        }
+
+                        continue;
+                    }
+
+                    //--- Show 'Public' privacy group only in Public post form. 
+                    if(!$bContext && $aValue['key'] == BX_DOL_PG_ALL) {
+                        $iGc += 1;
+                        continue;
+                    }
+
+                    //--- Show a default privacy groups in Profile (for Owner) post form.
+                    if($bContextOwner && ((int)$aValue['key'] >= 0 || $mixedContextId == $aValue['key'])) {
+                        $iGc += 1;
+                        continue;
+                    }
+
+                    unset($oForm->aInputs[$CNF[$sKey]]['values'][$iKey]);
+                }
+            }
+            else {
+                $oForm->aInputs[$CNF[$sKey]]['value'] = $mixedContextId;
+                $oForm->aInputs[$CNF[$sKey]]['type'] = 'hidden';
+            }
+        }
+
+        $sKey = 'FIELD_LABELS';
+        if(!empty($CNF[$sKey]) && !empty($oForm->aInputs[$CNF[$sKey]]) && empty($oForm->aInputs[$CNF[$sKey]]['value']) && $bContext) {
+            $iContextProfileId = $oContextProfile->id();
+            $sContextModule = $oContextProfile->getModule();
+            
+            if(bx_srv($sContextModule, 'is_fan', [$iContextProfileId, $iLoggedId])) {
+                $aContextInfo = bx_srv($sContextModule, 'get_content_info_by_profile_id', [$iContextProfileId]);
+                if(!empty($aContextInfo['hashtag']))
+                    $oForm->aInputs[$CNF[$sKey]]['value'] = [$aContextInfo['hashtag']];
+            }
+        }
+
+        bx_alert('system', 'get_object_form', 0, 0, array(
+            'module' => $this->_oConfig->getName(),
+            'type' => $sType,
+            'params' => $aParams,
+            'form' => &$oForm
+        ));
+
+        return $oForm;
+    }
+
+    /**
+     *
+     *
+     * Create entry form
+     * @return HTML string
+     */
+    public function serviceEntityCreate ($sParams = false)
+    {
+        $bParamsArray = is_array($sParams);
+
+        $sDisplay = is_string($sParams) ? $sParams : false;
+        if($bParamsArray && !empty($sParams['display']))
+            $sDisplay = $sParams['display'];
+
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'FormsEntryHelper';
+        $oFormsHelper = new $sClass($this);
+        if($bParamsArray && isset($sParams['dynamic_mode']))
+            $oFormsHelper->setDynamicMode($sParams['dynamic_mode']);
+
+        $mixedResult = $oFormsHelper->addDataForm($sDisplay);
+        
+        if (bx_is_api()){
+            return $mixedResult;
+        }
+        
+        if(isset($mixedResult['_dt']) && $mixedResult['_dt'] == 'json') {
+            echoJson($mixedResult);
+            exit;
+        }
+
+        return $mixedResult;
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-forms Forms
+     * @subsubsection bx_base_general-get_create_post_form get_create_post_form
+     * 
+     * @code bx_srv('bx_posts', 'get_create_post_form'); @endcode
+     * 
+     * Get content creation form
+     * @param $aParams additional parameters array: 
+     *                 context_id, ajax_mode, absolute_action_url, displays
+     * 
+     * @see BxBaseModGeneralModule::serviceGetCreatePostForm
+     */
+    /** 
+     * @ref bx_base_general-get_create_post_form "get_create_post_form"
+     */
+    public function serviceGetCreatePostForm($aParams = [])
+    {
+    	$aParams = array_merge($this->_aFormParams, $aParams);
+
+        if($aParams['context_id'] !== false && ($aContextInfo = BxDolProfileQuery::getInstance()->getInfoById(abs($aParams['context_id']))))
+            if(bx_srv($aContextInfo['type'], 'check_allowed_post_in_profile', [$aContextInfo['content_id'], $this->getName()]) !== CHECK_ACTION_RESULT_ALLOWED)
+                return '';
+
+    	$oForm = $this->serviceGetObjectForm('add', $aParams);
+    	if(!$oForm)
+            return '';
+
+    	return $this->serviceEntityCreate($aParams);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-forms Forms
+     * @subsubsection bx_base_general-entity_edit entity_edit
+     * 
+     * @code bx_srv('bx_posts', 'entity_edit', [...]); @endcode
+     * 
+     * Get content edit form
+     * @param $iContentId content ID
+     * @param $sDisplay optional form display name
+     * 
+     * @see BxBaseModGeneralModule::serviceEntityEdit
+     */
+    /** 
+     * @ref bx_base_general-entity_edit "entity_edit"
+     */
+    public function serviceEntityEdit ($iContentId = 0, $sDisplay = false)
+    {
+        $mixedResult = $this->_serviceEntityForm ('editDataForm', $iContentId, $sDisplay);
+        if(!bx_is_api()) 
+            return $mixedResult;
+
+        $aResult = [];
+        if(is_a($mixedResult, 'BxTemplFormView')){
+            $iContentId = $this->_getContent($iContentId, false);
+            $aResult = bx_api_get_block('form', $mixedResult->getCodeAPI(), ['ext' => ['name' => $this->getName(), 'request' => ['url' => '/api.php?r=' . $this->_aModule['name'] . '/entity_edit&params[]=' . $iContentId . '&params[]=' . $mixedResult->aParams['display'], 'immutable' => true]]]);
+        }
+        else
+            $aResult = $mixedResult;
+
+        return [
+            $aResult
+        ];
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-forms Forms
+     * @subsubsection bx_base_general-entity_delete entity_delete
+     * 
+     * @code bx_srv('bx_posts', 'entity_delete', [...]); @endcode
+     * 
+     * Get content delete form
+     * @param $iContentId content ID
+     * 
+     * @see BxBaseModGeneralModule::serviceEntityDelete
+     */
+    /** 
+     * @ref bx_base_general-entity_delete "entity_delete"
+     */
+    public function serviceEntityDelete ($iContentId = 0)
+    {
+        $mixedResult = $this->_serviceEntityForm ('deleteDataForm', $iContentId);
+        if(!bx_is_api()) 
+            return $mixedResult;
+
+        $aResult = [];
+        if(is_a($mixedResult, 'BxTemplFormView')){
+            $iContentId = $this->_getContent($iContentId, false);
+            $aResult = bx_api_get_block('form', $mixedResult->getCodeAPI(), ['ext' => ['name' => $this->getName(), 'request' => ['url' => '/api.php?r=' . $this->_aModule['name'] . '/entity_delete&params[]=' . $iContentId . '&params[]=' . $mixedResult->aParams['display'], 'immutable' => true]]]);
+        }
+        else
+            $aResult = $mixedResult;
+
+        return [
+            $aResult
+        ];
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-page_blocks Page Blocks
+     * @subsubsection bx_base_general-entity_text_block entity_text_block
+     * 
+     * @code bx_srv('bx_posts', 'entity_text_block', [...]); @endcode
+     * 
+     * Get content text/description
+     * @param $iContentId content ID
+     * 
+     * @see BxBaseModGeneralModule::serviceEntityTextBlock
+     */
+    /** 
+     * @ref bx_base_general-entity_text_block "entity_text_block"
+     */
+    public function serviceEntityTextBlock ($iContentId = 0)
+    {
+        return $this->_serviceEntityForm ('viewDataEntry', $iContentId);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-page_blocks Page Blocks
+     * @subsubsection bx_base_general-entity_info entity_info
+     * 
+     * @code bx_srv('bx_posts', 'entity_info', [...]); @endcode
+     * 
+     * Get content info
+     * @param $iContentId content ID
+     * 
+     * @see BxBaseModGeneralModule::serviceEntityInfo
+     */
+    /** 
+     * @ref bx_base_general-entity_info "entity_info"
+     */
+    public function serviceEntityInfo ($iContentId = 0, $sDisplay = false)
+    {
+        return $this->_serviceEntityForm ('viewDataForm', $iContentId, $sDisplay);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-page_blocks Page Blocks
+     * @subsubsection bx_base_general-entity_info_full entity_info_full
+     * 
+     * @code bx_srv('bx_posts', 'entity_info_full', [...]); @endcode
+     * 
+     * Get full content info
+     * @param $iContentId content ID
+     * 
+     * @see BxBaseModGeneralModule::serviceEntityInfoFull
+     */
+    /** 
+     * @ref bx_base_general-entity_info_full "entity_info_full"
+     */
+	public function serviceEntityInfoFull ($iContentId = 0)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	$sDisplayName = 'OBJECT_FORM_ENTRY_DISPLAY_VIEW_FULL';
+        return $this->_serviceEntityForm ('viewDataForm', $iContentId, !empty($CNF[$sDisplayName]) ? $CNF[$sDisplayName] : false);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-page_blocks Page Blocks
+     * @subsubsection bx_base_general-entity_info_extended entity_info_extended
+     * 
+     * @code bx_srv('bx_posts', 'entity_info_extended', [...]); @endcode
+     * 
+     * Get extended content info
+     * @param $iContentId content ID
+     * 
+     * @see BxBaseModGeneralModule::serviceEntityInfoExtended
+     */
+    /** 
+     * @ref bx_base_general-entity_info_extended "entity_info_extended"
+     */
+	public function serviceEntityInfoExtended ($iContentId = 0)
+    {
+        return $this->_serviceTemplateFunc ('entryInfo', $iContentId);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-page_blocks Page Blocks
+     * @subsubsection bx_base_general-entity_location entity_location
+     * 
+     * @code bx_srv('bx_posts', 'entity_location', [...]); @endcode
+     * 
+     * Get content location
+     * @param $iContentId content ID
+     * 
+     * @see BxBaseModGeneralModule::serviceEntityLocation
+     */
+    /** 
+     * @ref bx_base_general-entity_location "entity_location"
+     */
+    public function serviceEntityLocation ($iContentId = 0)
+    {
+        $iContentId = $this->_getContent($iContentId, false);
+        if($iContentId === false)
+            return false;
+
+        return $this->_oTemplate->entryLocation ($iContentId);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-page_blocks Page Blocks
+     * @subsubsection bx_base_general-entity_comments entity_comments
+     * 
+     * @code bx_srv('bx_posts', 'entity_comments', [...]); @endcode
+     * 
+     * Get content comments
+     * @param $iContentId content ID
+     * 
+     * @see BxBaseModGeneralModule::serviceEntityComments
+     */
+    /** 
+     * @ref bx_base_general-entity_comments "entity_comments"
+     */
+    public function serviceEntityComments ($iContentId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if(empty($CNF['OBJECT_COMMENTS']))
+            return '';
+
+        return $this->_entityComments($CNF['OBJECT_COMMENTS'], $iContentId);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-page_blocks Page Blocks
+     * @subsubsection bx_base_general-entity_attachments entity_attachments
+     * 
+     * @code bx_srv('bx_posts', 'entity_attachments', [...]); @endcode
+     * 
+     * Get content attachments
+     * @param $iContentId content ID
+     * 
+     * @see BxBaseModGeneralModule::serviceEntityAttachments
+     */
+    /** 
+     * @ref bx_base_general-entity_attachments "entity_attachments"
+     */
+    public function serviceEntityAttachments ($iContentId = 0)
+    {
+        return $this->_serviceTemplateFunc ('entryAttachments', $iContentId);
+    }
+    
+    /**
+     * Delete content entry
+     * @param $iContentId content id 
+     * @return error message or empty string on success
+     */
+    public function serviceDeleteEntity ($iContentId, $sFuncDelete = 'deleteData')
+    {
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_oConfig->getClassPrefix() . 'FormsEntryHelper';
+        $oFormsHelper = new $sClass($this);
+        return $oFormsHelper->$sFuncDelete($iContentId);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-menu Menu
+     * @subsubsection bx_base_general-my_entries_actions my_entries_actions
+     * 
+     * @code bx_srv('bx_posts', 'my_entries_actions', [...]); @endcode
+     * 
+     * My entries actions menu
+     * @param $iProfileId profiles ID, 
+     *        if omitted it try to get it from 'profile_id' GET param
+     * 
+     * @see BxBaseModGeneralModule::serviceMyEntriesActions
+     */
+    /** 
+     * @ref bx_base_general-my_entries_actions "my_entries_actions"
+     */
+    public function serviceMyEntriesActions ($iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if(empty($CNF['OBJECT_MENU_ACTIONS_MY_ENTRIES']))
+            return false;
+
+        if (!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if (!$iProfileId || !($oProfile = BxDolProfile::getInstance($iProfileId)))
+            return false;
+
+        if ($iProfileId != $this->_iProfileId)
+            return false;
+
+        $oMenu = BxTemplMenu::getObjectInstance($CNF['OBJECT_MENU_ACTIONS_MY_ENTRIES']);
+        return $oMenu ? $oMenu->getCode() : false;
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-menu Menu
+     * @subsubsection bx_base_general-entity_all_actions entity_all_actions
+     * 
+     * @code bx_srv('bx_posts', 'entity_all_actions', [...]); @endcode
+     * 
+     * Entry actions and social sharing actions
+     * @param $mixedContent content ID
+     * @param $aParams additional params
+     * 
+     * @see BxBaseModGeneralModule::serviceEntityAllActions
+     */
+    /** 
+     * @ref bx_base_general-entity_all_actions "entity_all_actions"
+     */
+    public function serviceEntityAllActions ($mixedContent = false, $aParams = array())
+    {
+        $mixedResult = $this->getEntryAllActions($mixedContent, $aParams);
+
+        if(bx_is_api())
+            return [bx_api_get_block('entity_actions', $mixedResult)];
+        else
+            return $this->_oTemplate->entryAllActions($mixedResult);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-menu Menu
+     * @subsubsection bx_base_general-entity_actions entity_actions
+     * 
+     * @code bx_srv('bx_posts', 'entity_actions', [...]); @endcode
+     * 
+     * Entry actions, without social sharing actions
+     * @param $iContentId content ID
+     * 
+     * @see BxBaseModGeneralModule::serviceEntityActions
+     */
+    /** 
+     * @ref bx_base_general-entity_actions "entity_actions"
+     */
+    public function serviceEntityActions ($iContentId = 0)
+    {
+        $iContentId = $this->_getContent($iContentId, false);
+        if($iContentId === false)
+            return false;
+
+        $oMenu = BxTemplMenu::getObjectInstance($this->_oConfig->CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY']);
+        return $oMenu ? $oMenu->getCode() : false;
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-page_blocks Page Blocks
+     * @subsubsection bx_base_general-entity_reports entity_reports
+     * 
+     * @code bx_srv('bx_posts', 'entity_reports', [...]); @endcode
+     * 
+     * Get content reports
+     * @param $iContentId content ID
+     * 
+     * @see BxBaseModGeneralModule::serviceEntityReports
+     */
+    /** 
+     * @ref bx_base_general-entity_reports "entity_reports"
+     */
+    public function serviceEntityReports ($iContentId = 0)
+    {
+        $mixedContent = $this->_getContent($iContentId);
+        list($iContentId, $aContentInfo) = $mixedContent;
+        
+        if($iContentId === false)
+            return false;
+        
+        $CNF = &$this->_oConfig->CNF;
+        
+        if (!isset($CNF['OBJECT_REPORTS']) || !isset($CNF['OBJECT_NOTES']))
+            return false;
+
+        if(!$aContentInfo || !(BxDolAcl::getInstance()->isMemberLevelInSet(192) || (isset($CNF['FIELD_AUTHOR']) && bx_get_logged_profile_id() == $aContentInfo[$CNF['FIELD_AUTHOR']])))
+            return false;
+
+        $oReport = BxDolReport::getObjectInstance($CNF['OBJECT_REPORTS'], $iContentId, true);
+        if(!$oReport)
+            return false;
+
+        return $oReport->getReportedByWithComments($CNF['OBJECT_NOTES']);
+    }
+    
+     /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-page_blocks Page Blocks
+     * @subsubsection bx_base_general-reports_count_by_status reports_count_by_status
+     * 
+     * @code bx_srv('bx_posts', 'reports_count_by_status', [...]); @endcode
+     * 
+     * Get number of reports with specified status
+     * @param $iStatus status ID
+     * 
+     * @see BxBaseModGeneralModule::serviceReportsCountByStatus
+     */
+    /** 
+     * @ref bx_base_general-entity_reports "entity_reports"
+     */
+    public function serviceReportsCountByStatus ($iStatus)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+        if (!isset($CNF['OBJECT_REPORTS']) || !isset($CNF['OBJECT_NOTES']))
+            return false;
+        
+        $oReport = BxDolReport::getObjectInstance($CNF['OBJECT_REPORTS'], 0, false);
+        return $oReport->getCountByStatus($iStatus);
+    }
+    
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-menu Menu
+     * @subsubsection bx_base_general-entity_social_sharing entity_social_sharing
+     * 
+     * @code bx_srv('bx_posts', 'entity_social_sharing', [...]); @endcode
+     * 
+     * Entry social sharing actions
+     * @param $mixedContent content ID or array with integer content ID and 
+     *                      array with content info, or false
+     * @param $aParams array with additional custom params 
+     *                 which may overwrite some default values
+     * 
+     * @see BxBaseModGeneralModule::serviceEntitySocialSharing
+     */
+    /** 
+     * @ref bx_base_general-entity_social_sharing "entity_social_sharing"
+     */
+    public function serviceEntitySocialSharing($mixedContent = false, $aParams = array())
+    {
+        if(!empty($mixedContent)) {
+            if(!is_array($mixedContent))
+               $mixedContent = array((int)$mixedContent, array());
+        }
+        else {
+            $mixedContent = $this->_getContent();
+            if($mixedContent === false)
+                return false;
+        }
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        $CNF = &$this->_oConfig->CNF;
+
+        $sUri = !empty($aParams['uri']) ? $aParams['uri'] : '';
+        if(empty($sUri) && !empty($CNF['URI_VIEW_ENTRY']))
+            $sUri = $CNF['URI_VIEW_ENTRY'];
+
+        $sUrl = !empty($sUri) ? BxDolPermalinks::getInstance()->permalink('page.php?i=' . $sUri . '&id=' . $iContentId) : '';
+
+        $sTitle = !empty($aParams['title']) ? $aParams['title'] : '';
+        if(empty($sTitle) && !empty($aContentInfo[$CNF['FIELD_TITLE']]))
+            $sTitle = $aContentInfo[$CNF['FIELD_TITLE']];
+
+        $aMarkers = array(
+            'id' => $iContentId,
+            'module' => $this->_aModule['name'],
+            'url' => bx_absolute_url($sUrl),
+            'title' => $sTitle,
+        );
+
+        $iIdThumb = !empty($aParams['id_thumb']) ? (int)$aParams['id_thumb'] : 0;
+        if(empty($iIdThumb) && !empty($CNF['FIELD_THUMB']) && !empty($aContentInfo[$CNF['FIELD_THUMB']]))
+            $iIdThumb = (int)$aContentInfo[$CNF['FIELD_THUMB']];
+
+        if ($iIdThumb) {
+            $sTranscoder = !empty($aParams['object_transcoder']) ? $aParams['object_transcoder'] : '';
+            $sStorage = !empty($aParams['object_storage']) ? $aParams['object_storage'] : '';
+            if(empty($sStorage) && !empty($CNF['OBJECT_STORAGE']))
+                $sStorage = $CNF['OBJECT_STORAGE'];
+
+            if(!empty($sTranscoder))
+                $o = BxDolTranscoder::getObjectInstance($sTranscoder);
+            else if(!empty($sStorage))
+                $o = BxDolStorage::getObjectInstance($sStorage);
+
+            $sImgUrl = $o ? $o->getFileUrlById($iIdThumb) : '';
+            if($sImgUrl)
+                $aMarkers['img_url'] = $sImgUrl;
+        }
+
+        $oMenu = BxDolMenu::getObjectInstance('sys_social_sharing');
+        $oMenu->addMarkers($aMarkers);
+        $sMenu = $oMenu->getCode();
+
+        if(empty($sMenu))
+            return '';
+
+        return $this->_oTemplate->parseHtmlByName('entry-share.html', array(
+            'menu' => $sMenu
+        ));
+    }
+    
+    /**
+     * Entry context block
+     */
+    public function serviceEntityContext ($iContentId = 0)
+    {
+        return $this->_serviceTemplateFunc ('entryContext', $iContentId);
+    }
+
+    /**
+     * Blocks for lables tree browsing
+     */
+    public function serviceGetLablesTree()
+    {
+		$aBreadcrumbs = array();
+		$this->_getLablesBreadcrumbsArray(bx_get('label'), $aBreadcrumbs);
+		return $this->_getLablesTreeLevel($aBreadcrumbs);
+	}	
+    
+    public function serviceGetLablesBreadcrumbs()
+    {
+		if (bx_get('label')){
+            $aBreadcrumbs = array();
+            $this->_getLablesBreadcrumbsArray(bx_process_input(bx_get('label'), BX_DATA_INT), $aBreadcrumbs);
+            $aLabels = array();
+            foreach (array_reverse($aBreadcrumbs) as $aLabel){
+                $aLabels[] = array(
+                   'value' => $aLabel['value'], 
+                   'url' => $this->_getLablesBrowseUrl($aLabel['id'])
+               );
+            }
+            
+            return $this->_oTemplate->parseHtmlByName('labels_breadcrumbs.html', array('bx_repeat:items' => $aLabels));
+		}
+        return '';
+	}
+    
+    public function serviceBrowseByLabel()
+    {   
+        $CNF = &$this->_oConfig->CNF;
+        if (empty($CNF['OBJECT_METATAGS']))
+            return '';
+        
+		$sMode = 'recent';
+		if(bx_srv('system', 'is_module_content', [$this->_aModule['name']]))
+		   $sMode = 'public';
+		   
+		$sClassSearchResult ='SearchResult';
+		
+		bx_import($sClassSearchResult, $this->_aModule['name']);
+        $sClass = $this->_aModule['class_prefix'] . $sClassSearchResult;
+		
+        $o = new $sClass($sMode, false);
+		$o->setDesignBoxTemplateId(BX_DB_PADDING_DEF);
+        $o->setDisplayEmptyMsg(true);
+        $o->setAjaxPaginate(false);
+        $o->setUnitParams(array('context' => $sMode));
+		
+		if (bx_get('label')){
+			$iLabelId = (int)bx_get('label');
+			$oLabel = BxDolLabel::getInstance();
+			$aLabel = $oLabel->getLabels(array('type' => 'id', 'id' => $iLabelId));
+			$oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+			$sTmp =$oMetatags->keywordsSetSearchCondition($o, $aLabel['value']);
+		}
+		
+        if ($o->isError)
+            return '';
+
+        if ($s = $o->processing())
+            return $s;
+        else
+            return '';
+	}
+    
+    public function serviceBrowseByCategories($sUnitView, $bEmptyMessage, $bAjaxPaginate, $sMode, $iPerPage)
+    {   
+        $CNF = &$this->_oConfig->CNF;
+        $sClassSearchResult ='SearchResult';
+		
+		bx_import($sClassSearchResult, $this->_aModule['name']);
+        $sClass = $this->_aModule['class_prefix'] . $sClassSearchResult;
+        $o = new $sClass($sMode, array('unit_view' => $sUnitView, 'paginate' => array('perPage' => 10, 'start' => 0, 'num' => 11)));
+		$o->setDesignBoxTemplateId(BX_DB_PADDING_DEF);
+        $o->setAjaxPaginate($bAjaxPaginate);
+        $o->setCategoryObject('multi');
+        
+        $aCategoriesOutput = array();
+		$aCategories = BxDolCategories::getInstance()->getData(array('type' => 'by_module_with_num', 'module' => $this->_aModule['name']));
+        
+        foreach($aCategories as $aCategory){
+            if ($aCategory['num'] > 0){
+                $o->setCustomSearchCondition(array('keyword' => $aCategory['value']));
+                $o->setPaginatePerPage(2);
+                if (!$o->isError){
+                    $aResult = $o->processing();
+                    if ($aResult && $aResult['content'] != '')
+                        $aCategoriesOutput[] =  array('name' => _t($aCategory['value']), 'url' => '', 'content' => $aResult['content']);
+                }
+            }
+        }
+
+        return $this->_oTemplate->parseHtmlByName('browse_by_categories.html', array(
+            'bx_repeat:categories' => $aCategoriesOutput,
+        ));
+
+	}
+    
+    /**
+     * Data for Notifications module
+     */
+    public function serviceGetNotificationsData()
+    {
+    	$sModule = $this->_aModule['name'];
+
+    	$sEventPrivacy = $sModule . '_allow_view_event_to';
+        if(BxDolPrivacy::getObjectInstance($sEventPrivacy) === false)
+            $sEventPrivacy = '';
+
+        $aResult = [
+            'handlers' => [
+                ['group' => $sModule . '_object', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'added', 'module_name' => $sModule, 'module_method' => 'get_notifications_post', 'module_class' => 'Module', 'module_event_privacy' => $sEventPrivacy],
+                ['group' => $sModule . '_object', 'type' => 'update', 'alert_unit' => $sModule, 'alert_action' => 'edited'],
+                ['group' => $sModule . '_object', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'deleted'],
+
+                ['group' => $sModule . '_object_approved', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'approved', 'module_name' => $sModule, 'module_method' => 'get_notifications_post_approved', 'module_class' => 'Module', 'module_event_privacy' => $sEventPrivacy],
+
+                ['group' => $sModule . '_comment', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'commentPost', 'module_name' => $sModule, 'module_method' => 'get_notifications_comment', 'module_class' => 'Module', 'module_event_privacy' => $sEventPrivacy],
+                ['group' => $sModule . '_comment', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'commentRemoved'],
+
+                ['group' => $sModule . '_reply', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'replyPost', 'module_name' => $sModule, 'module_method' => 'get_notifications_reply', 'module_class' => 'Module', 'module_event_privacy' => $sEventPrivacy],
+                ['group' => $sModule . '_reply', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'replyRemoved'],
+
+                ['group' => $sModule . '_vote', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'doVote', 'module_name' => $sModule, 'module_method' => 'get_notifications_vote', 'module_class' => 'Module', 'module_event_privacy' => $sEventPrivacy],
+                ['group' => $sModule . '_vote', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'undoVote'],
+
+                ['group' => $sModule . '_reaction', 'type' => 'insert', 'alert_unit' => $sModule . '_reactions', 'alert_action' => 'doVote', 'module_name' => $sModule, 'module_method' => 'get_notifications_reaction', 'module_class' => 'Module', 'module_event_privacy' => $sEventPrivacy],
+                ['group' => $sModule . '_reaction', 'type' => 'delete', 'alert_unit' => $sModule . '_reactions', 'alert_action' => 'undoVote'],
+
+                ['group' => $sModule . '_score_up', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'doVoteUp', 'module_name' => $sModule, 'module_method' => 'get_notifications_score_up', 'module_class' => 'Module', 'module_event_privacy' => $sEventPrivacy],
+
+                ['group' => $sModule . '_score_down', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'doVoteDown', 'module_name' => $sModule, 'module_method' => 'get_notifications_score_down', 'module_class' => 'Module', 'module_event_privacy' => $sEventPrivacy],
+
+                //--- Moderation related: For 'admins'.
+                ['group' => $sModule . '_object_pending_approval', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'pending_approval', 'module_name' => $sModule, 'module_method' => 'get_notifications_post_pending_approval', 'module_class' => 'Module', 'module_event_privacy' => $sEventPrivacy],
+            ],
+            'settings' => [
+                ['group' => 'content', 'unit' => $sModule, 'action' => 'added', 'types' => ['follow_member', 'follow_context']],
+                ['group' => 'content_updated', 'unit' => $sModule, 'action' => 'approved', 'types' => ['personal']],
+                ['group' => 'comment', 'unit' => $sModule, 'action' => 'commentPost', 'types' => ['personal', 'follow_member', 'follow_context']],
+                ['group' => 'reply', 'unit' => $sModule, 'action' => 'replyPost', 'types' => ['personal']],
+                ['group' => 'vote', 'unit' => $sModule, 'action' => 'doVote', 'types' => ['personal', 'follow_member', 'follow_context']],
+                ['group' => 'vote', 'unit' => $sModule . '_reactions', 'action' => 'doVote', 'types' => ['personal', 'follow_member', 'follow_context']],
+                ['group' => 'score_up', 'unit' => $sModule, 'action' => 'doVoteUp', 'types' => ['personal', 'follow_member', 'follow_context']],
+                ['group' => 'score_down', 'unit' => $sModule, 'action' => 'doVoteDown', 'types' => ['personal', 'follow_member', 'follow_context']],
+
+                //--- Moderation related: For 'admins'.
+                ['group' => 'action_required', 'unit' => $sModule, 'action' => 'pending_approval', 'types' => ['personal']],
+            ],
+            'alerts' => [
+                ['unit' => $sModule, 'action' => 'added'],
+                ['unit' => $sModule, 'action' => 'edited'],
+                ['unit' => $sModule, 'action' => 'deleted'],
+
+                ['unit' => $sModule, 'action' => 'approved'],
+
+                ['unit' => $sModule, 'action' => 'commentPost'],
+                ['unit' => $sModule, 'action' => 'commentRemoved'],
+
+                ['unit' => $sModule, 'action' => 'replyPost'],
+                ['unit' => $sModule, 'action' => 'replyRemoved'],
+
+                ['unit' => $sModule, 'action' => 'doVote'],
+                ['unit' => $sModule, 'action' => 'undoVote'],
+
+                ['unit' => $sModule . '_reactions', 'action' => 'doVote'],
+                ['unit' => $sModule . '_reactions', 'action' => 'undoVote'],
+
+                ['unit' => $sModule, 'action' => 'doVoteUp'],
+                ['unit' => $sModule, 'action' => 'doVoteDown'],
+
+                //--- Moderation related: For 'admins'.
+                ['unit' => $sModule, 'action' => 'pending_approval'],
+            ]
+        ];
+
+        if(!empty($this->_oConfig->CNF['FIELDS_DELAYED_PROCESSING'])) {
+            $aResult['handlers'] = array_merge($aResult['handlers'], [
+                ['group' => $sModule . '_object_publish_failed', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'publish_failed', 'module_name' => $sModule, 'module_method' => 'get_notifications_post_publish_failed', 'module_class' => 'Module', 'module_event_privacy' => ''],
+                ['group' => $sModule . '_object_publish_succeeded', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'publish_succeeded', 'module_name' => $sModule, 'module_method' => 'get_notifications_post_publish_succeeded', 'module_class' => 'Module', 'module_event_privacy' => ''],
+            ]);
+
+            $aResult['settings'] = array_merge($aResult['settings'], [
+                ['group' => 'content_updated', 'unit' => $sModule, 'action' => 'publish_failed', 'types' => ['personal']],
+                ['group' => 'content_updated', 'unit' => $sModule, 'action' => 'publish_succeeded', 'types' => ['personal']],
+            ]);
+
+            $aResult['alerts'] = array_merge($aResult['alerts'], [
+                ['unit' => $sModule, 'action' => 'publish_failed'],
+                ['unit' => $sModule, 'action' => 'publish_succeeded'],
+            ]);
+        }
+
+        return $aResult;
+    }
+
+    /**
+     * Entry post for Notifications module
+     */
+    public function serviceGetNotificationsPost($aEvent)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($aEvent['object_id']);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return [];
+
+        $sEntryUrl = bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]), '{bx_url_root}');
+        $sEntryCaption = isset($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : strmaxtextlen($aContentInfo[$CNF['FIELD_TEXT']], 20, '...');
+        $sEntrySummary = isset($aContentInfo[$CNF['FIELD_TEXT']]) ? $aContentInfo[$CNF['FIELD_TEXT']] : '';
+
+        return [
+            'entry_sample' => $CNF['T']['txt_sample_single'],
+            'entry_url' => $sEntryUrl,
+            'entry_caption' => $sEntryCaption,
+            'entry_summary' => $sEntrySummary,
+            'entry_author' => $aContentInfo[$CNF['FIELD_AUTHOR']],
+            'entry_privacy' => '', //may be empty or not specified. In this case Public privacy will be used.
+            'lang_key' => '', //may be empty or not specified, or a string, or an array('site' => '...', 'email' => '...', 'push' => '...'). In this case of empty/not specified the default one from Notification module will be used.
+            /*
+             * Custom settings for email and/or push notifications can be provided here. 
+             * Only necessary parts of 'settings' array can be used.
+             * 
+            'settings' => array(
+                'email' => array(
+                    'template' => '',   //--- custom email template
+                    'markers' => '',    //--- markers to parse email parts (subject, body) with
+                    'subject' => ''     //--- custom email subject
+                ),
+                'push' => array(
+                    'subject' => ''     //--- custom push notification subject
+                )
+            )
+             */
+        ];
+    }
+
+    public function serviceGetNotificationsPostPendingApproval($aEvent)
+    {
+        $aResult = $this->serviceGetNotificationsPost($aEvent);
+
+        /**
+         * Unset Entry Author because the notification is addressed to 'admins'.
+         */
+        if(isset($aResult['entry_author']))
+            unset($aResult['entry_author']);
+
+        return $aResult;
+    }
+
+    public function serviceGetNotificationsPostApproved($aEvent)
+    {
+        return $this->serviceGetNotificationsPost($aEvent);
+    }
+
+    public function serviceGetNotificationsPostPublishFailed($aEvent)
+    {
+        return $this->serviceGetNotificationsPost($aEvent);
+    }
+
+    public function serviceGetNotificationsPostPublishSucceeded($aEvent)
+    {
+        return $this->serviceGetNotificationsPost($aEvent);
+    }
+
+
+    /**
+     * Entry post comment for Notifications module
+     */
+    public function serviceGetNotificationsComment($aEvent)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	$iContentId = (int)$aEvent['object_id'];
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return [];
+
+        $oComment = BxDolCmts::getObjectInstance($CNF['OBJECT_COMMENTS'], $iContentId);
+        if(!$oComment || !$oComment->isEnabled())
+            return [];
+
+        $sEntryUrl = bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]), '{bx_url_root}');
+        $sEntryCaption = isset($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : strmaxtextlen($aContentInfo[$CNF['FIELD_TEXT']], 20, '...');
+        $sEntrySummary = isset($aContentInfo[$CNF['FIELD_TEXT']]) ? $aContentInfo[$CNF['FIELD_TEXT']] : '';
+
+        $iCommentId = (int)$aEvent['subobject_id'];
+
+        return [
+            'entry_sample' => $CNF['T']['txt_sample_single'],
+            'entry_url' => $sEntryUrl,
+            'entry_caption' => $sEntryCaption,
+            'entry_summary' => $sEntrySummary,
+            'entry_author' => $aContentInfo[$CNF['FIELD_AUTHOR']],
+            'subentry_sample' => $CNF['T']['txt_sample_comment_single'],
+            'subentry_url' => bx_absolute_url($oComment->getViewUrl($iCommentId, false), '{bx_url_root}'),
+            'subentry_summary' => $oComment->getViewText($iCommentId),
+            'lang_key' => '', //may be empty or not specified. In this case the default one from Notification module will be used.
+        ];
+    }
+
+	/**
+     * Entry post reply for Notifications module
+     */
+    public function serviceGetNotificationsReply($aEvent)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	$oComment = BxDolCmts::getObjectInstance($CNF['OBJECT_COMMENTS'], 0, false);
+        if(!$oComment || !$oComment->isEnabled())
+            return [];
+
+    	$iParentId = (int)$aEvent['object_id'];
+        $aParentInfo = $oComment->getQueryObject()->getCommentsBy(array('type' => 'id', 'id' => $iParentId));
+        if(empty($aParentInfo) || !is_array($aParentInfo))
+            return [];
+
+        $iObjectId = (int)$aParentInfo['cmt_object_id'];
+        $oComment->init($iObjectId);
+
+        $iCommentId = (int)$aEvent['subobject_id'];
+
+        return [
+            'object_id' => $iObjectId,
+            'entry_sample' => '_cmt_txt_sample_comment_single',
+            'entry_url' => bx_absolute_url($oComment->getViewUrl($iParentId, false), '{bx_url_root}'),
+            'entry_caption' => strmaxtextlen($aParentInfo['cmt_text'], 20, '...'),
+            'entry_summary' => $oComment->getViewText($iParentId),
+            'entry_author' => (int)$aParentInfo['cmt_author_id'],
+            'subentry_sample' => '_cmt_txt_sample_reply_to',
+            'subentry_url' => bx_absolute_url($oComment->getViewUrl($iCommentId, false), '{bx_url_root}'),
+            'subentry_summary' => $oComment->getViewText($iCommentId),
+            'lang_key' => '', //may be empty or not specified. In this case the default one from Notification module will be used.
+        ];
+    }
+
+	/**
+     * Entry post vote for Notifications module
+     */
+    public function serviceGetNotificationsVote($aEvent)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $iContentId = (int)$aEvent['object_id'];
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return [];
+
+        $oVote = BxDolVote::getObjectInstance($CNF['OBJECT_VOTES'], $iContentId);
+        if(!$oVote || !$oVote->isEnabled())
+            return [];
+
+        $sEntryUrl = bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]), '{bx_url_root}');
+        $sEntryCaption = isset($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : strmaxtextlen($aContentInfo[$CNF['FIELD_TEXT']], 20, '...');
+        $sEntrySummary = isset($aContentInfo[$CNF['FIELD_TEXT']]) ? $aContentInfo[$CNF['FIELD_TEXT']] : '';
+
+        return [
+            'entry_sample' => $CNF['T']['txt_sample_single'],
+            'entry_url' => $sEntryUrl,
+            'entry_caption' => $sEntryCaption,
+            'entry_summary' => $sEntrySummary,
+            'entry_author' => $aContentInfo[$CNF['FIELD_AUTHOR']],
+            'subentry_sample' => $CNF['T']['txt_sample_vote_single'],
+            'lang_key' => '', //may be empty or not specified. In this case the default one from Notification module will be used.
+        ];
+    }
+
+    /**
+     * Entry post vote for Notifications module
+     */
+    public function serviceGetNotificationsReaction($aEvent)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	$iContentId = (int)$aEvent['object_id'];
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return [];
+
+        $oReaction = BxDolVote::getObjectInstance($CNF['OBJECT_REACTIONS'], $iContentId);
+        if(!$oReaction || !$oReaction->isEnabled())
+            return [];
+
+        $aSubentry = $oReaction->getTrackBy(array('type' => 'id', 'id' => (int)$aEvent['subobject_id']));
+        if(empty($aSubentry) || !is_array($aSubentry))
+            return [];
+
+        $aSubentrySampleParams = array();
+        $aReaction = $oReaction->getReaction($aSubentry['reaction']);
+        if(!empty($aReaction['title']))
+            $aSubentrySampleParams[] = $aReaction['title'];
+        else
+            $aSubentrySampleParams[] = '_undefined';
+
+        $sEntryUrl = bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]), '{bx_url_root}');
+        $sEntryCaption = isset($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : strmaxtextlen($aContentInfo[$CNF['FIELD_TEXT']], 20, '...');
+        $sEntrySummary = isset($aContentInfo[$CNF['FIELD_TEXT']]) ? $aContentInfo[$CNF['FIELD_TEXT']] : '';
+
+        return [
+            'entry_sample' => $CNF['T']['txt_sample_single'],
+            'entry_url' => $sEntryUrl,
+            'entry_caption' => $sEntryCaption,
+            'entry_summary' => $sEntrySummary,
+            'entry_author' => $aContentInfo[$CNF['FIELD_AUTHOR']],
+            'subentry_sample' => $CNF['T']['txt_sample_reaction_single'],
+            'subentry_sample_params' => $aSubentrySampleParams,
+            'lang_key' => '', //may be empty or not specified. In this case the default one from Notification module will be used.
+        ];
+    }
+
+    /**
+     * Entry post score -> vote up for Notifications module
+     */
+    public function serviceGetNotificationsScoreUp($aEvent)
+    {
+    	return $this->_serviceGetNotificationsScore('up', $aEvent);
+    }
+
+    /**
+     * Entry post score -> vote up for Notifications module
+     */
+    public function serviceGetNotificationsScoreDown($aEvent)
+    {
+    	return $this->_serviceGetNotificationsScore('down', $aEvent);
+    }
+
+    protected function _serviceGetNotificationsScore($sType, $aEvent)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	$iContentId = (int)$aEvent['object_id'];
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return array();
+
+        $oScore = BxDolScore::getObjectInstance($CNF['OBJECT_SCORES'], $iContentId);
+        if(!$oScore || !$oScore->isEnabled())
+            return array();
+
+        $sEntryUrl = bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]), '{bx_url_root}');
+        $sEntryCaption = isset($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : strmaxtextlen($aContentInfo[$CNF['FIELD_TEXT']], 20, '...');
+        $sEntrySummary = isset($aContentInfo[$CNF['FIELD_TEXT']]) ? $aContentInfo[$CNF['FIELD_TEXT']] : '';
+
+        return [
+            'entry_sample' => $CNF['T']['txt_sample_single'],
+            'entry_url' => $sEntryUrl,
+            'entry_caption' => $sEntryCaption,
+            'entry_summary' => $sEntrySummary,
+            'entry_author' => $aContentInfo[$CNF['FIELD_AUTHOR']],
+            'subentry_sample' => $CNF['T']['txt_sample_score_' . $sType . '_single'],
+            'lang_key' => '', //may be empty or not specified. In this case the default one from Notification module will be used.
+        ];
+    }
+
+    /**
+     * Data for Timeline module
+     */
+    public function serviceGetTimelineData()
+    {
+    	$sModule = $this->_aModule['name'];
+
+        return array(
+            'handlers' => array(
+                array('group' => $sModule . '_object', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'added', 'module_name' => $sModule, 'module_method' => 'get_timeline_post', 'module_class' => 'Module',  'groupable' => 0, 'group_by' => ''),
+                array('group' => $sModule . '_object', 'type' => 'update', 'alert_unit' => $sModule, 'alert_action' => 'edited'),
+                array('group' => $sModule . '_object', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'deleted')
+            ),
+            'alerts' => array(
+                array('unit' => $sModule, 'action' => 'added'),
+                array('unit' => $sModule, 'action' => 'edited'),
+                array('unit' => $sModule, 'action' => 'deleted'),
+            )
+        );
+    }
+
+    /**
+     * Entry post for Timeline module
+     */
+    public function serviceGetTimelinePost($aEvent, $aBrowseParams = array())
+    {
+        $aContentInfo = $this->_oDb->getContentInfoById($aEvent['object_id']);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return false;
+
+        $CNF = &$this->_oConfig->CNF;
+
+        if((!empty($CNF['FIELD_STATUS']) && $aContentInfo[$CNF['FIELD_STATUS']] != 'active') || (!empty($CNF['FIELD_STATUS_ADMIN']) && $aContentInfo[$CNF['FIELD_STATUS_ADMIN']] != 'active'))
+            return false;
+
+        $iUserId = $this->getUserId();
+        $iAuthorId = (int)$aContentInfo[$CNF['FIELD_AUTHOR']];
+        $iAuthorIdAbs = abs($iAuthorId);
+
+        /**
+         * Don't show anonymous posts on the post's owner timeline.
+         */
+        if($iAuthorId < 0 && $iAuthorIdAbs != $iUserId && ((is_numeric($aEvent['owner_id']) && $iAuthorIdAbs == (int)$aEvent['owner_id']) || (is_array($aEvent['owner_id']) && in_array($iAuthorIdAbs, $aEvent['owner_id']))))
+            return false;
+
+        //--- Views
+        $oViews = isset($CNF['OBJECT_VIEWS']) ? BxDolView::getObjectInstance($CNF['OBJECT_VIEWS'], $aContentInfo[$CNF['FIELD_ID']]) : null;
+
+        $aViews = array();
+        if ($oViews && $oViews->isEnabled())
+            $aViews = array(
+                'system' => $CNF['OBJECT_VIEWS'],
+                'object_id' => $aContentInfo[$CNF['FIELD_ID']],
+                'count' => $aContentInfo['views']
+            );
+
+        //--- Votes
+        $oVotes = isset($CNF['OBJECT_VOTES']) ? BxDolVote::getObjectInstance($CNF['OBJECT_VOTES'], $aContentInfo[$CNF['FIELD_ID']]) : null;
+
+        $aVotes = array();
+        if ($oVotes && $oVotes->isEnabled())
+            $aVotes = array(
+                'system' => $CNF['OBJECT_VOTES'],
+                'object_id' => $aContentInfo[$CNF['FIELD_ID']],
+                'count' => $aContentInfo['votes']
+            );
+        
+        //--- Reactions
+        $oReactions = isset($CNF['OBJECT_REACTIONS']) ? BxDolVote::getObjectInstance($CNF['OBJECT_REACTIONS'], $aContentInfo[$CNF['FIELD_ID']]) : null;
+
+        $aReactions = array();
+        if ($oReactions && $oReactions->isEnabled())
+            $aReactions = array(
+                'system' => $CNF['OBJECT_REACTIONS'],
+                'object_id' => $aContentInfo[$CNF['FIELD_ID']],
+                'count' => $aContentInfo['rvotes']
+            );
+
+        //--- Scores
+        $oScores = isset($CNF['OBJECT_SCORES']) ? BxDolScore::getObjectInstance($CNF['OBJECT_SCORES'], $aContentInfo[$CNF['FIELD_ID']]) : null;
+
+        $aScores = array();
+        if ($oScores && $oScores->isEnabled())
+            $aScores = array(
+                'system' => $CNF['OBJECT_SCORES'],
+                'object_id' => $aContentInfo[$CNF['FIELD_ID']],
+                'score' => $aContentInfo['score']
+            );
+
+        //--- Reports
+        $oReports = isset($CNF['OBJECT_REPORTS']) ? BxDolReport::getObjectInstance($CNF['OBJECT_REPORTS'], $aContentInfo[$CNF['FIELD_ID']]) : null;
+
+        $aReports = array();
+        if ($oReports && $oReports->isEnabled())
+            $aReports = array(
+                'system' => $CNF['OBJECT_REPORTS'],
+                'object_id' => $aContentInfo[$CNF['FIELD_ID']],
+                'count' => $aContentInfo['reports']
+            );
+
+        //--- Comments
+        $oCmts = isset($CNF['OBJECT_COMMENTS']) ? BxDolCmts::getObjectInstance($CNF['OBJECT_COMMENTS'], $aContentInfo[$CNF['FIELD_ID']]) : null;
+
+        $aComments = array();
+        if($oCmts && $oCmts->isEnabled())
+            $aComments = array(
+                'system' => $CNF['OBJECT_COMMENTS'],
+                'object_id' => $aContentInfo[$CNF['FIELD_ID']],
+                'count' => $aContentInfo['comments']
+            );
+
+        //--- Title & Description
+        $sTitle = !empty($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : '';
+        if(empty($sTitle) && !empty($aContentInfo[$CNF['FIELD_TEXT']]))
+            $sTitle = $aContentInfo[$CNF['FIELD_TEXT']];
+
+        $iOwnerId = $iAuthorIdAbs;
+        if(isset($aEvent['object_privacy_view']) && (int)$aEvent['object_privacy_view'] < 0)
+            $iOwnerId = abs($aEvent['object_privacy_view']);
+
+        $bCache = true;
+        $aContent = $this->_getContentForTimelinePost($aEvent, $aContentInfo, $aBrowseParams);
+        if(isset($aContent['_cache'])) {
+            $bCache = (bool)$aContent['_cache'];
+
+            unset($aContent['_cache']);
+        }
+
+        return array(
+            '_cache' => $bCache,
+            'owner_id' => $iOwnerId,
+            'object_owner_id' => $iAuthorId,
+            'icon' => !empty($CNF['ICON']) ? $CNF['ICON'] : '',
+            'sample' => isset($CNF['T']['txt_sample_single_with_article']) ? $CNF['T']['txt_sample_single_with_article'] : $CNF['T']['txt_sample_single'],
+            'sample_wo_article' => $CNF['T']['txt_sample_single'],
+            'sample_action' => isset($CNF['T']['txt_sample_action']) ? $CNF['T']['txt_sample_action'] : '',
+            'url' => bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']])),
+            'content' => $aContent, //a string to display or array to parse default template before displaying.
+            'date' => $aContentInfo[$CNF['FIELD_ADDED']],
+            'views' => $aViews,
+            'votes' => $aVotes,
+            'reactions' => $aReactions,
+            'scores' => $aScores,
+            'reports' => $aReports,
+            'comments' => $aComments,
+            'title' => $sTitle, //may be empty.
+            'description' => '' //may be empty.
+        );
+    }
+
+    public function serviceGetTimelinePostAllowedView($aEvent)
+    {
+        $iContentId = (int)$aEvent['object_id'];
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return _t('_sys_txt_access_denied');
+
+        return $this->serviceCheckAllowedViewForProfile($aContentInfo);
+    }
+
+    /**
+     * Check particular action permission without content
+     * @param $sAction action to check, for example: Browse, Add
+     * @param $iContentId content ID
+     * @return message on error, or CHECK_ACTION_RESULT_ALLOWED when allowed
+     */ 
+    public function serviceCheckAllowed($sAction, $isPerformAction = false)
+    {
+        $sMethod = 'checkAllowed' . bx_gen_method_name($sAction);
+        if (!method_exists($this, $sMethod))
+            return _t('_sys_request_method_not_found_cpt');
+
+        return $this->$sMethod($isPerformAction);
+    }
+    
+    /**
+     * Check particular action permission with content
+     * @param $sAction action to check, for example: View, Edit
+     * @param $iContentId content ID
+     * @return message on error, or CHECK_ACTION_RESULT_ALLOWED when allowed
+     */ 
+    public function serviceCheckAllowedWithContent($sAction, $iContentId, $isPerformAction = false)
+    {
+        if (!$iContentId || !($aContentInfo = $this->_oDb->getContentInfoById($iContentId)))
+            return _t('_sys_request_page_not_found_cpt');
+
+        $sMethod = 'checkAllowed' . bx_gen_method_name($sAction);
+        if (!method_exists($this, $sMethod))
+            return _t('_sys_request_method_not_found_cpt');
+
+        return $this->$sMethod($aContentInfo, $isPerformAction);
+    }
+
+    /**
+     * Check particular action permission with content for specified profile
+     * @param $sAction action to check, for example: View, Edit
+     * @param $iContentId content ID
+     * @param $iProfileId profile ID which the permissions to be cheked for
+     * @return message on error, or CHECK_ACTION_RESULT_ALLOWED when allowed
+     */ 
+    public function serviceCheckAllowedWithContentForProfile($sAction, $iContentId, $iProfileId, $isPerformAction = false)
+    {
+        if (!$iContentId || !($aContentInfo = $this->_oDb->getContentInfoById($iContentId)))
+            return _t('_sys_request_page_not_found_cpt');
+
+        $sMethod = 'checkAllowed' . bx_gen_method_name($sAction) . 'ForProfile';
+        if (!method_exists($this, $sMethod))
+            return _t('_sys_request_method_not_found_cpt');
+
+        return $this->$sMethod($aContentInfo, $iProfileId, $isPerformAction);
+    }
+    
+    public function serviceCheckAllowedCommentsView($iContentId, $sObjectComments) 
+    {
+        //negative id used in comments for reports
+        if ($iContentId < 0)
+            return CHECK_ACTION_RESULT_ALLOWED;
+        
+        return $this->serviceCheckAllowedWithContent('comments_view', $iContentId);
+    }
+    
+    public function serviceCheckAllowedCommentsPost($iContentId, $sObjectComments) 
+    {
+        //negative id used in comments for reports
+        if ($iContentId < 0)
+            return CHECK_ACTION_RESULT_ALLOWED;
+        
+        return $this->serviceCheckAllowedWithContent('comments_post', $iContentId);
+    }
+
+    public function serviceGetContentOwnerProfileId ($iContentId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        // file owner must be author of the content or profile itself in case of profile based module
+        if ($iContentId) {
+            $sModule = $this->getName();
+            if ($this instanceof iBxDolProfileService && BxDolService::call($sModule, 'act_as_profile')) {
+                $oProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $sModule);
+            }
+            else {
+                $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+                $iContentAuthor = (int)$aContentInfo[$CNF['FIELD_AUTHOR']];
+                if($iContentAuthor < 0)
+                    $iContentAuthor = abs($iContentAuthor);
+
+                $oProfile = $aContentInfo ? BxDolProfile::getInstance($iContentAuthor) : null;
+            }
+
+            $iProfileId = $oProfile ? $oProfile->id() : bx_get_logged_profile_id();
+        }
+        else
+            $iProfileId = bx_get_logged_profile_id();
+
+        return $iProfileId;
+    }
+	
+    public function serviceGetBadges($iContentId,  $bIsSingle = false, $bIsCompact  = false)
+    {
+        $sModule = $this->getName();
+
+        $oBadges = BxDolBadges::getInstance();
+        $aBadges = $oBadges->getData(array(
+            'type' => ($bIsSingle ? 'by_module&object2_single' : 'by_module&object2'),
+            'module' => $sModule,
+            'object_id' => $iContentId
+        ));
+
+        $sResult = false;
+        bx_alert('system', 'get_badges', 0, 0, array(
+            'module' => $sModule, 
+            'content_id' => $iContentId,
+            'is_single' => $bIsSingle,
+            'is_compact' => $bIsCompact,
+            'badges' => $aBadges, 
+            'override_result' => &$sResult
+        ));
+        if($sResult !== false)
+            return $sResult;
+
+        if(empty($aBadges) || !is_array($aBadges))
+            return '';
+
+        if (bx_is_api()){
+            return $aBadges;
+        }
+        
+        if($bIsSingle)
+            return BxDolService::call('system', 'get_badge', array($aBadges[0], $bIsCompact), 'TemplServices');
+
+        $aBadgesOutput = array();
+        foreach($aBadges as $aBadge)
+            $aBadgesOutput[] =  array('badge' => BxDolService::call('system', 'get_badge', array($aBadge, $bIsCompact), 'TemplServices'));
+
+        return $this->_oTemplate->parseHtmlByName('badges.html', array(
+            'bx_repeat:items' => $aBadgesOutput,
+        ));
+    }
+    
+    /**
+     * @page service Service Calls
+     * @section bx_base_general Base General
+     * @subsection bx_base_general-page_blocks Page Blocks
+     * @subsubsection bx_base_general-categories_multi_list categories_multi_list
+     * 
+     * @code bx_srv('bx_posts', 'categories_multi_list', [...]); @endcode
+     * 
+     * Display multi-categorories block with number of posts in each category
+     * @param $bDisplayEmptyCats display empty categories
+     * 
+     * @see BxBaseModGeneralModule::serviceCategoriesMultiList
+     */
+    /** 
+     * @ref bx_base_general-categories_multi_list "categories_multi_list"
+     */
+    public function serviceCategoriesMultiList($bDisplayEmptyCats = true)
+    {
+        $aContextInfo = bx_get_page_info();
+
+        $mProfileContextId = false;
+        if ($aContextInfo !== false)
+            $mProfileContextId = $aContextInfo['context_profile_id'];
+        
+        $oCategories = BxDolCategories::getInstance();
+        if ($mProfileContextId)
+            $aCats = $oCategories->getData([
+                'type' => 'by_module&context_with_num', 
+                'module' => $this->getName(), 
+                'context_id' => $mProfileContextId
+            ]);
+        else{
+            $aCats = $oCategories->getData([
+                'type' => 'by_module_with_num', 
+                'module' => $this->getName()
+            ]);
+        }
+        $aVars = array('bx_repeat:cats' => array());
+        foreach ($aCats as $oCat) {
+            $sValue = $oCat['value'];
+            $iNum = $oCat['num'];
+            
+            $aVars['bx_repeat:cats'][] = array(
+                'url' => $oCategories->getUrl($this->getName(), $sValue, $mProfileContextId ? '&context_id=' . $mProfileContextId : ''),
+                'name' => _t($sValue),
+                'value' => $sValue,
+                'num' => $iNum,
+            );
+        }
+        
+        if (!$aVars['bx_repeat:cats'])
+            return '';
+
+        return $this->_oTemplate->parseHtmlByName('category_list_multi.html', $aVars);
+    }
+
+    /**
+     * ======
+     * PERMISSION METHODS
+     * ======
+     */
+    public function serviceCheckAllowedViewForProfile ($aDataEntry, $isPerformAction = false, $iProfileId = false)
+    {
+        $mixedResult = $this->_serviceCheckAllowedViewForProfile ($aDataEntry, $isPerformAction, $iProfileId);
+
+        // check alert to allow custom checks        
+        bx_alert('system', 'check_allowed_view', 0, 0, array('module' => $this->getName(), 'content_info' => $aDataEntry, 'profile_id' => $iProfileId, 'override_result' => &$mixedResult));
+
+        return $mixedResult;
+    }
+
+    public function checkAllowedSetThumb ($iContentId = 0)
+    {
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+    
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make "true === " checking.
+     */
+    public function checkAllowedBrowse ()
+    {
+        // check alert to allow custom checks
+        $mixedResult = null;
+        bx_alert('system', 'check_allowed_browse', 0, 0, array('module' => $this->getName(), 'profile_id' => $this->_iProfileId, 'override_result' => &$mixedResult));
+        if($mixedResult !== null)
+            return $mixedResult;
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedView ($aDataEntry, $isPerformAction = false)
+    {
+        return $this->serviceCheckAllowedViewForProfile ($aDataEntry, $isPerformAction);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedViewForProfile ($aDataEntry, $iProfileId, $isPerformAction = false)
+    {
+        return $this->serviceCheckAllowedViewForProfile ($aDataEntry, $isPerformAction, $iProfileId);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedAdd ($isPerformAction = false)
+    {
+        // check ACL
+        $aCheck = checkActionModule($this->_iProfileId, 'create entry', $this->getName(), $isPerformAction);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedApprove ($aDataEntry, $isPerformAction = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $sError = '_sys_txt_access_denied';
+
+        if($this->_oConfig->isAutoApproveEnabled())
+            return _t($sError);
+
+        if($aDataEntry[$CNF['FIELD_STATUS_ADMIN']] != BX_BASE_MOD_GENERAL_STATUS_PENDING)
+            return _t($sError);
+
+        // moderator always have access
+        if ($this->_isModerator($isPerformAction))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // check for context's admins 
+        if (!empty($CNF['FIELD_ALLOW_VIEW_TO']) && (int)$aDataEntry[$CNF['FIELD_ALLOW_VIEW_TO']] < 0) {
+            $oProfile = BxDolProfile::getInstance(-(int)$aDataEntry[$CNF['FIELD_ALLOW_VIEW_TO']]);
+            if ($oProfile){
+                $sModule = $oProfile->getModule();
+                $aEntity = BxDolRequest::serviceExists($sModule, 'get_all') ? BxDolService::call($sModule, 'get_all', array(array('type' => 'id', 'id' => $oProfile->getContentId()))) : array();
+
+                $oModule = BxDolModule::getInstance($sModule);
+
+                // check for context's extra roles with rights
+                if (method_exists($oModule, 'isAllowedModuleActionByProfile')) {
+                    $bResult = $oModule->isAllowedModuleActionByProfile($oProfile->getContentId(), $this->getName(), 'edit_any');
+                    if ($bResult !== NULL) return $bResult;
+                }
+
+                // if allowed edit a group then allowed to edit anything inside its context
+                if(isset($aEntity) && $oModule->checkAllowedEdit($aEntity) === CHECK_ACTION_RESULT_ALLOWED){
+                    return CHECK_ACTION_RESULT_ALLOWED;
+                }
+            }
+        }
+
+        return _t($sError);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedEdit ($aDataEntry, $isPerformAction = false)
+    {
+        // moderator and owner always have access
+        if ($this->isEntryAuthor($aDataEntry, $this->_iProfileId) || $this->_isModerator($isPerformAction))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // check for context's admins 
+        if (!empty($this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']) && (int)$aDataEntry[$this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']] < 0) {
+            $oProfile = BxDolProfile::getInstance(-(int)$aDataEntry[$this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']]);
+            if ($oProfile){
+                $sModule = $oProfile->getModule();
+                $aEntity = BxDolRequest::serviceExists($sModule, 'get_all') ? BxDolService::call($sModule, 'get_all', array(array('type' => 'id', 'id' => $oProfile->getContentId()))) : array();
+
+                $oModule = BxDolModule::getInstance($sModule);
+
+                // check for context's extra roles with rights
+                if (method_exists($oModule, 'isAllowedModuleActionByProfile')) {
+                    $bResult = $oModule->isAllowedModuleActionByProfile($oProfile->getContentId(), $this->getName(), 'edit_any');
+                    if ($bResult !== NULL) return $bResult;
+                }
+
+                // if allowed edit a group then allowed to edit anything inside its context
+                if(isset($aEntity) && $oModule->checkAllowedEdit($aEntity) === CHECK_ACTION_RESULT_ALLOWED){
+                    return CHECK_ACTION_RESULT_ALLOWED;
+                }
+            }
+        }
+
+        return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedDelete (&$aDataEntry, $isPerformAction = false)
+    {
+        // moderator always has access
+        if ($this->_isAdministrator($isPerformAction))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // check ACL
+        $aCheck = checkActionModule($this->_iProfileId, 'delete entry', $this->getName(), $isPerformAction);
+        if ($this->isEntryAuthor($aDataEntry, $this->_iProfileId) && $aCheck[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // check for context's admins 
+        if (isset($this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']) && (int)$aDataEntry[$this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']] < 0){
+            $oProfile = BxDolProfile::getInstance(-(int)$aDataEntry[$this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']]);
+            if ($oProfile){
+                $sModule = $oProfile->getModule();
+                $aEntity = BxDolRequest::serviceExists($sModule, 'get_all') ? BxDolService::call($sModule, 'get_all', array(array('type' => 'id', 'id' => $oProfile->getContentId()))) : array();
+
+                $oModule = BxDolModule::getInstance($sModule);
+
+                // check for context's extra roles with rights
+                if (method_exists($oModule, 'isAllowedModuleActionByProfile')) {
+                    $bResult = $oModule->isAllowedModuleActionByProfile($oProfile->getContentId(), $this->getName(), 'delete_any');
+                    if ($bResult !== NULL) return $bResult;
+                }
+
+                // if allowed delete a group then allowed to delete anything inside its context
+                if(isset($aEntity) && $oModule->checkAllowedDelete($aEntity) === CHECK_ACTION_RESULT_ALLOWED){
+                    return CHECK_ACTION_RESULT_ALLOWED;
+                }
+            }
+        }
+
+        return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedSetMembership (&$aDataEntry, $isPerformAction = false)
+    {
+        // admin always has access
+        if (isAdmin())
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // check ACL
+        $aCheck = checkActionModule($this->_iProfileId, 'set acl level', 'system', $isPerformAction);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedEditAnyEntry ($isPerformAction = false)
+    {
+    	return $this->checkAllowedEditAnyEntryForProfile($isPerformAction, $this->_iProfileId);
+    }
+    
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedDeleteAnyEntry ($isPerformAction = false)
+    {
+    	return $this->checkAllowedDeleteAnyEntryForProfile($isPerformAction, $this->_iProfileId);
+    }
+    
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedEditAnyEntryForProfile ($isPerformAction = false, $iProfileId = false)
+    {
+        if(!$iProfileId)
+            $iProfileId = $this->_iProfileId;
+
+    	$aCheck = checkActionModule($iProfileId, MEMBERSHIP_ACTION_EDIT_ANY_ENTRY, $this->getName(), $isPerformAction);
+    	if($aCheck[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED)
+    		return CHECK_ACTION_RESULT_ALLOWED;
+
+    	return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedDeleteAnyEntryForProfile ($isPerformAction = false, $iProfileId = false)
+    {
+        if(!$iProfileId)
+            $iProfileId = $this->_iProfileId;
+
+    	$aCheck = checkActionModule($iProfileId, MEMBERSHIP_ACTION_DELETE_ANY_ENTRY, $this->getName(), $isPerformAction);
+    	if($aCheck[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED)
+    		return CHECK_ACTION_RESULT_ALLOWED;
+
+    	return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make "true === " checking.
+     */
+    public function checkAllowedCommentsView ($aContentInfo, $isPerformAction = false)
+    {
+        return $this->checkAllowedView ($aContentInfo, $isPerformAction);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make "true === " checking.
+     */
+    public function checkAllowedCommentsPost ($aContentInfo, $isPerformAction = false)
+    {
+        return $this->checkAllowedView ($aContentInfo, $isPerformAction);
+    }
+
+    protected function _serviceCheckAllowedViewForProfile ($aDataEntry, $isPerformAction, $iProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$iProfileId)
+            $iProfileId = $this->_iProfileId;
+
+        if(empty($aDataEntry) || !is_array($aDataEntry))
+            return _t('_sys_txt_not_found');
+
+        // moderator and owner always have access
+        if(!empty($iProfileId) && (abs($aDataEntry[$CNF['FIELD_AUTHOR']]) == (int)$iProfileId || $this->_isModeratorForProfile($isPerformAction, $iProfileId)))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // check ACL
+        $aCheck = checkActionModule($iProfileId, 'view entry', $this->getName(), $isPerformAction);
+        if($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+
+        // check privacy
+        if(!empty($CNF['OBJECT_PRIVACY_VIEW'])) {
+            $oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW']);
+            if ($oPrivacy && !$oPrivacy->check($aDataEntry[$CNF['FIELD_ID']], $iProfileId))
+                return _t('_sys_access_denied_to_private_content');
+        }
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    public function _serviceBrowse ($sMode, $aParams = false, $iDesignBox = BX_DB_PADDING_DEF, $bDisplayEmptyMsg = false, $bAjaxPaginate = true, $sClassSearchResult = 'SearchResult')
+    {
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->checkAllowedBrowse()))
+            return bx_is_api() ? [bx_api_get_msg($sMsg)] : MsgBox($sMsg);
+
+        bx_import($sClassSearchResult, $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . $sClassSearchResult;
+        $o = new $sClass($sMode, $aParams);
+
+        $o->setDesignBoxTemplateId($iDesignBox);
+        $o->setDisplayEmptyMsg($bDisplayEmptyMsg);
+        $o->setAjaxPaginate($bAjaxPaginate);
+        $o->setUnitParams(array('context' => $sMode));
+        if (isset($aParams['condition']) && is_array($aParams['condition']))
+            $o->setCustomCurrentCondition($aParams['condition']);
+
+        if ($o->isError)
+            return '';
+
+        if ($s = $o->processing())
+            return bx_is_api() ? [bx_api_get_block('browse', $s)] : $s;
+        else
+            return '';
+    }
+
+    /**
+     * Shows a list of profiles.
+     */
+    protected function _serviceBrowseQuick($aProfiles, $iStart = 0, $iLimit = 4, $aAdditionalParams = array())
+    {
+        // get paginate object
+        $oPaginate = new BxTemplPaginate(array(
+            'on_change_page' => "return !loadDynamicBlockAutoPaginate(this, '{start}', '{per_page}', " . bx_js_string(json_encode($aAdditionalParams)) . ");",
+            'num' => count($aProfiles),
+            'per_page' => $iLimit,
+            'start' => $iStart,
+        ));
+
+        // remove last item from connection array, because we've got one more item for pagination calculations only
+        if (count($aProfiles) > $iLimit)
+            array_pop($aProfiles);
+
+        // get profiles HTML
+        $s = '';
+        foreach ($aProfiles as $mixedProfile) {
+            $bProfile = is_array($mixedProfile);
+
+            $oProfile = BxDolProfile::getInstance($bProfile ? (int)$mixedProfile['id'] : (int)$mixedProfile);
+            if(!$oProfile)
+                continue;
+
+            $aUnitParams = array('template' => array('name' => 'unit', 'size' => 'thumb'));
+            if(BxDolModule::getInstance($oProfile->getModule()) instanceof BxBaseModGroupsModule)
+                $aUnitParams['template']['name'] = 'unit_wo_cover';
+
+            if($bProfile && is_array($mixedProfile['info']))
+                $aUnitParams['template']['vars'] = $mixedProfile['info'];
+
+            $s .= $oProfile->getUnit(0, $aUnitParams);
+        }
+
+        $aTmplVarsPaginate = [];
+        if($iStart || $oPaginate->getNum() > $iLimit)
+            $aTmplVarsPaginate = ['paginate' => $oPaginate->getSimplePaginate()];
+
+        return $this->_oTemplate->parseHtmlByName('browse_quick.html', array(
+            'code' => $s,
+            'bx_if:show_paginate' => [
+                'condition' => !empty($aTmplVarsPaginate),
+                'content' => $aTmplVarsPaginate
+            ]
+        ));
+    }
+
+    // ====== COMMON METHODS
+    public function onPublished($iContentId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(!$aContentInfo)
+            return;
+
+        /*
+         * Process metas.
+         * Note. It's essential to process metas a the very end, 
+         * because all data related to an entry should be already
+         * processed and are ready to be passed to alert. 
+         */
+        $this->processMetasAdd($iContentId);
+
+        $this->alertAfterAdd($aContentInfo);
+    }
+
+    public function onFailed($iContentId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(!$aContentInfo)
+            return;
+
+        $aParams = array('object_author_id' => $aContentInfo[$CNF['FIELD_AUTHOR']]);
+        bx_alert($this->getName(), 'failed', $iContentId, $aContentInfo[$CNF['FIELD_AUTHOR']], $aParams);
+    }
+
+    public function onApprove($mixedContent)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!is_array($mixedContent))
+            $mixedContent = $this->_oDb->getContentInfoById((int)$mixedContent);
+
+        $this->alertAfterApprove($mixedContent);
+
+        $this->onPublished($mixedContent[$CNF['FIELD_ID']]);
+    }
+
+    public function onUpdateImage($iContentId, $sFiledName, $sFiledValue, $iProfileId = 0) {}
+
+    public function alertAfterAdd($aContentInfo) {}
+    
+    public function alertAfterEdit($aContentInfo) {}
+
+    public function alertAfterApprove($aContentInfo)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $iId = (int)$aContentInfo[$CNF['FIELD_ID']];
+
+        $sModule = $this->getName();
+        $aParams = $this->_alertParams($aContentInfo);
+        bx_alert($sModule, 'approved', $iId, false, $aParams);
+    }
+
+    /**
+     * Get array of params to be passed in Add/Edit Alert.
+     */
+    protected function _alertParams($aContentInfo)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        return [
+            'source' => $this->_oConfig->getName() . '_' . (int)$aContentInfo[$CNF['FIELD_ID']],
+
+            'object_author_id' => (int)$aContentInfo[$CNF['FIELD_AUTHOR']]
+        ];
+    }
+
+    public function processMetasAdd($iContentId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($CNF['OBJECT_METATAGS'])) 
+            return false;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+
+        $bFldStatus = !empty($CNF['FIELD_STATUS']);
+        $bFldStatusAdmin = !empty($CNF['FIELD_STATUS_ADMIN']);
+        $bContentInfo = $aContentInfo && (!$bFldStatus || ($bFldStatus && $aContentInfo[$CNF['FIELD_STATUS']] == 'active')) && (!$bFldStatusAdmin || ($bFldStatusAdmin && $aContentInfo[$CNF['FIELD_STATUS_ADMIN']] == 'active'));
+        if(!$bContentInfo)
+            return false;
+
+        $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+        $oMetatags->metaAddAuto($iContentId, $aContentInfo, $CNF, $CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD']);
+
+        $sKey = 'FIELD_LOCATION';
+        if($oMetatags->locationsIsEnabled() && !empty($CNF[$sKey]) && !empty($aContentInfo[$CNF[$sKey]])) {
+            $aLocation = unserialize($aContentInfo[$CNF[$sKey]]);
+            if(!empty($aLocation) && is_array($aLocation))
+                call_user_func_array(array($oMetatags, 'locationsAdd'), array_merge(array($iContentId), array_values($aLocation)));
+        }
+
+        $sKey = 'FIELD_LABELS';
+        if($oMetatags->keywordsIsEnabled() && !empty($CNF[$sKey]) && !empty($aContentInfo[$CNF[$sKey]])) {
+            $aLabels = unserialize($aContentInfo[$CNF[$sKey]]);
+            if(!empty($aLabels) && is_array($aLabels))
+                foreach ($aLabels as $sLabel) {
+                    if(!preg_match("/(\pL[\pL\pN_]+)/u", $sLabel)) 
+                        continue;
+
+                    $oMetatags->keywordsAddOne($iContentId, $sLabel, false);
+                }
+        }
+        
+        return true;
+    }
+
+    public function processMetasEdit($iContentId, $oForm)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($CNF['OBJECT_METATAGS']))
+            return false;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+
+        $bFldStatus = isset($CNF['FIELD_STATUS']);
+        $bFldStatusAdmin = isset($CNF['FIELD_STATUS_ADMIN']);
+        $bContentInfo = $aContentInfo && (!$bFldStatus || ($bFldStatus && $aContentInfo[$CNF['FIELD_STATUS']] == 'active')) && (!$bFldStatusAdmin || ($bFldStatusAdmin && $aContentInfo[$CNF['FIELD_STATUS_ADMIN']] == 'active'));
+        if(!$bContentInfo)
+            return false;
+
+        $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+        $oMetatags->metaAddAuto($iContentId, $aContentInfo, $CNF, $CNF['OBJECT_FORM_ENTRY_DISPLAY_EDIT']);
+
+        $sKey = 'FIELD_LOCATION';
+        if($oMetatags->locationsIsEnabled() && !empty($CNF[$sKey]) && !empty($aContentInfo[$CNF[$sKey]])) {
+            $aLocation = unserialize($aContentInfo[$CNF[$sKey]]);
+            if(!empty($aLocation) && is_array($aLocation))
+                call_user_func_array(array($oMetatags, 'locationsAdd'), array_merge(array($iContentId), array_values($aLocation)));
+        }
+
+        $sKey = 'FIELD_LABELS';
+        if($oMetatags->keywordsIsEnabled() && !empty($CNF[$sKey]) && !empty($aContentInfo[$CNF[$sKey]])) {
+            $aLabels = unserialize($aContentInfo[$CNF[$sKey]]);
+            if(!empty($aLabels) && is_array($aLabels))
+                foreach ($aLabels as $sLabel) {
+                    if(!preg_match("/(\pL[\pL\pN_]+)/u", $sLabel))
+                        continue;
+
+                    $oMetatags->keywordsAddOne($iContentId, $sLabel, false);
+                }
+        }
+
+        return true;
+    }
+
+    public function getEntryImageData($aContentInfo, $sField = 'FIELD_THUMB', $aTranscoders = array())
+    {
+        if(empty($aTranscoders))
+            $aTranscoders = array('OBJECT_TRANSCODER_COVER', 'OBJECT_IMAGES_TRANSCODER_COVER', 'OBJECT_IMAGES_TRANSCODER_GALLERY');
+
+        $CNF = &$this->_oConfig->CNF;
+        if(empty($CNF[$sField]) || empty($aContentInfo[$CNF[$sField]]) || empty($CNF['OBJECT_STORAGE']))
+            return false;
+
+        $iId = (int)$aContentInfo[$CNF[$sField]];
+        $aFile = BxDolStorage::getObjectInstance($CNF['OBJECT_STORAGE'])->getFile($iId);
+        if(!empty($aFile) && is_array($aFile) && strpos($aFile['mime_type'], 'svg') !== false)
+            return array('id' => $iId, 'object' => $CNF['OBJECT_STORAGE']);
+
+        foreach($aTranscoders as $sTranscoder)
+            if(!empty($CNF[$sTranscoder]))
+                return array('id' => $iId, 'transcoder' => $CNF[$sTranscoder]);
+
+        return array('id' => $iId, 'object' => $CNF['OBJECT_STORAGE']);
+    }
+
+    public function getEntryAllActions ($mixedContent = false, $aParams = [])
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!empty($mixedContent)) {
+            if(!is_array($mixedContent))
+                $mixedContent = array((int)$mixedContent, (method_exists($this->_oDb, 'getContentInfoById')) ? $this->_oDb->getContentInfoById((int)$mixedContent) : array());
+        }
+        else {
+            $mixedContent = $this->_getContent();
+            if($mixedContent === false)
+                return false;
+        }
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        $sObjectMenu = !empty($aParams['object_menu']) ? $aParams['object_menu'] : '';
+        if(empty($sObjectMenu) && !empty($CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY_ALL']))
+            $sObjectMenu = $CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY_ALL'];
+
+        if(empty($sObjectMenu))
+            return false;
+
+        $sEntryTitle = !empty($aParams['entry_title']) ? $aParams['entry_title'] : '';
+        if(empty($sEntryTitle) && !empty($CNF['FIELD_TITLE']) && !empty($aContentInfo[$CNF['FIELD_TITLE']]))
+            $sEntryTitle = $aContentInfo[$CNF['FIELD_TITLE']];
+
+        $sEntryUrl = !empty($aParams['entry_url']) ? $aParams['entry_url'] : '';
+        if(empty($sEntryUrl) && !empty($CNF['URI_VIEW_ENTRY']))
+            $sEntryUrl = bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $iContentId));
+
+        $iEntryThumb = !empty($aParams['entry_thumb']) ? (int)$aParams['entry_thumb'] : 0;
+        if(empty($iEntryThumb) && !empty($CNF['FIELD_THUMB']) && !empty($aContentInfo[$CNF['FIELD_THUMB']]))
+            $iEntryThumb = (int)$aContentInfo[$CNF['FIELD_THUMB']];
+
+        $sObjectStorage = !empty($aParams['object_storage']) ? $aParams['object_storage'] : false;
+        if(empty($sObjectStorage) && !empty($CNF['OBJECT_STORAGE']))
+            $sObjectStorage = $CNF['OBJECT_STORAGE'];
+
+        $sObjectTranscoder = !empty($aParams['object_transcoder']) ? $aParams['object_transcoder'] : false;
+
+        $aMarkers = [
+            'id' => $iContentId,
+            'module' => $this->_oConfig->getName(),
+            'title' => !empty($sEntryTitle) ? $sEntryTitle : '',
+            'url' => !empty($sEntryUrl) ? $sEntryUrl : '',
+            'img_url' => ''
+        ];
+
+        if(!empty($iEntryThumb)) {
+            if(!empty($sObjectTranscoder))
+                $o = BxDolTranscoder::getObjectInstance($sObjectTranscoder);
+            else if(!empty($sObjectStorage))
+                $o = BxDolStorage::getObjectInstance($sObjectStorage);
+
+            $sImageUrl = $o ? $o->getFileUrlById($iEntryThumb) : '';
+            if(!empty($sImageUrl))
+                $aMarkers['img_url'] = $sImageUrl;
+        }
+
+        $oActions = BxDolMenu::getObjectInstance($sObjectMenu, $this->_oTemplate);
+        if(!$oActions)
+            return false;
+
+        $oActions->setContentId($iContentId);
+        $oActions->addMarkers($aMarkers);
+
+        return $oActions->{bx_is_api() ? 'getCodeAPI' : 'getCode'}();
+    }
+
+    public function getProfileId()
+    {
+    	return bx_get_logged_profile_id();
+    }
+
+    public function getProfileInfo($iUserId = false)
+    {
+        $oProfile = $this->getObjectUser($iUserId);
+
+        $oAccount = null;
+        if($oProfile && !($oProfile instanceof BxDolProfileUndefined) && !($oProfile instanceof BxDolProfileAnonymous))
+            $oAccount = $oProfile->getAccountObject();
+        $bAccount = !empty($oAccount);
+
+        if(!$bAccount)
+            $oProfile = BxDolProfileUndefined::getInstance();
+
+        return array(
+            'id' => $oProfile->id(),
+            'name' => $oProfile->getDisplayName(),
+            'email' => $bAccount ? $oAccount->getEmail() : '',
+            'link' => $oProfile->getUrl(),
+            'icon' => $oProfile->getIcon(),
+            'thumb' => $oProfile->getThumb(),
+            'avatar' => $oProfile->getAvatar(),
+            'active' => $oProfile->isActive(),
+        );
+    }
+
+    public function getObjectUser($iUserId = false)
+    {
+    	bx_import('BxDolProfile');
+        return BxDolProfile::getInstanceMagic($iUserId);
+    }
+
+    public function getObjectFavorite($sSystem = '', $iId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if(empty($sSystem) && !empty($CNF['OBJECT_FAVORITES']))
+            $sSystem = $CNF['OBJECT_FAVORITES'];
+
+        if(empty($sSystem))
+            return false;
+
+        $oFavorite = BxDolFavorite::getObjectInstance($sSystem, $iId, true, $this->_oTemplate);
+        if(!$oFavorite || !$oFavorite->isEnabled())
+            return false;
+
+        return $oFavorite;
+    }
+
+	public function getUserId()
+    {
+        return isLogged() ? bx_get_logged_profile_id() : 0;
+    }
+
+    public function getUserIp()
+    {
+        return getVisitorIP();
+    }
+    
+    public function getUserInfo($iUserId = 0)
+    {
+        $oProfile = BxDolProfile::getInstanceMagic($iUserId);
+
+        return array(
+            $oProfile->getDisplayName(),
+            $oProfile->getUrl(),
+            $oProfile->getThumb(),
+            $oProfile->getUnit(),
+            $oProfile->getUnit(0, array('template' => 'unit_wo_info'))
+        );
+    }
+    
+    public function getUserInfoWithBadges($iUserId = 0)
+    {
+        $oProfile = BxDolProfile::getInstanceMagic($iUserId);
+
+        return array(
+            $oProfile->getDisplayName(),
+            $oProfile->getUrl(),
+            $oProfile->getThumb(),
+            $oProfile->getUnit(),
+            $oProfile->getUnit(0, array('template' => 'unit_wo_info')),
+            $oProfile->getBadges()
+        );
+    }
+
+    public function isMenuItemVisible($sObject, &$aItem, &$aContentInfo)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        // default visible settings
+        if(!BxDolAcl::getInstance()->isMemberLevelInSet($aItem['visible_for_levels']))
+            return false;
+
+        if (!empty($aItem['visibility_custom'])) {
+            $oMenu = BxDolMenu::getObjectInstance($sObject);
+            if ($oMenu && !BxDolService::callSerialized($aItem['visibility_custom'], $oMenu->getMarkers()))
+                return false;
+        }
+        
+        // get custom function name to check menu item visibility
+        $sFuncCheckAccess = false;
+        if(isset($CNF['MENU_ITEM_TO_METHOD'][$sObject][$aItem['name']]))
+            $sFuncCheckAccess = $CNF['MENU_ITEM_TO_METHOD'][$sObject][$aItem['name']];
+
+        // check custom visibility settings defined in module config class
+        if(!isset($aContentInfo))
+            $aContentInfo = array();
+
+        if($sFuncCheckAccess && CHECK_ACTION_RESULT_ALLOWED !== call_user_func_array(array($this, $sFuncCheckAccess), array(&$aContentInfo)))
+            return false;
+
+        return true;
+    }
+
+    public function _isModerator ($isPerformAction = false)
+    {
+        return $this->_isModeratorForProfile($isPerformAction, $this->_iProfileId);
+    }
+
+    public function _isModeratorForProfile($isPerformAction = false, $iProfileId = false)
+    {
+        return CHECK_ACTION_RESULT_ALLOWED === $this->checkAllowedEditAnyEntryForProfile ($isPerformAction, $iProfileId);
+    }
+
+    public function _isAdministrator ($isPerformAction = false)
+    {
+        return $this->_isAdministratorForProfile($isPerformAction, $this->_iProfileId);
+    }
+
+    public function _isAdministratorForProfile($isPerformAction = false, $iProfileId = false)
+    {
+        return CHECK_ACTION_RESULT_ALLOWED === $this->checkAllowedDeleteAnyEntryForProfile ($isPerformAction, $iProfileId);
+    }
+
+    public function _prepareAuditParams($aContentInfo, $bIsSaveData = true, $aOverrideAuditParams  = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+        $iContextId = isset($CNF['FIELD_ALLOW_VIEW_TO']) && (!empty($aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]) && (int)$aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']] < 0) ? - $aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']] : 0;
+        
+        $AuditParams = array(
+            'content_title' => (isset($CNF['FIELD_TITLE']) && isset($aContentInfo[$CNF['FIELD_TITLE']])) ? $aContentInfo[$CNF['FIELD_TITLE']] : '',
+            'context_profile_id' => $iContextId,
+            'content_info_object' =>  isset($CNF['OBJECT_CONTENT_INFO']) ? $CNF['OBJECT_CONTENT_INFO'] : '',
+            'data' => ($bIsSaveData ? $aContentInfo : array())
+        );
+        if ($iContextId > 0)
+            $AuditParams['context_profile_title'] = BxDolProfile::getInstance($iContextId)->getDisplayName();
+        
+        $AuditParams = array_merge($AuditParams, $aOverrideAuditParams);
+        
+        return $AuditParams;
+    }
+    
+    public function _getFavoriteListUrl ($iListId, $iProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        return bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_FAVORITES_LIST'] . '&profile_id=' . $iProfileId . '&list_id=' . $iListId));
+    }
+
+    public function getDataAPI($aData, $aParams = [])
+    {
+        $aDataApi = $this->decodeDataAPI($aData, $aParams);
+
+        $sModule = $this->getName();
+        $aExtras = [
+            'module' => $sModule,
+            'data' => $aData,
+            'params' => $aParams,
+            'data_api' => &$aDataApi,
+        ];
+
+        bx_alert('system', 'decode_data_api', 0, 0, $aExtras);
+        bx_alert($sModule, 'decode_data_api', 0, 0, $aExtras);
+
+        return $aDataApi;
+    }
+
+    public function decodeDataAPI($aData, $aParams = [])
+    {
+        return $aData;
+    }
+
+    // ====== PROTECTED METHODS
+
+    protected function _serviceEntityForm ($sFormMethod, $iContentId = 0, $sDisplay = false, $sCheckFunction = false, $bErrorMsg = true)
+    {
+        $iContentId = $this->_getContent($iContentId, false);
+        if($iContentId === false)
+            return false;
+
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'FormsEntryHelper';
+        $oFormsHelper = new $sClass($this);
+        return $oFormsHelper->$sFormMethod((int)$iContentId, $sDisplay, $sCheckFunction, $bErrorMsg);
+    }
+
+    protected function _serviceTemplateFunc ($sFunc, $iContentId, $sFuncGetContent = 'getContentInfoById')
+    {
+        return $this->_serviceTemplateFuncEx($sFunc, $iContentId, array('function_get_content' => $sFuncGetContent));
+    }
+    
+    protected function _serviceTemplateFuncEx ($sFunc, $iContentId, $aParams = array())
+    {
+        if (!isset($aParams['function_get_content']))
+            $aParams['function_get_content'] = 'getContentInfoById';
+        
+        $mixedContent = $this->_getContent($iContentId, $aParams['function_get_content']);
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        unset($aParams['function_get_content']);
+        
+        if (!empty($aParams))
+            return $this->_oTemplate->$sFunc($aContentInfo, $aParams);
+        else
+            return $this->_oTemplate->$sFunc($aContentInfo);
+    }
+
+    protected function _getApproveForm($iContentId, $aContentInfo)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sForm = 'sys_manage';
+        if(!empty($CNF['OBJECT_FORM_MANAGE']))
+            $sForm = $CNF['OBJECT_FORM_MANAGE'];
+
+        $sFormDisplay = 'sys_manage_approve';
+        if(!empty($CNF['OBJECT_FORM_MANAGE_APPROVE']))
+            $sFormDisplay = $CNF['OBJECT_FORM_MANAGE_APPROVE'];
+
+        $oForm = BxTemplFormView::getObjectInstance($sForm, $sFormDisplay);
+        $oForm->aFormAttrs['action'] = BX_DOL_URL_ROOT . $this->_oConfig->getBaseUri() . 'approve';
+        $oForm->aInputs['content_id']['value'] = $iContentId;
+        foreach($oForm->aInputs['controls'] as $iKey => $mixedValue)
+            if(is_numeric($iKey) && isset($mixedValue['name']) && $mixedValue['name'] == 'do_submit')
+                $oForm->aInputs['controls'][$iKey]['value'] = _t('_sys_form_manage_input_do_submit_approve');
+
+        bx_alert($this->_oConfig->getName(), 'get_approve_form', 0, 0, array(
+            'content_id' => $iContentId,
+            'content_info' => $aContentInfo,
+            'override_result' => &$oForm
+        ));
+
+        return $oForm;
+    }
+
+    protected function _sendApproveMessage($iContentId, $aContentInfo, &$oForm)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sETemplate = 't_ManageApprove';
+        if(!empty($CNF['ETEMPLATE_MANAGE_APPROVE']))
+            $sETemplate = $CNF['ETEMPLATE_MANAGE_APPROVE'];
+
+        $aEParams = array(
+            'content_title' => !empty($CNF['FIELD_TITLE']) && !empty($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : '',
+            'notes' => $oForm->getCleanValue('notes'),
+        );
+
+        bx_alert($this->_oConfig->getName(), 'get_approve_message', 0, 0, array(
+            'content_id' => $iContentId,
+            'content_info' => $aContentInfo,
+            'form' => $oForm,
+            'email_template' => &$sETemplate,
+            'email_params' => &$aEParams
+        ));
+
+        return sendMailTemplate($sETemplate, 0, $aContentInfo[$CNF['FIELD_AUTHOR']], $aEParams);
+    }
+
+    protected function _processModerationNotifications($aContentInfo)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($CNF['PARAM_AUTO_APPROVE']) || getParam($CNF['PARAM_AUTO_APPROVE']) == 'on')
+            return;
+
+        $aRecipients = [];
+        if(getParam('sys_notify_to_approve_by_role') == 'on' && bx_srv('system', 'is_module_content', [$this->_oConfig->getName()]) && !empty($CNF['FIELD_ALLOW_VIEW_TO']) && (int)$aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']] < 0) {
+            $iContextProfileId = abs((int)$aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]);
+            $oContextProfile = BxDolProfile::getInstance($iContextProfileId);
+
+            $aRecipients = bx_srv($oContextProfile->getModule(), 'get_admins_to_manage_content', [$iContextProfileId]);
+        }
+
+        $sModule = $this->getName();
+
+        if(empty($aRecipients))
+            $aRecipients = BxDolAclQuery::getInstance()->getProfilesByAction([
+                MEMBERSHIP_ACTION_EDIT_ANY_ENTRY, 
+                MEMBERSHIP_ACTION_DELETE_ANY_ENTRY
+            ], [
+                'module' => $sModule, 
+                'ids_only' => true
+            ]);
+
+        if(empty($aRecipients))
+            return;
+
+        foreach($aRecipients as $iRecipientId)
+            bx_alert($sModule, 'pending_approval', $aContentInfo[$CNF['FIELD_ID']], $aContentInfo[$CNF['FIELD_AUTHOR']], [
+                'object_author_id' => (int)$iRecipientId
+            ]);
+    }
+
+    protected function _rss ($aArgs, $sClass = 'SearchResult')
+    {
+        $sMode = array_shift($aArgs);
+
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->checkAllowedBrowse())) {
+            $this->_oTemplate->displayAccessDenied ($sMsg);
+            exit;
+        }
+
+        $aParams = $this->_buildRssParams($sMode, $aArgs);
+
+        bx_import ($sClass, $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . $sClass;
+        $o = new $sClass($sMode, $aParams);
+
+        if ($o->isError)
+            $this->_oTemplate->displayPageNotFound ();
+        else
+            $o->outputRSS();
+
+        exit;
+    }
+
+    protected function _getContent($iContentId = 0, $sFuncGetContent = true)
+    {
+        if(!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+
+        if(!$iContentId)
+            return false;
+
+        if($sFuncGetContent === true)
+            $sFuncGetContent = 'getContentInfoById';
+
+        if(empty($sFuncGetContent) || !method_exists($this->_oDb, $sFuncGetContent))
+            return $iContentId;
+
+        $aContentInfo = $this->_oDb->$sFuncGetContent($iContentId);
+        if(!$aContentInfo)
+            return false;
+
+        return array($iContentId, $aContentInfo);
+    }
+
+    protected function _getContentForTimelinePost($aEvent, $aContentInfo, $aBrowseParams = array())
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	$sUrl = bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]));
+
+    	//--- Image(s)
+        $aImages = $this->_getImagesForTimelinePost($aEvent, $aContentInfo, $sUrl, $aBrowseParams);
+        $aImagesAttach = $this->_getImagesForTimelinePostAttach($aEvent, $aContentInfo, $sUrl, $aBrowseParams);
+
+        if(bx_is_api() && getParam('sys_api_extended_units') != 'on') {
+            $sTitle = '';
+            if(isset($CNF['FIELD_TITLE']) && isset($aContentInfo[$CNF['FIELD_TITLE']]))
+                $sTitle = $aContentInfo[$CNF['FIELD_TITLE']];
+            else if(isset($CNF['FIELD_TEXT']) && isset($aContentInfo[$CNF['FIELD_TEXT']]))
+                $sTitle = strmaxtextlen($aContentInfo[$CNF['FIELD_TEXT']], 20, '...');
+
+            $sText = isset($CNF['FIELD_TEXT']) && isset($aContentInfo[$CNF['FIELD_TEXT']]) ? $aContentInfo[$CNF['FIELD_TEXT']] : '';
+            $sText = BxTemplFunctions::getInstance()->getStringWithLimitedLength(strip_tags($sText), 240);
+
+            return [
+                'url' => $sUrl,
+                'title' => $sTitle,
+                'text' => $sText,
+                'images' => $aImages
+            ];
+        }
+
+        //--- Video(s)
+        $aVideos = $this->_getVideosForTimelinePost($aEvent, $aContentInfo, $sUrl, $aBrowseParams);
+        $aVideosAttach = $this->_getVideosForTimelinePostAttach($aEvent, $aContentInfo, $sUrl, $aBrowseParams);
+
+        //--- Files(s)
+        $aFiles = $this->_getFilesForTimelinePost($aEvent, $aContentInfo, $sUrl, $aBrowseParams);
+        $aFilesAttach = $this->_getFilesForTimelinePostAttach($aEvent, $aContentInfo, $sUrl, $aBrowseParams);
+
+        //--- Title
+        $sTitle = '';
+        if(isset($CNF['FIELD_TITLE']) && isset($aContentInfo[$CNF['FIELD_TITLE']]))
+            $sTitle = $aContentInfo[$CNF['FIELD_TITLE']];
+        else if(isset($CNF['FIELD_TEXT']) && isset($aContentInfo[$CNF['FIELD_TEXT']]))
+            $sTitle = strmaxtextlen($aContentInfo[$CNF['FIELD_TEXT']], 20, '...');
+
+        //--- Text
+        $sText = isset($CNF['FIELD_TEXT']) && isset($aContentInfo[$CNF['FIELD_TEXT']]) ? $aContentInfo[$CNF['FIELD_TEXT']] : '';
+        if(!empty($CNF['OBJECT_METATAGS']) && is_string($sText)) {
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+            $sText = $oMetatags->metaParse($aContentInfo[$CNF['FIELD_ID']], $sText);
+        }
+
+        $sCategory = '';
+        if (isset($CNF['OBJECT_CATEGORY']) && isset($CNF['OBJECT_CATEGORY']) && isset($aContentInfo[$CNF['FIELD_CATEGORY']])){
+            $oCategory = BxDolCategory::getObjectInstance($CNF['OBJECT_CATEGORY']);
+            if ($oCategory)
+                $sCategory = $oCategory->getCategoryTitle($aContentInfo[$CNF['FIELD_CATEGORY']]);
+        } 
+        
+    	return array(
+            'sample' => isset($CNF['T']['txt_sample_single_with_article']) ? $CNF['T']['txt_sample_single_with_article'] : $CNF['T']['txt_sample_single'],
+            'sample_wo_article' => $CNF['T']['txt_sample_single'],
+            'sample_action' => isset($CNF['T']['txt_sample_action']) ? $CNF['T']['txt_sample_action'] : '',
+            'url' => $sUrl,
+            'title' => $sTitle,
+            'category' => $sCategory,
+            'text' => $sText,
+            'images' => $aImages,
+            'images_attach' => $aImagesAttach,
+            'videos' => $aVideos,
+            'videos_attach' => $aVideosAttach,
+            'files' => $aFiles,
+            'files_attach' => $aFilesAttach
+        );
+    }
+
+    protected function _getImagesForTimelinePost($aEvent, $aContentInfo, $sUrl, $aBrowseParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if (bx_is_api()){
+            $aResult = [];
+            if(isset($CNF['FIELD_COVER']) && !empty($aContentInfo[$CNF['FIELD_COVER']])) {
+                $aResult[] = bx_api_get_image($CNF['OBJECT_STORAGE'], (int)$aContentInfo[$CNF['FIELD_COVER']]);
+            }
+            if(isset($CNF['FIELD_THUMB']) && !empty($aContentInfo[$CNF['FIELD_THUMB']])) {
+                $aResult[] = bx_api_get_image($CNF['OBJECT_STORAGE'], (int)$aContentInfo[$CNF['FIELD_THUMB']]);
+            }
+            return $aResult;
+        }
+        else{
+            $iImageId = 0;
+            $sImageSm = $sImageMd = $sImageXl = '';
+            if(isset($CNF['FIELD_COVER']) && !empty($aContentInfo[$CNF['FIELD_COVER']])) {
+                $iImageId = (int)$aContentInfo[$CNF['FIELD_COVER']];
+                $sImageSm = $this->_oConfig->getImageUrl($iImageId, ['OBJECT_IMAGES_TRANSCODER_MINIATURE', 'OBJECT_IMAGES_TRANSCODER_THUMB']);
+                $sImageMd = $this->_oConfig->getImageUrl($iImageId, ['OBJECT_IMAGES_TRANSCODER_GALLERY', 'OBJECT_IMAGES_TRANSCODER_THUMB']);
+                $sImageXl = $this->_oConfig->getImageUrl($iImageId, ['OBJECT_IMAGES_TRANSCODER_COVER']);
+            }
+
+            if($sImageMd == '' && isset($CNF['FIELD_THUMB']) && !empty($aContentInfo[$CNF['FIELD_THUMB']])) {
+                $iImageId = (int)$aContentInfo[$CNF['FIELD_THUMB']];
+                $sImageSm = $this->_oConfig->getImageUrl($iImageId, ['OBJECT_IMAGES_TRANSCODER_MINIATURE', 'OBJECT_IMAGES_TRANSCODER_THUMB']);
+                $sImageMd = $this->_oConfig->getImageUrl($iImageId, ['OBJECT_IMAGES_TRANSCODER_GALLERY', 'OBJECT_IMAGES_TRANSCODER_THUMB']);
+                $sImageXl = $this->_oConfig->getImageUrl($iImageId, ['OBJECT_IMAGES_TRANSCODER_COVER']);
+            }
+
+            if(empty($sImageMd))
+                return [];
+
+            if($sImageSm == '')
+                $sImageSm = $sImageMd;
+
+            if($sImageXl == '')
+                $sImageXl = $sImageMd;
+
+            return [[
+                'id' => $iImageId, 
+                'url' => $sUrl, 
+                'src' => $sImageMd, 
+                'src_small' => $sImageSm, 
+                'src_medium' => $sImageMd, 
+                'src_orig' => $sImageXl
+            ]];
+        }
+    }
+
+    protected function _getImagesForTimelinePostAttach($aEvent, $aContentInfo, $sUrl, $aBrowseParams = array())
+    {
+        return array();
+    }
+
+    protected function _getVideosForTimelinePost($aEvent, $aContentInfo, $sUrl, $aBrowseParams = array())
+    {
+        return array();
+    }
+
+    protected function _getVideosForTimelinePostAttach($aEvent, $aContentInfo, $sUrl, $aBrowseParams = array())
+    {
+        return array();
+    }
+
+    protected function _getFilesForTimelinePost($aEvent, $aContentInfo, $sUrl, $aBrowseParams = array())
+    {
+        return array();
+    }
+
+    protected function _getFilesForTimelinePostAttach($aEvent, $aContentInfo, $sUrl, $aBrowseParams = array())
+    {
+        return array();
+    }
+
+    protected function _entityComments ($sObject, $iId = 0)
+    {
+        if (!$iId)
+            $iId = bx_process_input(bx_get('id'), BX_DATA_INT);
+
+        if (!$iId)
+            return false;
+
+        $oCmts = BxDolCmts::getObjectInstance($sObject, $iId);
+        if (!$oCmts || !$oCmts->isEnabled())
+            return false;
+        
+        if (bx_is_api()){
+            return [bx_srv('system', 'get_data_api', [['module' => $sObject, 'object_id' => $iId]], 'TemplCmtsServices')];
+        }
+        return $oCmts->getCommentsBlock([], ['in_designbox' => false, 'show_empty' => false]);
+    }
+
+    protected function _getFields($iContentId)
+    {
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo))
+            return array();
+
+        return BxDolContentInfo::formatFields($aContentInfo);
+    }
+
+    protected function _getFieldValue($sField, $iContentId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if(empty($CNF[$sField]))
+            return false;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || empty($aContentInfo[$CNF[$sField]]))
+            return false;
+
+        return $aContentInfo[$CNF[$sField]];
+    }
+
+    protected function _getFieldValueThumb($sField, $iContentId, $sTranscoder = '') 
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($sTranscoder) || empty($CNF[$sField]))
+            return false;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || empty($aContentInfo[$CNF[$sField]]))
+            return false;
+
+        $oImagesTranscoder = BxDolTranscoderImage::getObjectInstance($sTranscoder);
+        if(!$oImagesTranscoder)
+            return false;
+
+        return $oImagesTranscoder->getFileUrl($aContentInfo[$CNF[$sField]]);
+    }
+
+	protected function _prepareResponse($aResponse, $bAsJson = false, $aAdditional = array())
+    {
+    	if(!$bAsJson)
+    		return $aResponse;
+
+		if(!empty($aAdditional) && is_array($aAdditional))
+			$aResponse = array_merge($aResponse, $aAdditional);
+
+		echoJson($aResponse);
+		exit;
+    }  
+    
+    /**
+     * Blocks for lables tree browsing private functions
+     */
+    
+    private function _getLablesBreadcrumbsArray($iLabelId, &$aBreadcrumbs)
+	{
+		$oLabel = BxDolLabel::getInstance();
+		$aLabel = $oLabel->getLabels(array('type' => 'id', 'id' => $iLabelId));
+		if ($aLabel){
+            $aBreadcrumbs[$aLabel['id']] = array('id' => $aLabel['id'], 'value' => $aLabel['value']);
+			$this->_getLablesBreadcrumbsArray($aLabel['parent'], $aBreadcrumbs);
+		}
+	}
+    
+    private function _getLablesBrowseUrl($iLabelId)
+	{
+        list($sPageLink, $aPageParams) = bx_get_base_url_inline();
+		return BxDolPermalinks::getInstance()->permalink(bx_append_url_params($sPageLink, array_merge($aPageParams, array('label' => $iLabelId))));
+	}
+    
+    private function _getLablesTreeLevel($aBreadcrumbs, $iParent = 0)
+	{
+		$oLabel = BxDolLabel::getInstance();
+		$aLabelsOutput = array();
+        $aLabels = $oLabel->getLabels(array('type' => 'parent', 'parent' => $iParent));
+        if (count($aLabels) > 0){
+			foreach($aLabels as $aLabel) {
+				$sChild = $this->_getLablesTreeLevel($aBreadcrumbs, $aLabel['id']);
+				$bChildPresent = $sChild != '' ? true : false;
+				$bIsOpen = array_key_exists($aLabel['id'], $aBreadcrumbs) ? true : false;
+				if (!$bIsOpen){
+					$sChild = '';
+				}
+				$aLabelsOutput[] = array(
+					'value' => $aLabel['value'], 
+					'url' => $this->_getLablesBrowseUrl($aLabel['id']),
+					'child' => $sChild,
+					
+					'selected' => bx_process_input(bx_get('label'), BX_DATA_INT) == $aLabel['id'] ? 'selected' : ''.$aLabel['id'], 
+					'bx_if:open' => array(
+						'condition' => $bChildPresent && $bIsOpen,
+						'content' => array()
+					),
+					'bx_if:can_open' => array(
+						'condition' => $bChildPresent && !$bIsOpen,
+						'content' => array()
+					)
+				);
+			}
+			return $this->_oTemplate->parseHtmlByName('labels_tree.html', 
+				array(
+					'bx_repeat:items' => $aLabelsOutput,
+					'parent' => $iParent,
+				)
+			);
+		}
+		return '';
+	}
+}
+
+/** @} */

+ 176 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/general/classes/BxBaseModGeneralPageEntry.php

@@ -0,0 +1,176 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+bx_import('BxDolAcl');
+
+/**
+ * Entry create/edit pages
+ */
+class BxBaseModGeneralPageEntry extends BxTemplPage
+{
+    protected $MODULE;
+
+    protected $_oModule;
+    protected $_aContentInfo = false;
+
+    protected $_sCoverClass;
+
+    public function __construct($aObject, $oTemplate = false)
+    {
+        $this->_oModule = BxDolModule::getInstance($this->MODULE);
+
+        parent::__construct($aObject, $oTemplate ? $oTemplate : $this->_oModule->_oTemplate);
+
+        $this->_sCoverClass = $this->_oModule->getName() . '_cover';
+
+        $this->addMarkers([
+            'module' => $this->MODULE,
+        ]);
+    }
+
+    public function getContentInfo()
+    {
+        return $this->_aContentInfo;
+    }
+    
+    public function getCode ()
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (!$this->_isAvailablePage($this->_aObject)) {
+            $this->_oTemplate->displayPageNotFound();
+            exit;
+        }
+
+        $oCover = BxDolCover::getInstance($this->_oModule->_oTemplate);        
+        if ($oCover)
+            $oCover->setCoverClass($this->_sCoverClass);
+
+        // count views
+        if(!empty($CNF['OBJECT_VIEWS'])) {
+            $oView = BxDolView::getObjectInstance($CNF['OBJECT_VIEWS'], $this->_aContentInfo[$CNF['FIELD_ID']]);
+            if($oView && $oView->isEnabled())
+                $oView->doView();
+        }
+
+        // set cover image
+        $mixedCover = method_exists($this, '_getImageForPageCover') ? $this->_getImageForPageCover() : $this->_getThumbForMetaObject();
+        if($mixedCover !== false) {
+            $aCover = array(
+                'id' => $mixedCover['id']
+            );
+
+            if(!empty($mixedCover['transcoder']))
+                $aCover['transcoder'] = $mixedCover['transcoder'];
+            else if(!empty($CNF['OBJECT_IMAGES_TRANSCODER_COVER'])) 
+                $aCover['transcoder'] = $CNF['OBJECT_IMAGES_TRANSCODER_COVER'];
+            else if(!empty($mixedCover['object']))
+                $aCover['object'] = $mixedCover['object'];
+
+            $oCover->setCoverImageUrl($aCover);
+        }
+
+        // add content metatags
+        if(!empty($CNF['OBJECT_METATAGS']) && ($o = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS'])) !== false)
+            $o->addPageMetaInfo($this->_aContentInfo[$CNF['FIELD_ID']], $this->_getThumbForMetaObject());
+
+        $oMenuSubmenu = BxDolMenu::getObjectInstance('sys_site_submenu');
+        if($oMenuSubmenu) {
+            // add actions menu to submenu
+            if(isset($CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY']))
+                $oMenuSubmenu->setObjectActionsMenu($CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY']);
+
+            // add social sharing menu to submenu
+            $oMenuSubmenu->setServiceSocialSharing(array(
+                'module' => $this->MODULE,
+                'method' => 'entity_social_sharing',
+            ));
+        }
+        
+        BxDolTemplate::getInstance()->setPageUrl('page.php?i=' . $this->_aObject['uri'] . '&id=' . $this->_aContentInfo[$CNF['FIELD_ID']]);
+        
+        return parent::getCode ();
+    }
+
+    protected function _isAvailablePage ($a)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(!$this->_aContentInfo)
+            return false;
+
+        if(!empty($CNF['FIELD_CF'])) {
+            $oCf = BxDolContentFilter::getInstance();
+            if($oCf->isEnabled() && !$oCf->isAllowed($this->_aContentInfo[$CNF['FIELD_CF']]))
+                return false;
+        }
+
+        return parent::_isAvailablePage($a);
+    }
+
+    protected function _isVisiblePage ($a)
+    {
+        if(($mixedCheckResult = $this->_oModule->checkAllowedView($this->_aContentInfo)) !== CHECK_ACTION_RESULT_ALLOWED) 
+            return $mixedCheckResult;
+
+        if(!parent::_isVisiblePage($a))
+            return false;
+        
+        $this->_oModule->checkAllowedView($this->_aContentInfo, true);
+        
+        return true;
+    }
+
+    protected function _getPageAccessDeniedMsg ($mixedMsg = false)
+    {
+        $this->_oTemplate->displayAccessDenied($mixedMsg);
+        exit;
+    }
+
+    protected function _getPageMetaDesc()
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sResult = '';
+        if(isset($CNF['FIELD_TEXT'])) {
+            $sResult = strip_tags($this->_oModule->_oTemplate->getText($this->_aContentInfo, false));
+
+            $iLength = 0;
+            if(!empty($CNF['PARAM_CHARS_SUMMARY']))
+                $iLength = (int)getParam($CNF['PARAM_CHARS_SUMMARY']);
+            if(empty($iLength))
+                $iLength = 240;
+
+            if(mb_strlen($sResult) > $iLength)
+                $sResult = mb_substr($sResult, 0, $iLength);
+        }
+
+        if(empty($sResult))
+            $sResult = $this->_replaceMarkers(_t($this->_aObject['meta_description']));
+
+        return $sResult;
+    }
+
+    protected function _getThumbForMetaObject ()
+    {
+        return $this->_oModule->getEntryImageData($this->_aContentInfo);
+    }
+
+    protected function _getPageCacheParams ()
+    {
+        $s = parent::_getPageCacheParams ();
+        if (!$this->_aContentInfo)
+            return $s;
+        return $s . $this->_aContentInfo[$this->_oModule->_oConfig->CNF['FIELD_ID']]; // cache is different for every entry
+    }
+}
+
+/** @} */

+ 226 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/groups/classes/BxBaseModGroupsConnectionFans.php

@@ -0,0 +1,226 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Groups Groups
+ * @ingroup     UnaModules
+ * 
+ * @{
+ */
+
+class BxBaseModGroupsConnectionFans extends BxTemplConnection
+{
+    protected $_sModule;
+    protected $_oModule;
+
+    protected $_bQuestionnaire;
+
+    protected $_bBan;
+    protected $_oBanConnection;
+
+    public function __construct($aObject)
+    {
+        parent::__construct($aObject);
+
+        $this->_oModule = BxDolModule::getInstance($this->_sModule);
+
+        $this->_bQuestionnaire = false;
+
+        $this->_bBan = false;
+        $this->_oBanConnection = null;
+    }
+
+    public function init()
+    {
+        if($this->_bBan)
+            $this->_oBanConnection = BxDolConnection::getObjectInstance('sys_profiles_bans');
+    }
+
+    public function getModule()
+    {
+        return $this->_sModule;
+    }
+
+    public function checkAllowedAddConnection($iInitiator, $iContent, $isPerformAction = false, $isMutual = false, $isInvertResult = false, $isSwap = false, $isCheckExists = true)
+    {
+        if($this->_bBan && $this->_oBanConnection->isConnected($iContent, $iInitiator))
+            return _t('_sys_txt_access_denied');
+
+        return $this->checkAllowedConnect ($iInitiator, $iContent, $isPerformAction, $isMutual, $isInvertResult, $isSwap, $isCheckExists);
+    }
+
+    public function actionRemove($iContent = 0, $iInitiator = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $aResult = parent::actionRemove($iContent, $iInitiator);
+        if($this->_bQuestionnaire && $aResult['err'] == false) {
+            if(!$iContent)
+                $iContent = bx_process_input($_POST['id'], BX_DATA_INT);
+
+            $aContentInfo = $this->_oModule->_oDb->getContentInfoByProfileId($iContent);
+            if(!empty($aContentInfo) && is_array($aContentInfo))
+                $this->_oModule->_oDb->deleteAnswersProfileId($aContentInfo[$CNF['FIELD_ID']], $iInitiator ? $iInitiator : bx_get_logged_profile_id());
+        }
+
+        return $aResult;
+    }
+
+    public function actionReject ($iInitiator = 0, $iContent = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $aResult = parent::actionReject($iInitiator, $iContent);
+        if($this->_bQuestionnaire && $aResult['err'] == false) {
+            if(!$iContent)
+                $iContent = bx_process_input($_POST['id'], BX_DATA_INT);
+
+            $aContentInfo = $this->_oModule->_oDb->getContentInfoByProfileId($iContent);
+            if(!empty($aContentInfo) && is_array($aContentInfo))
+                $this->_oModule->_oDb->deleteAnswersProfileId($aContentInfo[$CNF['FIELD_ID']], $iInitiator ? $iInitiator : bx_get_logged_profile_id());
+        }
+
+        return $aResult;
+    }
+
+    public function hasQuestionnaire($iContentProfileId)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        
+        if(!$this->_bQuestionnaire || empty($CNF['FIELD_JOIN_CONFIRMATION']))
+            return false;
+
+        $aContentInfo = $this->_oModule->_oDb->getContentInfoByProfileId($iContentProfileId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return false;
+
+        return (int)$aContentInfo[$CNF['FIELD_JOIN_CONFIRMATION']] != 0 && $this->_oModule->_oDb->hasQuestions($aContentInfo[$CNF['FIELD_ID']]);
+    }
+    
+    public function isQuestionnaireAnswered($iContentProfileId, $iProfileId = 0)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return false;
+
+        if(!$this->_bQuestionnaire || empty($CNF['FIELD_JOIN_CONFIRMATION']))
+            return false;
+
+        $aContentInfo = $this->_oModule->_oDb->getContentInfoByProfileId($iContentProfileId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return false;
+
+        return (int)$aContentInfo[$CNF['FIELD_JOIN_CONFIRMATION']] != 0 && $this->_oModule->_oDb->areQuestionsAnswered($aContentInfo[$CNF['FIELD_ID']], $iProfileId);
+    }
+
+    public function getQuestionnaireForm($sAction, $iContentProfileId, $aParams = [])
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        
+        if(!$this->_bQuestionnaire)
+            return false;
+
+        if(empty($aParams['request']) || !is_array($aParams['request']))
+            $aParams['request'] = [];
+
+        $aForm = [
+            'form_attrs' => [
+                'id' => $this->_oModule->getName() . '_questionnaire',
+                'action' => BX_DOL_URL_ROOT . bx_append_url_params($this->_oModule->_oConfig->getBaseUri() . 'get_questionnaire', array_merge([
+                    'o' => $this->_sObject, 
+                    'a' => $sAction, 
+                    'cpi' => $iContentProfileId
+                ], $aParams['request']))
+            ],
+            'params' => [
+                'db' => [
+                    'table' => $CNF['TABLE_ANSWERS'],
+                    'key' => '',
+                    'uri' => '',
+                    'uri_title' => '',
+                    'submit_name' => 'do_submit'
+                ],
+                'module' => $this->_sModule,
+                'object' => $this->_sModule . '_questionnaire',
+                'display' => $this->_sModule . '_questionnaire_answer',
+                'view_mode' => 0,
+            ],
+            'inputs' => []
+        ];
+
+        $aQuestions = $this->_oModule->_oDb->getQuestions(['sample' => 'content_pid', 'content_pid' => $iContentProfileId]);
+        if(empty($aQuestions) || !is_array($aQuestions))
+            return false;
+
+        foreach($aQuestions as $aQuestion) {
+            $sName = 'question_' . $aQuestion['id'];
+
+            $aForm['inputs'][$sName] = [
+                'type' => 'text',
+                'name' => $sName,
+                'caption' => $aQuestion['question'],
+                'value' => '',
+                'required' => '1',
+                'checker' => [
+                    'func' => 'Avail',
+                    'params' => [],
+                    'error' => _t($CNF['T']['form_qnr_field_qn_err']),
+                ],
+                'db' => [
+                    'pass' => 'Xss',
+                ],
+            ];
+        }
+
+        $aForm['inputs']['controls'] = [
+            'name' => 'controls',
+            'type' => 'input_set', [
+                'type' => 'submit',
+                'name' => 'do_submit',
+                'value' => _t('_Submit'),
+            ], [
+                'type' => 'reset',
+                'name' => 'close',
+                'value' => _t('_Cancel'),
+                'attrs' => [
+                    'onclick' => "$('.bx-popup-applied:visible').dolPopupHide()",
+                    'class' => 'bx-def-margin-sec-left',
+                ],
+            ]
+        ];
+
+    	return new BxTemplFormView($aForm);
+    }
+
+    protected function _checkAllowedConnectInitiator ($oInitiator, $isPerformAction = false)
+    {
+        if(!bx_srv($oInitiator->getModule(), 'act_as_profile'))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return parent::_checkAllowedConnectInitiator($oInitiator, $isPerformAction);
+    }
+
+    public function _checkAllowedConnectContent ($oContent)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(bx_srv($oContent->getModule(), 'act_as_profile'))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        if(!empty($CNF['OBJECT_PRIVACY_VIEW']) && ($oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW'])) !== false) {
+            $iContentId = $oContent->getContentId();
+            $aContentInfo = $this->_oModule->_oDb->getContentInfoById($iContentId);
+
+            if(in_array($aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']], array_merge($oPrivacy->getPartiallyVisiblePrivacyGroups(), ['s'])))
+                return CHECK_ACTION_RESULT_ALLOWED;
+        }
+
+        return parent::_checkAllowedConnectContent($oContent);
+    }
+}
+
+/** @} */

+ 605 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/groups/classes/BxBaseModGroupsDb.php

@@ -0,0 +1,605 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/*
+ * Groups module database queries
+ */
+class BxBaseModGroupsDb extends BxBaseModProfileDb
+{
+    public function __construct(&$oConfig)
+    {
+        parent::__construct($oConfig);
+    }
+
+    public function updateAuthorById ($iContentId, $iProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sQuery = "UPDATE `" . $this->_oConfig->CNF['TABLE_ENTRIES'] . "` SET `" . $CNF['FIELD_AUTHOR'] . "` = :author WHERE `" . $CNF['FIELD_ID'] . "` = :id";
+        return $this->query($sQuery, array(
+    		'id' => $iContentId,
+    		'author' => $iProfileId,
+    	));
+    }
+
+    public function toAdmins ($iGroupProfileId, $mixedFansIds)
+    {
+        if (is_array($mixedFansIds)) {
+            foreach ($mixedFansIds as $iFanId)
+                $this->toAdmins ($iGroupProfileId, $iFanId);
+
+            return true;
+        }
+
+        $iFanId = (int)$mixedFansIds;
+        $sQuery = $this->prepare("INSERT IGNORE INTO `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` SET `group_profile_id` = ?, `fan_id` = ?, `role` = ?", $iGroupProfileId, $iFanId, BX_BASE_MOD_GROUPS_ROLE_ADMINISTRATOR);
+        if (!$this->res($sQuery))
+            return false;
+
+        $oModule = BxDolModule::getInstance($this->_oConfig->getName());
+        if ($oModule && method_exists($oModule, 'onFanAddedToAdmins'))
+            $oModule->onFanAddedToAdmins($iGroupProfileId, $iFanId);
+
+        $oModule->doAudit($iGroupProfileId, $iFanId, '_sys_audit_action_group_to_admins');
+        
+        return true;
+    }
+
+    public function fromAdmins ($iGroupProfileId, $mixedFansIds)
+    {
+        if (is_array($mixedFansIds)) {
+            foreach ($mixedFansIds as $iFanId)
+                $this->fromAdmins ($iGroupProfileId, $iFanId);
+            return true;
+        }
+
+        $iFanId = (int)$mixedFansIds;
+        $sQuery = $this->prepare("DELETE FROM `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` WHERE `group_profile_id` = ? AND `fan_id` = ?", $iGroupProfileId, $iFanId);
+        if (!$this->res($sQuery))
+            return false;
+
+        $oModule = BxDolModule::getInstance($this->_oConfig->getName());
+        if ($oModule && method_exists($oModule, 'onFanRemovedFromAdmins'))
+            $oModule->onFanRemovedFromAdmins($iGroupProfileId, $iFanId);
+        
+        $oModule->doAudit($iGroupProfileId, $iFanId, '_sys_audit_action_group_from_admins');
+        
+        return true;
+    }
+
+    public function deleteAdminsByGroupId ($iGroupProfileId, $iProfileId = 0)
+    {
+        if ($iProfileId != 0)
+            $sQuery = $this->prepare("DELETE FROM `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` WHERE `group_profile_id` = ? AND `fan_id` = ?", $iGroupProfileId, $iProfileId);
+        else
+            $sQuery = $this->prepare("DELETE FROM `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` WHERE `group_profile_id` = ?", $iGroupProfileId);
+        
+        return $this->res($sQuery);
+    }
+
+    public function deleteAdminsByProfileId ($iProfileId)
+    {
+        $sQuery = $this->prepare("DELETE FROM `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` WHERE `fan_id` = ?", $iProfileId);
+        return $this->res($sQuery);
+    }
+
+    public function isAdmin ($iGroupProfileId, $iFanId, $aDataEntry = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(isset($aDataEntry[$CNF['FIELD_AUTHOR']]) && $iFanId == $aDataEntry[$CNF['FIELD_AUTHOR']])
+            return true;
+
+        if(!isset($CNF['TABLE_ADMINS']))
+            return false;
+
+        $aBindings = array(
+            'group_profile_id' => $iGroupProfileId, 
+            'fan_id' => $iFanId, 
+            'role' => BX_BASE_MOD_GROUPS_ROLE_COMMON
+        );
+        $sWhereClause = " AND `group_profile_id` = :group_profile_id AND `fan_id` = :fan_id AND `role` <> :role";
+
+        return $this->getOne("SELECT `id` FROM `" . $CNF['TABLE_ADMINS'] . "` WHERE 1" . $sWhereClause, $aBindings) ? true : false;
+    }
+
+    public function getAdmin ($iGroupProfileId, $iProfileId, $bRoleOnly = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sMethod = 'getRow';
+        $sSelectClause = '*';
+
+        if($bRoleOnly) {
+            $sMethod = 'getOne';
+            $sSelectClause = '`role`';
+        }
+
+        $aBindings = array('group_profile_id' => $iGroupProfileId, 'fan_id' => $iProfileId, 'role' => BX_BASE_MOD_GROUPS_ROLE_COMMON);
+        $sWhereClause = " AND `group_profile_id` = :group_profile_id AND `fan_id` = :fan_id AND `role` <> :role";
+
+        return $this->$sMethod("SELECT " . $sSelectClause . " FROM `" . $CNF['TABLE_ADMINS'] . "` WHERE 1" . $sWhereClause . " LIMIT 1", $aBindings);
+    }
+
+    public function getAdmins ($iGroupProfileId, $iStart = 0, $iLimit = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aBindings = array('group_profile_id' => $iGroupProfileId, 'role' => BX_BASE_MOD_GROUPS_ROLE_COMMON);
+        $sWhereClause = " AND `group_profile_id` = :group_profile_id AND `role` <> :role";
+
+        $sLimitClause = "";
+        if($iLimit > 0)
+            $sLimitClause = " LIMIT " . (int)$iStart . ", " . (int)$iLimit;
+
+        return $this->getColumn("SELECT `fan_id` FROM `" . $CNF['TABLE_ADMINS'] . "` INNER JOIN `sys_profiles` as `p` ON (`p`.`id` = `fan_id` AND `p`.`status` = 'active') WHERE 1" . $sWhereClause . $sLimitClause, $aBindings);
+    }
+
+    public function getRole($iGroupProfileId, $iProfileId)
+    {
+        return (int)$this->getRoles(array('type' => 'by_gf_id', 'group_profile_id' => $iGroupProfileId, 'fan_id' => $iProfileId, 'role_only' => true));
+    }
+
+    public function getRoles($aParams)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+
+        $sSelectClause = "`ta`.*";
+        $sJoinClause = $sWhereClause = $sOrderClause = $sLimitClause = "";
+
+        switch($aParams['type']) {
+            case 'by_id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'id' => $aParams['value']
+                );
+
+                $sWhereClause = "AND `ta`.`id`=:id";
+                break;
+
+            case 'by_gf_id':
+                $aMethod['name'] = 'getRow';
+            	$aMethod['params'][1] = array(
+                    'group_profile_id' => $aParams['group_profile_id'],
+                    'fan_id' => $aParams['fan_id'],
+                );
+
+                $sWhereClause = "AND `group_profile_id` = :group_profile_id AND `fan_id` = :fan_id";
+                $sLimitClause = 1;
+
+                if(!empty($aParams['role_only'])) {
+                    $aMethod['name'] = 'getOne';
+                    $sSelectClause = "`ta`.`role`";
+                }
+                break;
+
+            case 'fan_pids_by_group_pid':
+                $aMethod['name'] = 'getColumn';
+            	$aMethod['params'][1] = array(
+                    'group_profile_id' => $aParams['group_profile_id'],
+                );
+
+                $sSelectClause = "`ta`.`fan_id`";
+                $sWhereClause = "AND `group_profile_id` = :group_profile_id";
+
+                if(!empty($aParams['role'])) {
+                    $aMethod['params'][1]['role'] = $aParams['role'];
+
+                    $sWhereClause .= " AND `ta`.`role` = :role";
+                }
+
+                if(isset($aParams['start'], $aParams['limit']))
+                    $sLimitClause = $aParams['start'] . ', ' . $aParams['limit'];
+                break;
+
+            case 'group_pids_by_fan_id':
+                $aMethod['name'] = 'getColumn';
+                $aMethod['params'][1] = [
+                    'fan_id' => $aParams['fan_id'],
+                ];
+
+                $sSelectClause = "`ta`.`group_profile_id`";
+                $sWhereClause = "AND `ta`.`fan_id` = :fan_id";
+
+                if(!empty($aParams['role'])) {
+                    $aMethod['params'][1]['role'] = $aParams['role'];
+
+                    $sWhereClause .= " AND `ta`.`role` = :role";
+                }
+                break;
+
+            case 'expired':
+                $sWhereClause .= "AND `ta`.`added` < UNIX_TIMESTAMP() AND `ta`.`expired` <> 0 AND `ta`.`expired` < UNIX_TIMESTAMP()";
+                break;
+        }
+
+        if(!empty($sOrderClause))
+            $sOrderClause = "ORDER BY " . $sOrderClause;
+        
+        if(!empty($sLimitClause))
+            $sLimitClause = "LIMIT " . $sLimitClause;
+
+        $sSql = "SELECT {select} FROM `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` AS `ta` " . $sJoinClause . " WHERE 1 " . $sWhereClause . " {order} {limit}";
+
+        $aMethod['params'][0] = str_replace(array('{select}', '{order}', '{limit}'), array($sSelectClause, $sOrderClause, $sLimitClause), $sSql);
+        return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+
+    public function updateRoles($aSet, $aWhere)
+    {
+        $sWhereClause = 1;
+        if(!empty($aWhere))
+            $sWhereClause = $this->arrayToSQL($aWhere, ' AND ');
+
+        return (int)$this->query("UPDATE `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` SET " . $this->arrayToSQL($aSet) . " WHERE " . $sWhereClause) > 0;
+    }
+    
+    public function deleteRoles($aWhere)
+    {
+    	if(empty($aWhere))
+            return false;
+
+        return (int)$this->query("DELETE FROM `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` WHERE " . $this->arrayToSQL($aWhere, ' AND ')) > 0;
+    }
+
+    public function setRole ($iGroupProfileId, $iProfileId, $mixedRole, $mixedPeriod = false, $sOrder = '')
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if($this->_oConfig->isMultiRoles() && is_array($mixedRole)) {
+            if(count($mixedRole) != 1 || current($mixedRole) != 0) {
+                $iProfileRole = 0;
+
+                foreach($mixedRole as $iRole)
+                    $iProfileRole = $iProfileRole | pow(2, ($iRole - 1));
+
+                $mixedRole = $iProfileRole;
+            }
+            else 
+                $mixedRole = 0;
+        }
+
+        $aBindings = array(
+            'group_profile_id' => $iGroupProfileId,
+            'fan_id' => $iProfileId,
+            'role' => $mixedRole,
+            'added' => time()
+        );
+
+        $sSetClause = "`group_profile_id` = :group_profile_id, `fan_id` = :fan_id, `role` = :role, `added` = :added";
+
+        if(!empty($mixedPeriod)) {
+            if(is_numeric($mixedPeriod))
+        	$mixedPeriod = array('period' => (int)$mixedPeriod, 'period_unit' => BX_BASE_MOD_GROUPS_PERIOD_UNIT_DAY);
+
+            $aBindings['period'] = (int)$mixedPeriod['period'];
+
+            $sSetExpired = "";
+            switch($mixedPeriod['period_unit']) {
+                case BX_BASE_MOD_GROUPS_PERIOD_UNIT_DAY:
+                case BX_BASE_MOD_GROUPS_PERIOD_UNIT_WEEK:
+                    if($mixedPeriod['period_unit'] == BX_BASE_MOD_GROUPS_PERIOD_UNIT_WEEK)
+                        $aBindings['period'] *= 7;
+
+                    $sSetExpired = "DATE_ADD(FROM_UNIXTIME(:added), INTERVAL :period DAY)";
+                    break;
+
+                case BX_BASE_MOD_GROUPS_PERIOD_UNIT_MONTH:
+                    $sSetExpired = "DATE_ADD(FROM_UNIXTIME(:added), INTERVAL :period MONTH)";
+                    break;
+
+                case BX_BASE_MOD_GROUPS_PERIOD_UNIT_YEAR:
+                    $sSetExpired = "DATE_ADD(FROM_UNIXTIME(:added), INTERVAL :period YEAR)";
+                    break;
+            }
+
+            if(!empty($sSetExpired) && !empty($mixedPeriod['period_reserve'])) {
+                $aBindings['reserve'] = (int)$mixedPeriod['period_reserve'];
+
+                $sSetExpired = "DATE_ADD(" . $sSetExpired . ", INTERVAL :reserve DAY)";
+            }
+
+            if(!empty($sSetExpired))
+                $sSetClause .= ", `expired` = UNIX_TIMESTAMP(" . $sSetExpired . ")";
+        }
+
+        if(!empty($sOrder)) {
+            $aBindings['order'] = $sOrder;
+            
+            $sSetClause .= ", `order` = :order";
+        }
+
+        return !$this->query("REPLACE INTO `" . $CNF['TABLE_ADMINS'] . "` SET " . $sSetClause, $aBindings) ? false : true;
+    }
+
+    public function unsetRole($iGroupProfileId, $iProfileId)
+    {
+        return $this->deleteRoles(array('group_profile_id' => $iGroupProfileId, 'fan_id' => $iProfileId));
+    }
+
+    public function getPrices($aParams)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+
+        $sSelectClause = "`tp`.*";
+        $sJoinClause = $sWhereClause = $sOrderClause = $sLimitClause = "";
+
+        if(!isset($aParams['order']) || empty($aParams['order']))
+            $sOrderClause = "ORDER BY `tp`.`Order` ASC";
+
+        switch($aParams['type']) {
+            case 'by_id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'id' => $aParams['value']
+                );
+
+                $sWhereClause .= "AND `tp`.`id`=:id";
+                break;
+
+            case 'by_name':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'id' => $aParams['value']
+                );
+
+                $sWhereClause .= "AND `tp`.`name`=:name";
+                break;
+
+            case 'by_profile_id':
+            	$aMethod['params'][1] = array(
+                    'profile_id' => $aParams['profile_id']
+                );
+
+                $sWhereClause .= "AND `tp`.`profile_id`=:profile_id";
+                break;
+
+            case 'by_prpp':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'profile_id' => $aParams['profile_id'],
+                    'role_id' => $aParams['role_id'],
+                    'period' => $aParams['period'],
+                    'period_unit' => $aParams['period_unit'],
+                );
+
+                $sWhereClause .= "AND `tp`.`profile_id`=:profile_id AND `tp`.`role_id`=:role_id AND `tp`.`period`=:period AND `tp`.`period_unit`=:period_unit";
+                break;
+        }
+
+        $sSql = "SELECT {select} FROM `" . $this->_oConfig->CNF['TABLE_PRICES'] . "` AS `tp` " . $sJoinClause . " WHERE 1 " . $sWhereClause . " {order} {limit}";
+
+        $aMethod['params'][0] = str_replace(array('{select}', '{order}', '{limit}'), array($sSelectClause, $sOrderClause, $sLimitClause), $sSql);
+        return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+
+    public function getPriceOrderMax($iRoleId)
+    {
+        return (int)$this->getOne("SELECT MAX(`order`) FROM `" . $this->_oConfig->CNF['TABLE_PRICES'] . "` WHERE `role_id`=:role_id", array(
+            'role_id' => $iRoleId
+        ));
+    }
+
+    public function deletePrices($aWhere)
+    {
+        return (int)$this->query("DELETE FROM `" . $this->_oConfig->CNF['TABLE_PRICES'] . "` WHERE " . $this->arrayToSQL($aWhere, " AND ")) > 0;
+    }
+
+    public function insertInvite($sKey, $sGroupProfileId, $iAuthorProfileId, $iInvitedProfileId)
+    {
+        $aBindings = array(
+            'key' => $sKey,
+            'group_profile_id' => $sGroupProfileId,
+            'author_profile_id' => $iAuthorProfileId,
+            'invited_profile_id' => $iInvitedProfileId,
+            'added' => time()
+        );
+        $CNF = $this->_oConfig->CNF; 
+        $this->query("INSERT `" . $CNF["TABLE_INVITES"] . "` (`key`, `group_profile_id`, `author_profile_id`, `invited_profile_id`, `added`) VALUES (:key, :group_profile_id, :author_profile_id, :invited_profile_id, :added)", $aBindings);
+        return (int)$this->lastId();
+    }
+    
+    public function getInviteByKey($sKey, $iGroupProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        return $this->getRow("SELECT * FROM `" . $CNF["TABLE_INVITES"] . "` WHERE `key` = :key AND group_profile_id = :group_profile_id", [
+            'key' => $sKey,
+            'group_profile_id' => $iGroupProfileId
+        ]);
+    }
+
+    public function getInviteByInvited($iInvitedProfileId, $iGroupProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF; 
+
+        return $this->getRow("SELECT * FROM `" . $CNF["TABLE_INVITES"] . "` WHERE `invited_profile_id` = :invited_profile_id AND group_profile_id = :group_profile_id", [
+            'invited_profile_id' => $iInvitedProfileId,
+            'group_profile_id' => $iGroupProfileId
+        ]);
+    }
+
+    public function isInviteByInvited($iInvitedProfileId, $iGroupProfileId)
+    {
+        $aInvite = $this->getInviteByInvited($iInvitedProfileId, $iGroupProfileId);
+        return !empty($aInvite) && is_array($aInvite);
+    }
+
+    public function updateInviteByKey($sKey, $iGroupProfileId, $sColumn, $sValue)
+    {
+        $aBindings = array(
+           'key' => $sKey,
+           'value' => $sValue,
+           'group_profile_id' => $iGroupProfileId
+       );
+        $CNF = $this->_oConfig->CNF; 
+        return $this->query("UPDATE `" . $CNF["TABLE_INVITES"] . "` SET `" . $sColumn . "` = :value WHERE `key` = :key AND group_profile_id = :group_profile_id", $aBindings);
+    }
+    
+    public function deleteInviteByKey($sKey, $iGroupProfileId)
+    {
+        $aBindings = array(
+           'key' => $sKey,
+           'group_profile_id' => $iGroupProfileId
+       );
+        $CNF = $this->_oConfig->CNF; 
+        return $this->query("DELETE FROM `" . $CNF["TABLE_INVITES"] . "` WHERE `key` = :key AND group_profile_id = :group_profile_id", $aBindings);
+    }
+    
+    public function deleteInvite($iId)
+    {
+        $aBindings = array(
+           'id' => $iId
+       );
+        $CNF = $this->_oConfig->CNF; 
+        return $this->query("DELETE FROM `" . $CNF["TABLE_INVITES"] . "` WHERE `id` = :id", $aBindings);
+    }
+
+    public function getQuestions($aParams = [])
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+    	$aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
+
+        $sSelectClause = "`tq`.*";
+        $sJoinClause = $sWhereClause = $sOrderClause = $sLimitClause = "";
+        switch($aParams['sample']) {
+            case 'id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = [
+                    'id' => $aParams['id']
+                ];
+
+                $sWhereClause = " AND `tq`.`id`=:id";
+                break;
+            
+             case 'content_id':
+                $aMethod['params'][1] = [
+                    'content_id' => $aParams['content_id']
+                ];
+
+                $sWhereClause = " AND `tq`.`content_id`=:content_id";
+                $sOrderClause = "`tq`.`order` ASC";
+                break;
+            
+            case 'content_pid':
+                $aMethod['params'][1] = [
+                    'module' => $this->_oConfig->getName(),
+                    'content_pid' => $aParams['content_pid']
+                ];
+
+                $sJoinClause = "INNER JOIN `sys_profiles` AS `tp` ON `tq`.`content_id`=`tp`.`content_id` AND `tp`.`type`=:module";
+                $sWhereClause = " AND `tp`.`id`=:content_pid";
+                $sOrderClause = "`tq`.`order` ASC";
+                break;
+
+            case 'answers':
+                $aMethod['params'][1] = [
+                    'content_id' => $aParams['content_id'],
+                    'profile_id' => $aParams['profile_id']
+                ];
+
+                $sSelectClause .= ", `ta`.`answer`";
+                $sJoinClause = "INNER JOIN `" . $CNF['TABLE_ANSWERS'] . "` AS `ta` ON `tq`.`id`=`ta`.`question_id` AND `ta`.`profile_id`=:profile_id";
+                $sWhereClause = " AND `tq`.`content_id`=:content_id";
+                break;
+        }
+
+        if(!empty($sOrderClause))
+            $sOrderClause = 'ORDER BY ' . $sOrderClause;
+
+        if(!empty($sLimitClause))
+            $sLimitClause = 'LIMIT ' . $sLimitClause;
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . " FROM `" . $CNF['TABLE_QUESTIONS'] . "` AS `tq` " . $sJoinClause . " WHERE 1" . $sWhereClause . " " . $sOrderClause . " " . $sLimitClause;
+
+        return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
+    }
+    
+    public function hasQuestions($iContentId)
+    {
+        $aQuestions = $this->getQuestions([
+            'sample' => 'content_id', 
+            'content_id' => $iContentId
+        ]);
+
+        return !empty($aQuestions) && is_array($aQuestions);
+    }
+    
+    public function areQuestionsAnswered($iContentId, $iProfileId)
+    {
+        $aQuestions = $this->getQuestions([
+            'sample' => 'answers', 
+            'content_id' => $iContentId,
+            'profile_id' => $iProfileId
+        ]);
+
+        return !empty($aQuestions) && is_array($aQuestions);
+    }
+
+    public function getQuestionOrderMax($iContentId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        return (int)$this->getOne("SELECT MAX(`order`) FROM `" . $CNF['TABLE_QUESTIONS'] . "` WHERE `content_id`=:content_id", [
+            'content_id' => $iContentId
+        ]);
+    }
+
+    public function insertAnswer($iQuestionId, $iProfileId, $sAnswer)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sSetClause = $this->arrayToSQL([
+            'question_id' => $iQuestionId, 
+            'profile_id' => $iProfileId, 
+            'answer' => $sAnswer
+        ]);
+
+        return $this->query("INSERT INTO `" . $CNF['TABLE_ANSWERS'] . "` SET " . $sSetClause . ", `added`=UNIX_TIMESTAMP() ON DUPLICATE KEY UPDATE `answer`=:answer, `added`=UNIX_TIMESTAMP()", [
+            'answer' => $sAnswer
+        ]) !== false ? (int)$this->lastId() : false;
+    }
+
+    public function deleteAnswersProfileId($iContentId, $iProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        return $this->query("DELETE FROM `ta` USING `" . $CNF['TABLE_QUESTIONS'] . "` AS `tq` LEFT JOIN `" . $CNF['TABLE_ANSWERS'] . "` AS `ta` ON `tq`.`id`=`ta`.`question_id` WHERE `tq`.`content_id`=:content_id AND `ta`.`profile_id`=:profile_id", [
+            'content_id' => $iContentId,
+            'profile_id' => $iProfileId
+        ]) !== false;
+    }
+
+    public function deleteQuestionnaires($iContentId) 
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $this->query("DELETE FROM `tq`, `ta` USING `" . $CNF['TABLE_QUESTIONS'] . "` AS `tq` LEFT JOIN `" . $CNF['TABLE_ANSWERS'] . "` AS `ta` ON `tq`.`id`=`ta`.`question_id` WHERE `tq`.`content_id`=:content_id", [
+            'content_id' => $iContentId
+        ]) !== false;
+    }
+
+    protected function _getEntriesBySearchIds($aParams, &$aMethod, &$sSelectClause, &$sJoinClause, &$sWhereClause, &$sOrderClause, &$sLimitClause)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!empty($CNF['FIELD_STATUS']))
+            $sWhereClause .= " AND `" . $CNF['TABLE_ENTRIES'] . "`.`" . $CNF['FIELD_STATUS'] . "`='active'";
+
+        if(!empty($CNF['FIELD_STATUS_ADMIN']))
+            $sWhereClause .= " AND `" . $CNF['TABLE_ENTRIES'] . "`.`" . $CNF['FIELD_STATUS_ADMIN'] . "`='active'";
+
+        parent::_getEntriesBySearchIds($aParams, $aMethod, $sSelectClause, $sJoinClause, $sWhereClause, $sOrderClause, $sLimitClause);        
+    }
+}
+
+/** @} */

+ 86 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/groups/classes/BxBaseModGroupsGridBans.php

@@ -0,0 +1,86 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ * 
+ * @{
+ */
+
+class BxBaseModGroupsGridBans extends BxDolGridConnections
+{
+    protected $_oModule;
+    protected $_sContentModule;
+
+    protected $_iGroupProfileId;
+    protected $_aContentInfo = [];
+
+    protected $_bManageMembers;
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        $this->_oModule = BxDolModule::getInstance($this->_sContentModule);
+
+        $this->_sObjectConnections = 'sys_profiles_bans';
+
+        parent::__construct ($aOptions, $oTemplate);
+        if(!$this->_bInit) 
+            return;
+
+        $this->_iGroupProfileId = $this->_oProfile->id();
+
+        $this->_aContentInfo = $this->_oModule->_oDb->getContentInfoById($this->_oProfile->getContentId());
+        if($this->_oModule->checkAllowedEdit($this->_aContentInfo) === CHECK_ACTION_RESULT_ALLOWED || $this->_iGroupProfileId == bx_get_logged_profile_id())
+            $this->_bOwner = true;
+
+        $this->_bManageMembers = $this->_oModule->checkAllowedManageFans($this->_iGroupProfileId) === CHECK_ACTION_RESULT_ALLOWED || $this->_oModule->checkAllowedManageAdmins($this->_iGroupProfileId) === CHECK_ACTION_RESULT_ALLOWED;
+
+        $aSQLParts = $this->_oConnection->getConnectedContentAsSQLParts('p', 'id', $this->_iGroupProfileId);
+        $this->addMarkers(array(
+            'profile_id' => $this->_iGroupProfileId,
+            'join_connections' => $aSQLParts['join'],
+            'content_module' => $this->_sContentModule,
+        ));
+    }
+
+    public function getCode ($isDisplayHeader = true)
+    {
+        if(!$this->_bManageMembers)
+            return '';
+
+        return parent::getCode($isDisplayHeader);        
+    }
+
+    protected function _getCellAdded ($mixedValue, $sKey, $aField, $aRow)
+    {
+        return parent::_getCellDefault(bx_time_js($mixedValue), $sKey, $aField, $aRow);
+    }
+
+    protected function _getActionDelete ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if (!$this->_bManageMembers)
+            return $this->_bIsApi ? [] : '';
+
+        if (isset($aRow[$this->_aOptions['field_id']]))
+            $a['attr']['bx_grid_action_data'] = $aRow[$this->_aOptions['field_id']] . ':' . $this->_iGroupProfileId;
+
+        return parent::_getActionDelete($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _delete ($mixedId)
+    {
+        list($iProfileId, $iContextId) = $this->__prepareIds($mixedId);
+
+        if($this->_oConnection->isConnected($iContextId, $iProfileId))
+            $a = $this->_oConnection->removeConnection($iContextId, $iProfileId);
+
+        if(isset($a['err']) && $a['err'])
+            return false;
+
+        return true;
+    }
+}
+
+/** @} */

+ 474 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/groups/classes/BxBaseModGroupsGridConnections.php

@@ -0,0 +1,474 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ * 
+ * @{
+ */
+
+class BxBaseModGroupsGridConnections extends BxDolGridConnections
+{
+    protected $_oModule;
+    protected $_sContentModule;
+
+    protected $_bRoles;
+    protected $_aRoles;
+
+    protected $_iGroupProfileId;
+    protected $_aContentInfo = array();
+
+    protected $_bPaidJoin;
+    protected $_bMember;
+    protected $_bManageMembers;
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        $this->_oModule = BxDolModule::getInstance($this->_sContentModule);
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $this->_sObjectConnections = $CNF['OBJECT_CONNECTIONS'];
+
+        parent::__construct ($aOptions, $oTemplate);
+        if(!$this->_bInit) 
+            return;
+
+        $this->_bRoles = $this->_oModule->_oConfig->isRoles();
+        $this->_aRoles = $this->_oModule->_oConfig->getRoles();
+
+        $this->_iGroupProfileId = $this->_oProfile->id();
+
+        $this->_aContentInfo = $this->_oModule->_oDb->getContentInfoById($this->_oProfile->getContentId());
+        if($this->_oModule->checkAllowedEdit($this->_aContentInfo) === CHECK_ACTION_RESULT_ALLOWED || $this->_iGroupProfileId == bx_get_logged_profile_id())
+            $this->_bOwner = true;
+
+        $this->_bPaidJoin = $this->_oModule->isPaidJoinByProfile($this->_iGroupProfileId);
+
+        $this->_bMember = $this->_oModule->isFan($this->_aContentInfo[$CNF['FIELD_ID']]);
+        $this->_bManageMembers = $this->_oModule->checkAllowedManageFans($this->_iGroupProfileId) === CHECK_ACTION_RESULT_ALLOWED || $this->_oModule->checkAllowedManageAdmins($this->_iGroupProfileId) === CHECK_ACTION_RESULT_ALLOWED;
+
+        $aSQLParts = $this->_oConnection->getConnectedInitiatorsAsSQLParts('p', 'id', $this->_iGroupProfileId, $this->_bOwner ? false : true);
+        $this->addMarkers(array(
+            'profile_id' => $this->_iGroupProfileId,
+            'join_connections' => $aSQLParts['join'],
+            'content_module' => $this->_sContentModule,
+        ));
+    }
+
+    public function getCode ($isDisplayHeader = true)
+    {
+        $sResult = parent::getCode($isDisplayHeader);
+        if(!$sResult)
+            return $sResult;
+
+        if($this->_bRoles)
+            $sResult .= $this->_oModule->_oTemplate->getJsCode('main', array(
+                'sObjNameGrid' => $this->_sObject,
+            ));
+
+        return $sResult;
+    }
+
+    public function performActionQuestionnaire()
+    {
+       
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(($mixedResult = $this->_oModule->checkAllowedManageAdmins($this->_iGroupProfileId)) !== CHECK_ACTION_RESULT_ALLOWED)
+            return echoJson(['msg' => $mixedResult]);
+
+        list($iId, $iViewedId) = $this->_prepareIds();
+        if($this->_bIsApi){
+            $iId = bx_get($CNF['FIELD_ID']);
+        }
+        if(!$iId)
+            return echoJson(['msg' => _t('_sys_txt_error_occured')]);
+
+        $sPopupContent = $this->_oModule->_oTemplate->getPopupQuestionnaire($this->_aContentInfo[$CNF['FIELD_ID']], $iId);
+        
+        if($this->_bIsApi){
+             return [bx_api_get_block('simple_list',  $sPopupContent)];
+        }
+        
+        if(!$sPopupContent)
+            return echoJson([]);
+
+        $oFunctions = BxTemplFunctions::getInstance();
+        return echoJson(['popup' => $oFunctions->transBox(str_replace('_', '-', $this->_sContentModule) . '-questionnaire-popup', $oFunctions->simpleBoxContent($sPopupContent))]);
+    }
+
+    public function performActionSetRole()
+    {
+        if(!$this->_bRoles)
+            return echoJson(array());
+
+        if(($mixedResult = $this->_oModule->checkAllowedManageAdmins($this->_iGroupProfileId)) !== CHECK_ACTION_RESULT_ALLOWED)
+            return echoJson(array('msg' => $mixedResult));
+
+        list($iId, $iViewedId) = $this->_prepareIds();
+        if(!$iId)
+            return echoJson(array('msg' => _t('_sys_txt_error_occured')));
+
+        if(empty($this->_aRoles) || !is_array($this->_aRoles))
+            return echoJson(array('msg' => _t('_sys_txt_error_occured')));
+
+        $iRole = $this->_oModule->_oDb->getRole($this->_iGroupProfileId, $iId);
+
+        if(!$this->_oModule->_oConfig->isMultiRoles()) {
+            $sJsObject = $this->_oModule->_oConfig->getJsObject('main');
+            $sHtmlIdPrefix = str_replace('_', '-', $this->_sContentModule) . '-set-role-';
+
+            $aMenuItems = array();
+            foreach($this->_aRoles as $iRoleId => $sRoleTitle)
+                $aMenuItems[] = array(
+                    'id' => $sHtmlIdPrefix . $iRoleId, 
+                    'name' => $sHtmlIdPrefix . $iRoleId, 
+                    'class' => '', 
+                    'link' => 'javascript:void(0)', 
+                    'onclick' => $sJsObject . '.onClickSetRole(' . $iId . ', ' . $iRoleId . ')',
+                    'target' => '_self', 
+                    'title' => $sRoleTitle, 
+                    'active' => 1
+                );            
+
+            $oMenu = new BxTemplMenu(array('template' => 'menu_vertical.html', 'menu_id'=> $sHtmlIdPrefix . 'menu', 'menu_items' => $aMenuItems));
+            if(!empty($iRole))
+                $oMenu->setSelected('', $sHtmlIdPrefix . $iRole);
+
+            $sPopupContent = $oMenu->getCode();
+        }
+        else
+            $sPopupContent = $this->_oModule->_oTemplate->getPopupSetRole($this->_aRoles, $iId, $iRole);
+
+        $oFunctions = BxTemplFunctions::getInstance();
+        return echoJson(array('popup' => $oFunctions->transBox(str_replace('_', '-', $this->_sContentModule) . '-set-role-popup', $oFunctions->simpleBoxContent($sPopupContent))));
+    }
+
+    public function performActionSetRoleSubmit()
+    {
+        if(!$this->_bRoles)
+            return echoJson(array());
+
+        if(($mixedResult = $this->_oModule->checkAllowedManageAdmins($this->_iGroupProfileId)) !== CHECK_ACTION_RESULT_ALLOWED)
+            return echoJson(array('msg' => $mixedResult));
+
+        list($iId, $iViewedId) = $this->_prepareIds();
+
+        if(!$iId)
+            return echoJson(array('msg' => _t('_sys_txt_error_occured')));
+
+        if(!$this->_oModule->setRole($this->_iGroupProfileId, $iId, bx_process_input(bx_get('role'), BX_DATA_INT)))
+            return echoJson(array('msg' => _t('_error occured')));
+
+        echoJson(array('grid' => $this->getCode(false), 'blink' => $iId));
+    }
+    
+    public function performActionDeleteAndBan()
+    {
+        $this->_replaceMarkers ();
+
+        $aResult = [];
+        $iAffected = 0;
+
+        $aIds = bx_get('ids');
+        if(!$aIds || !is_array($aIds))
+            return $this->_getActionResult($aResult);
+
+        $oConnection = BxDolConnection::getObjectInstance('sys_profiles_bans');
+
+        foreach($aIds as $mixedId) {
+            if(!$this->_delete($mixedId))
+                continue;
+
+            if($oConnection !== false) {
+                list($iProfileId, $iContextId) = $this->__prepareIds($mixedId);
+
+                $oConnection->addConnection($iContextId, $iProfileId, [
+                    'module' => $this->_sContentModule
+                ]);
+            }
+
+            $iAffected += 1;
+        }
+
+        if($iAffected)
+            $aResult = !$this->_bIsApi ? ['grid' => $this->getCode(false)] : [];
+        else
+            $aResult = ['msg' => _t("_sys_grid_delete_failed")];
+
+        return $this->_getActionResult($aResult);
+    }
+
+    protected function _getCellRole($mixedValue, $sKey, $aField, $aRow)
+    {
+        $iProfileRole = $this->_oModule->_oDb->getRole($this->_iGroupProfileId, $aRow[$this->_aOptions['field_id']]);
+
+        if(!empty($iProfileRole) && $this->_oModule->_oConfig->isMultiRoles()) {
+            $aRoles = array();
+            foreach($this->_aRoles as $iRole => $sRole) {
+                if(!$iRole)
+                    continue;
+
+                if($iProfileRole & (1 << ($iRole - 1)))
+                    $aRoles[] = $sRole;
+            }
+
+            $mixedValue = implode(', ', $aRoles);
+        }
+        else 
+            $mixedValue = !empty($this->_aRoles[$iProfileRole]) ? $this->_aRoles[$iProfileRole] : _t('_uknown');
+
+        return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellHeaderRoleAdded($sKey, $aField)
+    {
+        if(!$this->_bPaidJoin || !($this->_bMember || $this->_bManageMembers))
+            return $this->_bIsApi ? [] : '';
+
+        return parent::_getCellHeaderDefault ($sKey, $aField);
+    }
+
+    protected function _getCellRoleAdded($mixedValue, $sKey, $aField, $aRow)
+    {
+        $mixedValue = '';
+        if(!$this->_bPaidJoin || !($this->_bMember || $this->_bManageMembers))
+            return $this->_bIsApi ? [] : $mixedValue;
+
+        $iProfileId = (int)$aRow[$this->_aOptions['field_id']];
+        if($this->_bManageMembers || $iProfileId == bx_get_logged_profile_id()) {
+            $aRole = $this->_oModule->_oDb->getRoles(array('type' => 'by_gf_id', 'group_profile_id' => $this->_iGroupProfileId, 'fan_id' => $iProfileId));
+            if(!empty($aRole) && is_array($aRole)) {
+                if(!empty($aRole['added'])) {
+                    if($this->_bIsApi)
+                        return ['type' => 'time', 'data' => $aRole['added']];
+
+                    $mixedValue = bx_time_js($aRole['added'], BX_FORMAT_DATE, true);
+                }
+                else {
+                    if($this->_bIsApi)
+                        return ['type' => 'text', 'data' => ''];
+
+                    $mixedValue = '';
+                }
+            }
+        }
+
+        return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellHeaderRoleExpired($sKey, $aField)
+    {
+        if(!$this->_bPaidJoin || !($this->_bMember || $this->_bManageMembers))
+            return $this->_bIsApi ? [] : '';
+        
+        return parent::_getCellHeaderDefault ($sKey, $aField);
+    }
+
+    protected function _getCellRoleExpired($mixedValue, $sKey, $aField, $aRow)
+    {
+        $mixedValue = '';
+        if(!$this->_bPaidJoin || !($this->_bMember || $this->_bManageMembers))
+            return $this->_bIsApi ? [] : '';
+
+        $iProfileId = (int)$aRow[$this->_aOptions['field_id']];
+        if($this->_bManageMembers || $iProfileId == bx_get_logged_profile_id()) {
+            $aRole = $this->_oModule->_oDb->getRoles(array('type' => 'by_gf_id', 'group_profile_id' => $this->_iGroupProfileId, 'fan_id' => $iProfileId));
+            if(!empty($aRole) && is_array($aRole)) {
+                if(!empty($aRole['expired'])) {
+                    if($this->_bIsApi)
+                        return ['type' => 'time', 'data' => $aRole['expired']];
+
+                    $mixedValue = bx_time_js($aRole['expired'], BX_FORMAT_DATE, true);
+                }
+                else {
+                    if($this->_bIsApi)
+                        return ['type' => 'text', 'data' => ''];
+
+                    $mixedValue = '';
+                }
+            }
+        }
+
+        return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getActionSetRole ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        /**
+         * Note. The feature isn't available in API for now.
+         */
+        if($this->_bIsApi)
+            return [];
+
+        if ($this->_oModule->checkAllowedManageAdmins($this->_iGroupProfileId) !== CHECK_ACTION_RESULT_ALLOWED)
+            return $this->_bIsApi ? [] : '';
+
+        return parent::_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getActionSetRoleSubmit ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        return $this->_bIsApi ? [] : '';
+    }
+
+    protected function _getActionAccept ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if ($aRow['mutual'])
+            return $this->_bIsApi ? [] : '';
+
+        if ($this->_oModule->checkAllowedManageFans($this->_iGroupProfileId) !== CHECK_ACTION_RESULT_ALLOWED)
+            return $this->_bIsApi ? [] : '';
+
+        if (isset($aRow[$this->_aOptions['field_id']]))
+            $a['attr']['bx_grid_action_data'] = $aRow[$this->_aOptions['field_id']] . ':' . $this->_iGroupProfileId;
+
+        return parent::_getActionAccept($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getActionDelete ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if ($this->_oModule->checkAllowedManageFans($this->_iGroupProfileId) !== CHECK_ACTION_RESULT_ALLOWED)
+            return $this->_bIsApi ? [] : '';
+
+        if (isset($aRow[$this->_aOptions['field_id']]))
+            $a['attr']['bx_grid_action_data'] = $aRow[$this->_aOptions['field_id']] . ':' . $this->_iGroupProfileId;
+
+        return parent::_getActionDelete($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getActionDeleteAndBan ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = [])
+    {
+        return $this->_getActionDelete($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    /**
+     * Note. Methods related to 'To Admin'/'From Admin' functionality can be removed after UNA 12 will be released.
+     */
+    protected function _getActionToAdmins ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if ($this->_oModule->_oDb->isAdmin($this->_iGroupProfileId, $aRow[$this->_aOptions['field_id']]))
+            return '';
+
+        return $this->_getActionManageAdmins ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getActionFromAdmins ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if (!$this->_oModule->_oDb->isAdmin($this->_iGroupProfileId, $aRow[$this->_aOptions['field_id']]))
+            return '';
+
+        return $this->_getActionManageAdmins ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getActionManageAdmins ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if (!$this->_oConnection->isConnected($aRow[$this->_aOptions['field_id']], $this->_iGroupProfileId, true))
+            return '';
+
+        if ($this->_oModule->checkAllowedManageAdmins($this->_iGroupProfileId) !== CHECK_ACTION_RESULT_ALLOWED)
+            return '';
+
+        if (isset($aRow[$this->_aOptions['field_id']]))
+            $a['attr']['bx_grid_action_data'] = $aRow[$this->_aOptions['field_id']] . ':' . $this->_iGroupProfileId;
+
+        return parent::_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    /**
+     * 'To Admins' action handler
+     */
+    public function performActionToAdmins()
+    {
+        $this->_performActionAdmins('toAdmins');
+    }
+
+    /**
+     * 'From Admins' action handler
+     */
+    public function performActionFromAdmins()
+    {
+        $this->_performActionAdmins('fromAdmins');        
+    }
+
+    public function _performActionAdmins($sFunc)
+    {
+        list ($iId, $iGroupProfileId) = $this->_prepareIds();
+
+        if (!$iId) {
+            echoJson(array('msg' => _t('_sys_txt_error_occured')));
+            exit;
+        }
+
+        if (CHECK_ACTION_RESULT_ALLOWED !== $this->_oModule->checkAllowedManageAdmins($iGroupProfileId)) {
+            echoJson(array('msg' => _t('_sys_txt_access_denied')));
+            exit;
+        }
+    
+        if (!$this->_oModule->_oDb->$sFunc($iGroupProfileId, $iId)) {
+            echoJson(array('msg' => _t('_sys_txt_error_occured')));
+        } 
+        else {
+
+            $sEmailTemplate = 'toAdmins' == $sFunc ? $this->_oModule->_oConfig->CNF['EMAIL_FAN_BECOME_ADMIN'] : $this->_oModule->_oConfig->CNF['EMAIL_ADMIN_BECOME_FAN'];
+            list($iGroupProfileId, $iProfileId) = $this->_prepareGroupProfileAndMemberProfile($iGroupProfileId, $iId);
+            if (bx_get_logged_profile_id() != $iProfileId) {
+                // notify about admin status
+                sendMailTemplate($sEmailTemplate, 0, $iProfileId, array(
+                    'EntryUrl' => BxDolProfile::getInstance($iGroupProfileId)->getUrl(),
+                    'EntryTitle' => BxDolProfile::getInstance($iGroupProfileId)->getDisplayName(),
+                ), BX_EMAIL_NOTIFY);
+            }
+
+            // subscribe admins automatically
+            if ('toAdmins' == $sFunc && ($oConn = BxDolConnection::getObjectInstance('sys_profiles_subscriptions')))
+                $oConn->addConnection($iProfileId, $iGroupProfileId);
+
+            echoJson(array('grid' => $this->getCode(false), 'blink' => $iId));
+        }
+    }
+
+    protected function _delete ($mixedId)
+    {
+        list ($iId, $iViewedId) = $this->_prepareIds();
+
+        // send email notification
+        $sEmailTemplate = $this->_oConnection->isConnected($iViewedId, $iId, true) ? $this->_oModule->_oConfig->CNF['EMAIL_FAN_REMOVE'] : $this->_oModule->_oConfig->CNF['EMAIL_JOIN_REJECT'];
+        list($iGroupProfileId, $iProfileId) = $this->_prepareGroupProfileAndMemberProfile($iId, $iViewedId);
+        if (bx_get_logged_profile_id() != $iProfileId) {
+            sendMailTemplate($sEmailTemplate, 0, $iProfileId, array(
+                'EntryUrl' => BxDolProfile::getInstance($iGroupProfileId)->getUrl(),
+                'EntryTitle' => BxDolProfile::getInstance($iGroupProfileId)->getDisplayName(),
+            ), BX_EMAIL_NOTIFY);
+        }
+
+        // delete admin associated with profile
+        $this->_oModule->_oDb->deleteAdminsByGroupId($iGroupProfileId, $iProfileId);
+
+        return parent::_delete ($mixedId);
+    }
+
+    /**
+     * @return array where first element is group profile id and second element is some other persons profile id
+     */
+    protected function _prepareGroupProfileAndMemberProfile($iId1, $iId2)
+    {
+        if (BxDolProfile::getInstance($iId1)->getModule() == $this->_sContentModule)
+            return array($iId1, $iId2);
+        else
+            return array($iId2, $iId1);
+    }
+
+    protected function _addJsCss()
+    {
+        parent::_addJsCss();
+
+        if($this->_bRoles)
+            $this->_oModule->_oTemplate->addJs(['modules/base/groups/js/|main.js', 'main.js']);
+    }
+}
+
+/** @} */

+ 75 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/groups/classes/BxBaseModGroupsGridInvites.php

@@ -0,0 +1,75 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ * 
+ * @{
+ */
+
+class BxBaseModGroupsGridinvites extends BxTemplGrid
+{
+    protected $_oModule;
+    protected $_sContentModule;
+
+    protected $_iGroupProfileId;    
+
+    protected $_bManageMembers;
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        $this->_oModule = BxDolModule::getInstance($this->_sContentModule);
+
+        parent::__construct ($aOptions, $oTemplate);
+
+        $this->_iGroupProfileId = 0;
+        if(($iProfileId = bx_get('profile_id')) !== false)
+            $this->_iGroupProfileId = (int)$iProfileId;
+
+        $this->_bManageMembers = $this->_oModule->checkAllowedManageFans($this->_iGroupProfileId) === CHECK_ACTION_RESULT_ALLOWED || $this->_oModule->checkAllowedManageAdmins($this->_iGroupProfileId) === CHECK_ACTION_RESULT_ALLOWED;
+
+        $this->_aQueryAppend['profile_id'] = $this->_iGroupProfileId;
+    }
+
+    public function getCode ($isDisplayHeader = true)
+    {
+        if(!$this->_bManageMembers)
+            return '';
+
+        return parent::getCode($isDisplayHeader);        
+    }
+
+    protected function _getCellName ($mixedValue, $sKey, $aField, $aRow)
+    {
+        $oProfile = BxDolProfile::getInstance($aRow['invited_profile_id']);
+        if (!$oProfile)
+            return _t('_sys_txt_error_occured');
+
+        return parent::_getCellDefault ($oProfile->getUnit(), $sKey, $aField, $aRow);
+    }
+    
+    protected function _getCellAdded ($mixedValue, $sKey, $aField, $aRow)
+    {
+        return parent::_getCellDefault(bx_time_js($mixedValue), $sKey, $aField, $aRow);
+    }
+    
+    protected function _getActionDelete ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        $aCheck = checkActionModule(bx_get_logged_profile_id(), 'delete invites', $this->_sContentModule, false);
+        
+         if ($aRow['author_profile_id'] == bx_get_logged_profile_id() || $aCheck[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED)
+            return parent::_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+         
+         return '';
+    }
+    
+    protected function _getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage)
+    {
+        $this->_aOptions['source'] .= $this->_oModule->_oDb->prepareAsString(" AND `group_profile_id` = ?", (int)bx_get('profile_id'));
+        return parent::_getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage);
+    }
+}
+
+/** @} */

+ 2239 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/groups/classes/BxBaseModGroupsModule.php

@@ -0,0 +1,2239 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+define('BX_BASE_MOD_GROUPS_MMODE_MULTI_ROLES', 'multi_roles');
+
+define('BX_BASE_MOD_GROUPS_ROLE_COMMON', 0);
+define('BX_BASE_MOD_GROUPS_ROLE_ADMINISTRATOR', 1);
+define('BX_BASE_MOD_GROUPS_ROLE_MODERATOR', 2);
+
+define('BX_BASE_MOD_GROUPS_ACTION_EDIT', 'edit');
+define('BX_BASE_MOD_GROUPS_ACTION_CHANGE_COVER', 'change_cover');
+define('BX_BASE_MOD_GROUPS_ACTION_INVITE', 'invite');
+define('BX_BASE_MOD_GROUPS_ACTION_MANAGE_FANS', 'manage_fans');
+define('BX_BASE_MOD_GROUPS_ACTION_MANAGE_ROLES', 'manage_roles');
+define('BX_BASE_MOD_GROUPS_ACTION_DELETE', 'delete');
+define('BX_BASE_MOD_GROUPS_ACTION_EDIT_CONTENT', 'edit_any');
+define('BX_BASE_MOD_GROUPS_ACTION_DELETE_CONTENT', 'delete_any');
+define('BX_BASE_MOD_GROUPS_ACTION_TIMELINE_POST_PIN', 'pin'); //for timeline posts only
+
+define('BX_BASE_MOD_GROUPS_PERIOD_UNIT_DAY', 'day');
+define('BX_BASE_MOD_GROUPS_PERIOD_UNIT_WEEK', 'week');
+define('BX_BASE_MOD_GROUPS_PERIOD_UNIT_MONTH', 'month');
+define('BX_BASE_MOD_GROUPS_PERIOD_UNIT_YEAR', 'year');
+
+/**
+ * Groups profiles module.
+ */
+class BxBaseModGroupsModule extends BxBaseModProfileModule
+{
+    function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+
+        $CNF = &$this->_oConfig->CNF;
+
+        if(isset($CNF['FIELD_PUBLISHED']))
+            $this->_aSearchableNamesExcept = array_merge($this->_aSearchableNamesExcept, array(
+                $CNF['FIELD_PUBLISHED'],
+            ));
+    }
+
+    /**
+     * Get possible recipients for start conversation form
+     */
+    public function actionAjaxGetInitialMembers ()
+    {
+        $sTerm = bx_get('term');
+
+        $a = BxDolService::call('system', 'profiles_search', [$sTerm, ['module' => $this->_oConfig->getName()]], 'TemplServiceProfiles');
+
+        header('Content-Type:text/javascript; charset=utf-8');
+        echo(json_encode($a));
+    }
+    
+    /**
+     * Process Process Invitation
+     */
+    public function actionProcessInvite ($sKey, $iGroupProfileId, $bAccept)
+    {
+        $aData = $this->_oDb->getInviteByKey($sKey, $iGroupProfileId);
+        if (isset($aData['invited_profile_id'])){
+            $CNF = &$this->_oConfig->CNF;
+            if (!isset($CNF['OBJECT_CONNECTIONS']) || !($oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])))
+                return '';
+            $iInvitedProfileId = $aData['invited_profile_id'];
+            if ($iInvitedProfileId != bx_get_logged_profile_id())
+                return '';
+            if ($bAccept){
+                if($oConnection && !$oConnection->isConnected($iInvitedProfileId, $iGroupProfileId)){
+                    $oConnection->addConnection($iInvitedProfileId, $iGroupProfileId);
+                    $oConnection->addConnection($iGroupProfileId, $iInvitedProfileId);
+                }
+            }
+            $this->_oDb->deleteInviteByKey($sKey, $iGroupProfileId);
+        }   
+    }
+
+    public function actionCheckName()
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+    	$sName = bx_process_input(bx_get('name'));
+    	if(empty($sName))
+            return echoJson(array());
+
+        $sResult = '';
+
+        $iId = (int)bx_get('id');
+        if(!empty($iId)) {
+            $aPrice = $this->_oDb->getPrices(array('type' => 'by_id', 'value' => $iId)); 
+            if(strcmp($sName, $aPrice[$CNF['FIELD_PRICE_NAME']]) == 0) 
+                $sResult = $sName;
+        }
+
+    	echoJson(array(
+            'name' => !empty($sResult) ? $sResult : $this->_oConfig->getPriceName($sName)
+    	));
+    }
+
+    public function actionGetQuestionnaire()
+    {
+        $sSource = '';
+        if(($sSource = bx_get('s')) !== false)
+            $sSource = bx_process_url_param($sSource);
+
+        $sObject = '';
+        if(($sObject = bx_get('o')) !== false)
+            $sObject = bx_process_url_param($sObject);
+        
+        $sAction = '';
+        if(($sAction = bx_get('a')) !== false)
+            $sAction = bx_process_url_param($sAction);
+
+        $iContentProfileId = 0;
+        if(($iContentProfileId = bx_get('cpi')) !== false)
+            $iContentProfileId = (int)$iContentProfileId;
+
+        echoJson($this->serviceGetQuestionnaire($sSource, $sObject, $sAction, $iContentProfileId));
+    }
+
+    public function serviceManageTools($sType = 'common')
+    {
+        $sResult = parent::serviceManageTools($sType);
+        if(!empty($sResult))
+            $this->_oTemplate->addJsSystem(['modules/base/groups/js/|manage_tools.js']);
+
+        return $sResult;
+    }
+    
+    public function decodeDataAPI($aData, $aParams = [])
+    {
+        $CNF = $this->_oConfig->CNF;
+        
+        $aResult = parent::decodeDataAPI($aData, $aParams);
+        
+        if ($aParams['template'] == 'unit_wo_info')
+            return $aResult;
+
+        if(getParam('sys_api_conn_in_prof_units') == 'on' && ($oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])) !== false) {
+            $oProfile = BxDolProfile::getInstanceByContentAndType($aData[$CNF['FIELD_ID']], $this->_aModule['name']);
+
+            $aResult['members_count'] = $oConnection->getConnectedInitiatorsCount($oProfile->id(), false);
+            $aResult['members_list'] = $oConnection->getConnectedListAPI($oProfile->id(), false, BX_CONNECTIONS_CONTENT_TYPE_INITIATORS);
+        }
+        $aResult['visibility'] = $aData[$CNF['FIELD_ALLOW_VIEW_TO']];
+
+        return $aResult;
+    }
+
+    public function serviceGetMenuAddonManageTools()
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aResult = parent::serviceGetMenuAddonManageTools();
+
+        if(!empty($CNF['FIELD_STATUS_ADMIN']))
+            $aResult['counter1_value'] = $this->_oDb->getEntriesNumByParams([[
+                'key' => $CNF['FIELD_STATUS_ADMIN'],
+                'value' => BX_BASE_MOD_GENERAL_STATUS_PENDING, 
+                'operator' => '='
+            ]]);
+
+        return $aResult;
+    }
+
+    public function serviceGetOptionsMembersMode()
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        return array(
+            array('key' => '', 'value' => _t('_None')),
+            array('key' => BX_BASE_MOD_GROUPS_MMODE_MULTI_ROLES, 'value' => _t($CNF['T']['option_members_mode_' . BX_BASE_MOD_GROUPS_MMODE_MULTI_ROLES])),
+        );
+    }
+
+    public function serviceGetSearchResultUnit ($iContentId, $sUnitTemplate = '')
+    {
+        if(empty($sUnitTemplate))
+            $sUnitTemplate = 'unit.html';
+
+        return parent::serviceGetSearchResultUnit($iContentId, $sUnitTemplate);
+    }
+
+    /**
+     * @see BxBaseModProfileModule::serviceGetSpaceTitle
+     */ 
+    public function serviceGetSpaceTitle()
+    {
+        return _t($this->_oConfig->CNF['T']['txt_sample_single']);
+    }
+    
+    /**
+     * @see iBxDolProfileService::serviceGetParticipatingProfiles
+     */ 
+    public function serviceGetParticipatingProfiles($iProfileId, $aConnectionObjects = false)
+    {
+        if (isset($this->_oConfig->CNF['OBJECT_CONNECTIONS'])){
+            $aConnectionObjects = array($this->_oConfig->CNF['OBJECT_CONNECTIONS'], 'sys_profiles_subscriptions');
+            return parent::serviceGetParticipatingProfiles($iProfileId, $aConnectionObjects);
+        }
+        return parent::serviceGetParticipatingProfiles($iProfileId);
+    }
+
+    public function serviceGetSafeServices()
+    {
+        return array_merge(parent::serviceGetSafeServices(), [
+            'GetQuestionnaire' => '',
+        ]);
+    }
+
+    /**
+     * Check if this module entry can be used as profile
+     */
+    public function serviceActAsProfile ()
+    {
+        return false;
+    }
+
+    /**
+     * Check if this module is group profile
+     */
+    public function serviceIsGroupProfile ()
+    {
+        return true;
+    }
+
+    public function serviceIsEnableForContext($iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sCnfKey = 'ENABLE_FOR_CONTEXT_IN_MODULES';
+        if(empty($iProfileId) || empty($CNF[$sCnfKey]) || !is_array($CNF[$sCnfKey]))
+            return false;
+
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if(!$oProfile)
+            return false;
+
+        if(!in_array($oProfile->getModule(), $CNF[$sCnfKey]))
+            return false;
+
+        return true;
+    }
+
+    /**
+     * check if provided profile is member of the group 
+     */ 
+    public function serviceIsFan ($iGroupProfileId, $iProfileId = false) 
+    {
+        $oGroupProfile = BxDolProfile::getInstance($iGroupProfileId);
+        return $oGroupProfile !== false && $this->isFan($oGroupProfile->getContentId(), $iProfileId);
+    }
+
+    /**
+     * check if provided profile is admin of the group 
+     */ 
+    public function serviceIsAdmin ($iGroupProfileId, $iProfileId = false) 
+    {
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+
+        $oGroupProfile = BxDolProfile::getInstance($iGroupProfileId);
+        if(!$oGroupProfile)
+            return false;
+
+        $iGroupContentId = $oGroupProfile->getContentId();
+        if(!$this->isFan($iGroupContentId, $iProfileId))
+            return false;
+
+        $aGroupContentInfo = $this->_oDb->getContentInfoById($iGroupContentId);
+        return $this->_oDb->isAdmin($iGroupProfileId, $iProfileId, $aGroupContentInfo);
+    }
+
+    public function serviceGetAdminRole($iGroupProfileId, $iProfileId = false)
+    {
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+
+        if(!$this->serviceIsAdmin($iGroupProfileId, $iProfileId))
+            return 0;
+
+        return $this->_oDb->getRole($iGroupProfileId, $iProfileId);
+    }
+
+    /*
+     * Get context (group) advanced members who can perform the specified action.
+     * If 'Roles' are disabled for the context then all context admins are returned.
+     */
+    public function serviceGetAdminsByAction($iGroupProfileId, $mixedAction)
+    {
+        if(!$this->_oConfig->isAdmins())
+            return [];
+
+        $aGroupContentInfo = $this->_oDb->getContentInfoByProfileId($iGroupProfileId);
+        if(empty($aGroupContentInfo) || !is_array($aGroupContentInfo))
+            return [];
+
+        $aAdmins = $this->_oDb->getAdmins($iGroupProfileId);
+        if(!$this->_oConfig->isRoles())
+            return $aAdmins;
+
+        if(!is_array($mixedAction))
+            $mixedAction = [$mixedAction];
+
+        $aResult = [];
+        foreach($mixedAction as $sAction)
+            foreach($aAdmins as $iAdminProfileId)
+                if(!in_array($iAdminProfileId, $aResult) && $this->isAllowedActionByRole($sAction, $aGroupContentInfo, $iGroupProfileId, $iAdminProfileId))
+                    $aResult[] = $iAdminProfileId;
+
+        return $aResult;
+    }
+
+    public function serviceGetAdminsToManageContent($iGroupProfileId)
+    {
+        return $this->serviceGetAdminsByAction($iGroupProfileId, [
+            BX_BASE_MOD_GROUPS_ACTION_EDIT_CONTENT, 
+            BX_BASE_MOD_GROUPS_ACTION_DELETE_CONTENT
+        ]);
+    }
+
+    /**
+     * Delete profile from fans and admins tables
+     * @param $iProfileId profile id 
+     */
+    public function serviceDeleteProfileFromFansAndAdmins ($iProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $this->_oDb->deleteAdminsByProfileId($iProfileId);
+
+        if (isset($CNF['OBJECT_CONNECTIONS']) && ($oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])))
+            $oConnection->onDeleteInitiatorAndContent($iProfileId);
+    }
+
+    /**
+     * Reset group's author for particular group
+     * @param $iContentId group id 
+     * @parem $iAuthorId new author profile ID
+     * @return false of error, or number of updated records on success
+     */
+    public function serviceReassignEntityAuthor ($iContentId, $iAuthorId = 0)
+    {
+        $aContentInfo = $this->_oDb->getContentInfoById((int)$iContentId);
+        if (!$aContentInfo)
+            return false;
+
+        if (empty($iAuthorId)) {
+            $oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName());
+            if (!$oGroupProfile)
+                return false;
+
+            $aAdmins = $this->_oDb->getAdmins($oGroupProfile->id());
+            if($aAdmins)
+                $iAuthorId = array_pop($aAdmins);
+        }
+
+        return $this->_oDb->updateAuthorById($iContentId, $iAuthorId);
+    }
+
+    /**
+     * Entry actions and social sharing block
+     */
+    public function serviceEntityAllActions ($mixedContent = false, $aParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!empty($mixedContent)) {
+            if(!is_array($mixedContent))
+                $mixedContent = array((int)$mixedContent, (method_exists($this->_oDb, 'getContentInfoById')) ? $this->_oDb->getContentInfoById((int)$mixedContent) : array());
+        }
+        else {
+            $mixedContent = $this->_getContent();
+            if($mixedContent === false)
+                return false;
+        }
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        if(!empty($CNF['FIELD_PICTURE']) && !empty($aContentInfo[$CNF['FIELD_PICTURE']]))
+            $aParams = array_merge(array(
+                'entry_thumb' => (int)$aContentInfo[$CNF['FIELD_PICTURE']]
+            ), $aParams); 
+
+        return parent::serviceEntityAllActions ($mixedContent, $aParams);
+    }
+    
+    /**
+     * Reset group's author when author profile is deleted
+     * @param $iProfileId author profile id 
+     * @param $iAuthorId new author profile id 
+     * @return number of changed items
+     */
+    public function serviceReassignEntitiesByAuthor ($iProfileId, $iAuthorId = 0)
+    {
+        $a = $this->_oDb->getEntriesByAuthor((int)$iProfileId);
+        if (!$a)
+            return 0;
+
+        $iCount = 0;
+        foreach ($a as $aContentInfo)
+            $iCount += ('' == $this->serviceReassignEntityAuthor($aContentInfo[$this->_oConfig->CNF['FIELD_ID']], $iAuthorId) ? 1 : 0);
+
+        return $iCount;
+    }
+
+    public function servicePrepareFields ($aFieldsProfile)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+        $aFieldsProfile[$CNF['FIELD_NAME']] = $aFieldsProfile['name'];
+        $aFieldsProfile[$CNF['FIELD_TEXT']] = isset($aFieldsProfile['description']) ? $aFieldsProfile['description'] : '';
+        unset($aFieldsProfile['name']);
+        unset($aFieldsProfile['description']);
+        return $aFieldsProfile;
+    }
+
+    public function serviceOnRemoveConnection ($iGroupProfileId, $iInitiatorId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        list ($iProfileId, $iGroupProfileId, $oGroupProfile) = $this->_prepareProfileAndGroupProfile($iGroupProfileId, $iInitiatorId);
+        if (!$oGroupProfile)
+            return false;
+
+        $this->_oDb->fromAdmins($iGroupProfileId, $iProfileId);
+
+        if ($oConn = BxDolConnection::getObjectInstance('sys_profiles_subscriptions'))
+            return $oConn->removeConnection($iProfileId, $iGroupProfileId);
+
+        return false;
+    }
+
+    public function serviceAddMutualConnection ($iGroupProfileId, $iInitiatorId, $bSendInviteOnly = false)
+    {        
+        $CNF = &$this->_oConfig->CNF;
+
+        list ($iProfileId, $iGroupProfileId, $oGroupProfile) = $this->_prepareProfileAndGroupProfile($iGroupProfileId, $iInitiatorId);
+        if (!$oGroupProfile)
+            return false;
+
+        if (!($aContentInfo = $this->_oDb->getContentInfoById((int)BxDolProfile::getInstance($iGroupProfileId)->getContentId())))
+            return false;
+
+        if (!isset($CNF['OBJECT_CONNECTIONS']) || !($oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])))
+            return false;
+
+        $sEntryTitle = $aContentInfo[$CNF['FIELD_NAME']];
+        $sEntryUrl = bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]));
+
+        // send invitation to the group 
+        $sModule = $this->getName();
+        if ($bSendInviteOnly && !$oConnection->isConnected((int)$iInitiatorId, $oGroupProfile->id()) && !$oConnection->isConnected($oGroupProfile->id(), (int)$iInitiatorId) && bx_get_logged_profile_id() != $iProfileId) {
+            bx_alert($sModule, 'join_invitation', $aContentInfo[$CNF['FIELD_ID']], $iGroupProfileId, array(
+                'content' => $aContentInfo, 
+                'entry_title' => $sEntryTitle, 
+                'entry_url' => $sEntryUrl, 
+                'group_profile' => $iGroupProfileId, 
+                'profile' => $iProfileId, 
+                'notification_subobject_id' => $iProfileId, 
+                'object_author_id' => $iGroupProfileId
+            ));
+
+            /**
+             * 'Invitation Received' alert for Notifications module.
+             * Note. It's essential to use Recipient ($iInitiatorId) in 'object_author_id' parameter. 
+             * In this case notification will be received by Recipient profile.
+             */
+            bx_alert($sModule, 'join_invitation_notif', $aContentInfo[$CNF['FIELD_ID']], $iGroupProfileId, array(
+                'object_author_id' => $iInitiatorId, 
+                'privacy_view' => isset($aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]) ? $aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']] : 3, 
+            ));
+        }
+        // send notification to group's admins that new connection is pending confirmation 
+        elseif (!$bSendInviteOnly && $oConnection->isConnected((int)$iInitiatorId, $oGroupProfile->id()) && !$oConnection->isConnected($oGroupProfile->id(), (int)$iInitiatorId) && $aContentInfo['join_confirmation']) {
+
+            bx_alert($this->getName(), 'join_request', $aContentInfo[$CNF['FIELD_ID']], $iGroupProfileId, array(
+            	'object_author_id' => $iGroupProfileId,
+            	'performer_id' => $iProfileId, 
+
+            	'content' => $aContentInfo, 
+            	'entry_title' => $sEntryTitle, 
+            	'entry_url' => $sEntryUrl, 
+
+            	'group_profile' => $iGroupProfileId, 
+            	'profile' => $iProfileId
+            ));
+        }
+        // send notification that join request was accepted 
+        else if (!$bSendInviteOnly && $oConnection->isConnected((int)$iInitiatorId, $oGroupProfile->id(), true) && $oGroupProfile->getModule() != $this->getName() && bx_get_logged_profile_id() != $iProfileId) {
+            bx_alert($this->getName(), 'join_request_accepted', $aContentInfo[$CNF['FIELD_ID']], $iGroupProfileId, array(
+            	'object_author_id' => $iGroupProfileId,
+            	'performer_id' => $iProfileId,
+
+            	'content' => $aContentInfo, 
+            	'entry_title' => $sEntryTitle, 
+            	'entry_url' => $sEntryUrl, 
+
+            	'group_profile' => $iGroupProfileId, 
+            	'profile' => $iProfileId
+            ));
+        }
+
+        // new fan was added
+        if ($oConnection->isConnected($oGroupProfile->id(), (int)$iInitiatorId, true)) {
+            // follow group on join
+            if (BxDolService::call($oGroupProfile->getModule(), 'act_as_profile')){
+                 $this->addFollower($oGroupProfile->id(), (int)$iInitiatorId);
+            }
+            else{
+                 $this->addFollower((int)$iInitiatorId, $oGroupProfile->id()); 
+            }
+            
+            bx_alert($this->getName(), 'fan_added', $aContentInfo[$CNF['FIELD_ID']], $iGroupProfileId, array(
+            	'object_author_id' => $iGroupProfileId,
+            	'performer_id' => $iProfileId,
+
+            	'content' => $aContentInfo,
+            	'entry_title' => $sEntryTitle, 
+            	'entry_url' => $sEntryUrl,
+
+            	'group_profile' => $iGroupProfileId, 
+            	'profile' => $iProfileId,
+            ));
+            
+            $this->doAudit($iGroupProfileId, $iInitiatorId, '_sys_audit_action_group_join_request_accepted');
+            
+            return false;
+        }
+
+        // don't automatically add connection (mutual) if group requires manual join confirmation
+        if ($bSendInviteOnly || $aContentInfo['join_confirmation'])
+            return false;
+
+        // check if connection already exists
+        if ($oConnection->isConnected($oGroupProfile->id(), (int)$iInitiatorId, true) || $oConnection->isConnected($oGroupProfile->id(), (int)$iInitiatorId))
+            return false;
+
+        if (!$oConnection->addConnection($oGroupProfile->id(), (int)$iInitiatorId))
+            return false;
+
+        return true;
+    }
+
+    public function serviceFansTable ()
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $oGrid = BxDolGrid::getObjectInstance($CNF['OBJECT_GRID_CONNECTIONS']);
+        if(!$oGrid)
+            return false;
+
+        if($this->_bIsApi){
+            return [
+                bx_api_get_block('grid', $oGrid->getCodeAPI())
+            ];
+        }
+
+        return $oGrid->getCode();
+    }
+    
+    public function serviceInvitesTable ()
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!isset($CNF['OBJECT_GRID_INVITES']))
+            return false;
+
+        $oGrid = BxDolGrid::getObjectInstance($CNF['OBJECT_GRID_INVITES']);
+        if(!$oGrid)
+            return false;
+
+        if($this->_bIsApi)
+            return [
+                bx_api_get_block('grid', $oGrid->getCodeAPI())
+            ];
+
+        return $oGrid->getCode();
+    }
+
+    public function serviceBansTable ()
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!isset($CNF['OBJECT_GRID_BANS']))
+            return false;
+
+        $oGrid = BxDolGrid::getObjectInstance($CNF['OBJECT_GRID_BANS']);
+        if(!$oGrid)
+            return false;
+
+        if($this->_bIsApi)
+            return [
+                bx_api_get_block('grid', $oGrid->getCodeAPI())
+            ];
+
+        return $oGrid->getCode();
+    }
+
+    public function serviceFans ($iContentId = 0, $bAsArray = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+
+        if (!$iContentId)
+            return false;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if (!$aContentInfo)
+            return false;
+
+        if (!($oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName())))
+            return false;
+
+        if(!$bAsArray) {
+            bx_import('BxDolConnection');
+            $mixedResult = $this->serviceBrowseConnectionsQuick ($oGroupProfile->id(), $CNF['OBJECT_CONNECTIONS'], BX_CONNECTIONS_CONTENT_TYPE_CONTENT, true);
+            if (!$mixedResult)
+                return MsgBox(_t('_sys_txt_empty'));
+        }
+        else
+            $mixedResult = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])->getConnectedContent($oGroupProfile->id(), true);
+
+        return $mixedResult;
+    }
+    
+    public function serviceFansWithoutAdmins ($iContentId = 0, $bAsArray = false)
+    {
+        if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iContentId)
+            return false;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if (!$aContentInfo)
+            return false;
+
+        if (!($oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName())))
+            return false;
+        
+        $CNF = &$this->_oConfig->CNF;
+
+        $aFans = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])->getConnectedContent($oGroupProfile->id(), true);
+        if(empty($aFans) || !is_array($aFans))
+            return false;
+
+        $aAdmins = $this->_oDb->getAdmins($oGroupProfile->id());
+        if(!empty($aAdmins) && is_array($aAdmins))
+            $aFans = array_diff($aFans, $aAdmins);
+
+        $iStart = (int)bx_get('start');
+        $iLimit = !empty($CNF['PARAM_NUM_CONNECTIONS_QUICK']) ? getParam($CNF['PARAM_NUM_CONNECTIONS_QUICK']) : 4;
+        if(!$iLimit)
+            $iLimit = 4;
+
+        return $this->_serviceBrowseQuick($aFans, $iStart, $iLimit);
+    }
+
+    public function serviceAdmins ($iContentId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if(!$iContentId)
+            return false;
+
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName());
+        if(!$oGroupProfile)
+            return false;
+
+        $iStart = (int)bx_get('start');
+        $iLimit = !empty($CNF['PARAM_NUM_CONNECTIONS_QUICK']) ? getParam($CNF['PARAM_NUM_CONNECTIONS_QUICK']) : 4;
+        if(!$iLimit)
+            $iLimit = 4;
+        
+        $aProfiles = $this->_oDb->getAdmins($oGroupProfile->id(), $iStart,  $iLimit+1);
+        if(empty($aProfiles) || !is_array($aProfiles))
+            return false;
+
+        return $this->_serviceBrowseQuick($aProfiles, $iStart, $iLimit);
+    }
+
+    public function serviceMembersByRole ($iContentId = 0, $iRole = BX_BASE_MOD_GROUPS_ROLE_COMMON)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if(!$iContentId)
+            return false;
+
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName());
+        if(!$oGroupProfile)
+            return false;
+
+        $iStart = (int)bx_get('start');
+        $iLimit = !empty($CNF['PARAM_NUM_CONNECTIONS_QUICK']) ? getParam($CNF['PARAM_NUM_CONNECTIONS_QUICK']) : 4;
+        if(!$iLimit)
+            $iLimit = 4;
+
+        $aProfiles = $this->_oDb->getRoles([
+            'type' => 'fan_pids_by_group_pid', 
+            'group_profile_id' => $oGroupProfile->id(), 
+            'role' => $iRole,
+            'start' => $iStart,  
+            'limit' => $iLimit + 1
+        ]);
+
+        if(empty($aProfiles) || !is_array($aProfiles))
+            return false;
+
+        return $this->_serviceBrowseQuick($aProfiles, $iStart, $iLimit);
+    }
+
+    public function serviceBrowseJoinedEntries ($iProfileId = 0, $bDisplayEmptyMsg = false)
+    {
+        if (!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if (!$iProfileId)
+            return '';
+
+        return $this->_serviceBrowse ('joined_entries', array('joined_profile' => $iProfileId), BX_DB_PADDING_DEF, $bDisplayEmptyMsg);
+    }
+    
+    public function serviceBrowseFollowedEntries ($iProfileId = 0, $bDisplayEmptyMsg = false)
+    {
+        if (!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if (!$iProfileId)
+            return '';
+
+        return $this->_serviceBrowse ('followed_entries', array('followed_profile' => $iProfileId), BX_DB_PADDING_DEF, $bDisplayEmptyMsg);
+    }
+
+    public function serviceBrowseCreatedEntries ($iProfileId = 0, $bDisplayEmptyMsg = false)
+    {
+        if (!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if (!$iProfileId)
+            return '';
+
+        return $this->_serviceBrowse ('created_entries', array('author' => $iProfileId), BX_DB_PADDING_DEF, $bDisplayEmptyMsg);
+    }
+
+    public function serviceBrowseRecommendationsFans ($iProfileId = 0, $aParams = [])
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if($this->_bIsApi)
+            $aParams = bx_api_get_browse_params($aParams, true);
+
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return '';
+
+        $aParams = array_merge([
+            'empty_message' => false,
+            'start' => 0,
+            'per_page' => 0
+        ], $aParams);
+
+        if(($iStartGet = bx_get('start')) !== false)
+            $aParams['start'] = (int)$iStartGet;
+
+        if(($iPerPageGet = bx_get('per_page')) !== false)
+            $aParams['per_page'] = (int)$iPerPageGet;
+
+        $oRecommendation = BxDolRecommendation::getObjectInstance($CNF['OBJECT_RECOMMENDATIONS_FANS']);
+        if(!$oRecommendation)
+            return false;
+
+        if($this->_bIsApi) {
+            $aData = $oRecommendation->getCodeAPI($iProfileId, $aParams);
+            $aData = array_merge($aData, [
+                'module' => 'system',
+                'unit' => 'mixed', 
+                'request_url' => '/api.php?r=bx_groups/browse_recommendations_fans&params[]=' . $iProfileId . '&params[]='
+            ]);
+
+            return [bx_api_get_block('browse', $aData)];
+        }
+
+        $sCode = $oRecommendation->getCode($iProfileId, $aParams);
+        if(!$sCode && $aParams['empty_message'])
+            $sCode = MsgBox(_t(!empty($aParams['empty_message_text']) ? $aParams['empty_message_text'] : '_Empty'));
+
+        return $sCode;
+    }
+
+    public function serviceBrowseMembers ($iProfileId = 0, $aParams = [])
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if(!$iProfileId)
+            return '';
+
+        $oGroupProfile = BxDolProfile::getInstance($iProfileId);
+        if(!($oGroupProfile))
+            return '';
+
+        return bx_srv('system', 'browse_members', [$oGroupProfile->id(), $CNF['OBJECT_CONNECTIONS'], $aParams], 'TemplServiceProfiles');
+    }
+    
+    public function serviceEntityPricing($iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if(!$iProfileId)
+            return '';
+
+        if(!$this->_oConfig->isPaidJoin())
+            return '';
+
+        $oPayments = BxDolPayments::getInstance();
+    	if(!$oPayments->isActive())
+            return MsgBox(_t('_sys_payments_err_no_payments'));
+
+        if($this->checkAllowedUsePaidJoin() !== CHECK_ACTION_RESULT_ALLOWED)
+            return MsgBox(_t('_Access denied'));
+
+        if($this->checkAllowedManageAdmins($iProfileId) !== CHECK_ACTION_RESULT_ALLOWED)
+            return MsgBox(_t('_Access denied'));
+
+        $oGrid = BxDolGrid::getObjectInstance($CNF['OBJECT_GRID_PRICES_MANAGE']);
+        if(!$oGrid)
+            return '';
+
+        $sNote = '';
+        if(!$oPayments->isAcceptingPayments($this->_iProfileId))
+            $sNote = MsgBox(_t('_sys_payments_err_not_accept_payments', $oPayments->getDetailsUrl()));
+
+        return $sNote . $oGrid->getCode();
+    }
+
+    public function serviceEntityJoin($iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if(!$iProfileId)
+            return '';
+
+        if(!$this->_oConfig->isPaidJoin())
+            return '';
+
+        $oGrid = BxDolGrid::getObjectInstance($CNF['OBJECT_GRID_PRICES_VIEW']);
+        if(!$oGrid)
+            return '';
+
+        return $oGrid->getCode();
+    }
+    
+    public function serviceEntityInvite ($iContentId = 0, $bErrorMsg = true)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!isset($CNF['OBJECT_FORM_ENTRY_DISPLAY_INVITE']))
+            return false;
+
+        return $this->_serviceEntityForm ('inviteForm', $iContentId, false, false, $bErrorMsg);
+    }
+    
+    /**
+     * Entry social sharing block
+     */
+    public function serviceEntitySocialSharing ($mixedContent = false, $aParams = array())
+    {
+        if(!empty($mixedContent)) {
+            if(!is_array($mixedContent))
+               $mixedContent = array((int)$mixedContent, array());
+        }
+        else {
+            $mixedContent = $this->_getContent();
+            if($mixedContent === false)
+                return false;
+        }
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType((int)$iContentId, $this->getName());
+        if(!$oGroupProfile)
+            return false;
+
+        return parent::serviceEntitySocialSharing(array($iContentId, $aContentInfo), array(
+            'title' => $oGroupProfile->getDisplayName()
+        ));
+    }
+
+    public function serviceIsPricingAvaliable($iProfileId)
+    {
+        if(!$this->_oConfig->isPaidJoin())
+            return false;
+
+        if($this->checkAllowedUsePaidJoin() !== CHECK_ACTION_RESULT_ALLOWED)
+            return false;        
+
+        if($this->checkAllowedManageAdmins($iProfileId) !== CHECK_ACTION_RESULT_ALLOWED)
+            return false;
+
+        return true;
+    }
+
+    public function serviceIsPaidJoinAvaliable($iGroupProfileId, $iProfileId = 0)
+    {
+        return $this->isPaidJoinByProfileForProfile($iGroupProfileId, $iProfileId);
+    }
+
+    public function serviceIsPaidJoinAvaliableByContent($iGroupContentId, $iProfileId = 0)
+    {
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($iGroupContentId, $this->getName());
+        if(!$oGroupProfile)
+            return false;
+
+        return $this->isPaidJoinByProfileForProfile($oGroupProfile->id(), $iProfileId);
+    }
+
+    public function serviceIsFreeJoinAvaliable($iGroupProfileId, $iProfileId = 0)
+    {
+        return !$this->isPaidJoinByProfileForProfile($iGroupProfileId, $iProfileId);
+    }
+
+    public function serviceIsFreeJoinAvaliableByContent($iGroupContentId, $iProfileId = 0)
+    {
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($iGroupContentId, $this->getName());
+        if(!$oGroupProfile)
+            return false;
+
+        return !$this->isPaidJoinByProfileForProfile($oGroupProfile->id(), $iProfileId);
+    }
+
+    /**
+     * Is Paid Join enabled in the group and whether a profile can use it.
+     * 
+     * @param type $iGroupProfileId - Group profile ID.
+     * @param type $iProfileId - Profile ID of the user who wants to join.
+     * @return boolean
+     */
+    public function isPaidJoinByProfileForProfile($iGroupProfileId, $iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($iProfileId))
+            $iProfileId = $this->_iProfileId;
+
+        if(BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])->isConnected($iProfileId, $iGroupProfileId))
+            return false;
+
+        return $this->isPaidJoinByProfile($iGroupProfileId);
+    }
+
+    /**
+     * Is Paid Join enabled as is and whether a group has pricing plans added.
+     * 
+     * @param type $iGroupProfileId - Group profile ID.
+     * @return boolean
+     */
+    public function isPaidJoinByProfile($iGroupProfileId)
+    {
+        if(!$this->_oConfig->isPaidJoin())
+            return false;
+
+        $aPrices = $this->_oDb->getPrices(array('type' => 'by_profile_id', 'profile_id' => $iGroupProfileId));
+        if(empty($aPrices) || !is_array($aPrices))
+            return false;
+
+        return true;
+    }
+
+    /**
+     * Integration with Payments.
+     */
+    public function serviceGetPaymentData()
+    {
+        return $this->_aModule;
+    }
+
+    public function serviceGetCartItem($mixedItemId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$mixedItemId)
+            return array();
+
+        if(is_numeric($mixedItemId))
+            $aItem = $this->_oDb->getPrices(array('type' => 'by_id', 'value' => (int)$mixedItemId));
+        else 
+            $aItem = $this->_oDb->getPrices(array('type' => 'by_name', 'value' => $mixedItemId));
+
+        if(empty($aItem) || !is_array($aItem))
+            return array();
+
+        if(!$this->isPaidJoinByProfile($aItem['profile_id']))
+            return array();
+
+        $oGroupProfile = BxDolProfile::getInstance($aItem['profile_id']);
+        if(!$oGroupProfile)
+            return array();
+
+        $aGroupProfile = $this->_oDb->getContentInfoById($oGroupProfile->getContentId());
+        
+        $aRoles = BxDolFormQuery::getDataItems($CNF['OBJECT_PRE_LIST_ROLES']);
+
+        $sTitle = '';
+        if(!empty($aItem['period']) && !empty($aItem['period_unit']))
+            $sTitle = _t($CNF['T']['txt_cart_item_title'], $oGroupProfile->getDisplayName(), $aRoles[$aItem['role_id']], $aItem['period'], $aItem['period_unit']);
+        else
+            $sTitle = _t($CNF['T']['txt_cart_item_title_lifetime'], $oGroupProfile->getDisplayName(), $aRoles[$aItem['role_id']]);
+
+        return array (
+            'id' => $aItem['id'],
+            'author_id' => $aGroupProfile[$CNF['FIELD_AUTHOR']],
+            'name' => $aItem['name'],
+            'title' => $sTitle,
+            'description' => '',
+            'url' => $oGroupProfile->getUrl(),
+            'price_single' => $aItem['price'],
+            'price_recurring' => $aItem['price'],
+            'period_recurring' => $aItem['period'],
+            'period_unit_recurring' => $aItem['period_unit'],
+            'trial_recurring' => 0
+        );
+    }
+
+    public function serviceGetCartItems($iSellerId)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	if(empty($iSellerId))
+    	    return array();
+
+        $sModule = $this->getName();
+        $aRoles = BxDolFormQuery::getDataItems($CNF['OBJECT_PRE_LIST_ROLES']);
+
+        $aGroups = $this->_oDb->getEntriesBy(array('type' => 'author', 'author' => $iSellerId));
+
+        $aResult = array();
+        foreach($aGroups as $aGroup) {
+            $oGroupProfile = BxDolProfile::getInstanceByContentAndType($aGroup[$CNF['FIELD_ID']], $sModule);
+            if(!$oGroupProfile)
+                continue;
+
+            $aPrices = $this->_oDb->getPrices(array('type' => 'by_profile_id', 'profile_id' => $oGroupProfile->id()));
+            if(empty($aPrices) || !is_array($aPrices))
+                continue;
+
+            $sTitle = $oGroupProfile->getDisplayName();
+            $sUrl = $oGroupProfile->getUrl();
+
+            foreach($aPrices as $aPrice)
+                $aResult[] = array(
+                    'id' => $aPrice['id'],
+                    'author_id' => $iSellerId,
+                    'name' => $aPrice['name'],
+                    'title' => _t($CNF['T']['txt_cart_item_title'], $sTitle, $aRoles[$aPrice['role_id']], $aPrice['period'], $aPrice['period_unit']),
+                    'description' => '',
+                    'url' => $sUrl,
+                    'price_single' => $aPrice['price'],
+                    'price_recurring' => $aPrice['price'],
+                    'period_recurring' => $aPrice['period'],
+                    'period_unit_recurring' => $aPrice['period_unit']
+               );
+        }
+
+        return $aResult;
+    }
+
+    public function serviceRegisterCartItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder, $sLicense)
+    {
+        return $this->_serviceRegisterItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder);
+    }
+
+    public function serviceRegisterSubscriptionItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder, $sLicense)
+    {
+        return $this->_serviceRegisterItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder);
+    }
+
+    public function serviceReregisterCartItem($iClientId, $iSellerId, $iItemIdOld, $iItemIdNew, $sOrder)
+    {
+        return $this->_serviceReregisterItem($iClientId, $iSellerId, $iItemIdOld, $iItemIdNew, $sOrder);
+    }
+
+    public function serviceReregisterSubscriptionItem($iClientId, $iSellerId, $iItemIdOld, $iItemIdNew, $sOrder)
+    {
+        return $this->_serviceReregisterItem($iClientId, $iSellerId, $iItemIdOld, $iItemIdNew, $sOrder);
+    }
+
+    public function serviceUnregisterCartItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder, $sLicense)
+    {
+        return $this->_serviceUnregisterItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder);
+    }
+
+    public function serviceUnregisterSubscriptionItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder, $sLicense)
+    {
+    	return $this->_serviceUnregisterItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder); 
+    }
+
+    public function serviceCancelSubscriptionItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder)
+    {
+    	return true;
+    }
+
+    protected function _serviceRegisterItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+    	$aItem = $this->serviceGetCartItem($iItemId);
+        if(empty($aItem) || !is_array($aItem))
+            return array();
+
+        $aItemInfo = $this->_oDb->getPrices(array('type' => 'by_id', 'value' => $iItemId));
+
+        $mixedPeriod = false;
+        if((int)$aItemInfo['period'] != 0)
+            $mixedPeriod = array(
+                'period' => (int)$aItemInfo['period'], 
+                'period_unit' => $aItemInfo['period_unit'], 
+                'period_reserve' => $CNF['PARAM_RECURRING_RESERVE']
+            );
+
+        if(!$this->setRole($aItemInfo['profile_id'], $iClientId, $aItemInfo['role_id'], $mixedPeriod, $sOrder))
+            return array();
+
+        return $aItem;
+    }
+
+    protected function _serviceReregisterItem($iClientId, $iSellerId, $iItemIdOld, $iItemIdNew, $sOrder)
+    {
+        $aItem = $this->serviceGetCartItem($iItemIdNew);
+        if(empty($aItem) || !is_array($aItem))
+            return array();
+
+        $aItemInfoOld = $this->_oDb->getPrices(array('type' => 'by_id', 'value' => $iItemIdOld));
+        if(empty($aItemInfoOld) || !is_array($aItemInfoOld))
+            return array();
+
+        $aItemInfoNew = $this->_oDb->getPrices(array('type' => 'by_id', 'value' => $iItemIdNew));
+        if(empty($aItemInfoNew) || !is_array($aItemInfoNew))
+            return array();
+
+        if(!$this->unsetRole($aItemInfoOld['profile_id'], $iClientId))
+            return array();
+        
+        $aResult = $this->_serviceRegisterItem($iClientId, $iSellerId, $iItemIdNew, 1, $sOrder);
+        if(empty($aResult) || !is_array($aResult))
+            return array();
+
+    	return $aItem;
+    }
+
+    protected function _serviceUnregisterItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder)
+    {
+        $aItemInfo = $this->_oDb->getPrices(array('type' => 'by_id', 'value' => $iItemId));
+        if(empty($aItemInfo) || !is_array($aItemInfo))
+            return false;
+
+        return $this->unsetRole($aItemInfo['profile_id'], $iClientId);
+    }
+
+    public function serviceGetQuestionnaire($sSource, $sObject, $sAction, $iContentProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $sMsg = _t('_sys_txt_not_found');
+
+        $aContentInfo = $this->_oDb->getContentInfoByProfileId($iContentProfileId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return $this->_bIsApi ? [bx_api_get_msg($sMsg)] : ['code' => 1, 'msg' => $sMsg];
+        
+        $oForm = null;
+        if(($oConnection = BxDolConnection::getObjectInstance($sObject)) !== false) {
+            $oForm = $oConnection->getQuestionnaireForm($sAction, $iContentProfileId, ['request' => [
+                's' => $sSource,
+            ]]);
+
+            if($oForm === false)
+                return $this->_bIsApi ? [bx_api_get_msg($sMsg)] : ['code' => 3, 'msg' => $sMsg];
+        }
+        else
+            return $this->_bIsApi ? [bx_api_get_msg($sMsg)] : ['code' => 2, 'msg' => $sMsg];
+
+        $oForm->initChecker();
+        if($oForm->isSubmittedAndValid()) {
+            $iProfileId = bx_get_logged_profile_id();
+
+            $aQuestions = $this->_oDb->getQuestions(['sample' => 'content_pid', 'content_pid' => $iContentProfileId]);
+            foreach($aQuestions as $aQuestion)
+                $this->_oDb->insertAnswer((int)$aQuestion['id'], $iProfileId, $oForm->getCleanValue('question_' . $aQuestion['id']));
+
+            return $this->_bIsApi ? [] : ['code' => 0, 'o' => $sObject, 'a' => $sAction, 'cpi' => $iContentProfileId, 'ci' => $aContentInfo[$CNF['FIELD_ID']], 'eval' => $this->_oConfig->getJsObject($sSource) . '.connActionPerformed(oData)'];
+        }
+
+        if($this->_bIsApi)
+            return [bx_api_get_block('form', $oForm->getCodeAPI(), ['ext' => [
+                'name' => $this->_oModule->getName(), 
+                'request' => ['url' => $oForm->aFormAttrs['action'], 'immutable' => true]]
+            ])];
+
+        bx_import('BxTemplFunctions');
+        $sContent = BxTemplFunctions::getInstance()->popupBox($this->_oConfig->getHtmlIds('popup_questionnaire'), _t($CNF['T']['popup_title_questionnaire']), $this->_oTemplate->parseHtmlByName('popup_qnr_questionnaire.html', [
+            'form_id' => $oForm->getId(),
+            'form' => $oForm->getCode(true),
+        ]));
+
+        return ['popup' => ['html' => $sContent, 'options' => ['closeOnOuterClick' => false]]];
+    }
+
+    /**
+     * Data for Notifications module
+     */
+    public function serviceGetNotificationsData()
+    {
+    	$sModule = $this->_aModule['name'];
+
+        $aSettingsTypes = ['follow_member', 'follow_context'];
+        if($this->serviceActAsProfile())
+            $aSettingsTypes = ['personal', 'follow_member'];
+
+        return [
+            'handlers' => [
+                ['group' => $sModule . '_vote', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'doVote', 'module_name' => $sModule, 'module_method' => 'get_notifications_vote', 'module_class' => 'Module'],
+                ['group' => $sModule . '_vote', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'undoVote'],
+                
+                ['group' => $sModule . '_score_up', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'doVoteUp', 'module_name' => $sModule, 'module_method' => 'get_notifications_score_up', 'module_class' => 'Module'],
+
+                ['group' => $sModule . '_score_down', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'doVoteDown', 'module_name' => $sModule, 'module_method' => 'get_notifications_score_down', 'module_class' => 'Module'],
+
+                ['group' => $sModule . '_fan_added', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'fan_added', 'module_name' => $sModule, 'module_method' => 'get_notifications_fan_added', 'module_class' => 'Module'],
+
+                ['group' => $sModule . '_join_invitation', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'join_invitation_notif', 'module_name' => $sModule, 'module_method' => 'get_notifications_join_invitation', 'module_class' => 'Module'],
+                
+                ['group' => $sModule . '_join_request', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'join_request', 'module_name' => $sModule, 'module_method' => 'get_notifications_join_request', 'module_class' => 'Module', 'module_event_privacy' => $this->_oConfig->CNF['OBJECT_PRIVACY_VIEW_NOTIFICATION_EVENT']],
+                
+                ['group' => $sModule . '_timeline_post_common', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'timeline_post_common', 'module_name' => $sModule, 'module_method' => 'get_notifications_timeline_post_common', 'module_class' => 'Module'],
+                
+                //--- Moderation related: For 'admins'.
+                ['group' => $sModule . '_object_pending_approval', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'pending_approval', 'module_name' => $sModule, 'module_method' => 'get_notifications_post_pending_approval', 'module_class' => 'Module'],
+            ],
+            'settings' => [
+                ['group' => 'vote', 'unit' => $sModule, 'action' => 'doVote', 'types' => $aSettingsTypes],
+
+                ['group' => 'score_up', 'unit' => $sModule, 'action' => 'doVoteUp', 'types' => $aSettingsTypes],
+
+                ['group' => 'score_down', 'unit' => $sModule, 'action' => 'doVoteDown', 'types' => $aSettingsTypes],
+                
+                ['group' => 'fan', 'unit' => $sModule, 'action' => 'fan_added', 'types' => $aSettingsTypes],
+
+                ['group' => 'invite', 'unit' => $sModule, 'action' => 'join_invitation_notif', 'types' => ['personal']],
+
+                ['group' => 'join', 'unit' => $sModule, 'action' => 'join_request', 'types' => $aSettingsTypes],
+
+                ['group' => 'timeline_post', 'unit' => $sModule, 'action' => 'timeline_post_common', 'types' => $aSettingsTypes],
+
+                //--- Moderation related: For 'admins'.
+                ['group' => 'action_required', 'unit' => $sModule, 'action' => 'pending_approval', 'types' => ['personal']],
+            ],
+            'alerts' => [
+                ['unit' => $sModule, 'action' => 'doVote'],
+                ['unit' => $sModule, 'action' => 'undoVote'],
+
+                ['unit' => $sModule, 'action' => 'doVoteUp'],
+                ['unit' => $sModule, 'action' => 'doVoteDown'],
+
+                ['unit' => $sModule, 'action' => 'fan_added'],
+
+                ['unit' => $sModule, 'action' => 'join_invitation_notif'],
+
+                ['unit' => $sModule, 'action' => 'join_request'],
+
+                ['unit' => $sModule, 'action' => 'timeline_post_common'],
+                
+                //--- Moderation related: For 'admins'.
+                ['unit' => $sModule, 'action' => 'pending_approval'],
+            ]
+        ];
+    }
+
+    public function serviceGetNotificationsInsertData($oAlert, $aHandler, $aDataItems)
+    {
+        if($oAlert->sAction != 'join_invitation_notif' || empty($aDataItems) || !is_array($aDataItems))
+            return $aDataItems;
+
+        foreach($aDataItems as $iIndex => $aDataItem)
+            $aDataItems[$iIndex]['object_privacy_view'] = BX_DOL_PG_ALL;
+
+        return $aDataItems;
+    }
+
+    /**
+     * Notification about new invitation to join the group
+     */
+    public function serviceGetNotificationsJoinInvitation($aEvent)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $iContentId = (int)$aEvent['object_id'];
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType((int)$iContentId, $this->getName());
+        if(!$oGroupProfile)
+            return array();
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return array();
+
+        /*
+         * It's essential that 'object_owner_id' contains invited member profile id.
+         */
+        $oProfile = BxDolProfile::getInstance((int)$aEvent['object_owner_id']);
+        if(!$oProfile)
+            return array();
+
+        /*
+         * Note. Group Profile URL is used for both Entry and Subentry URLs, 
+         * because Subentry URL has higher display priority and notification
+         * should be linked to Group Profile (Group Profile -> Members tab) 
+         * instead of Personal Profile of invited member.
+         */
+        $sEntryUrl = bx_absolute_url(str_replace(BX_DOL_URL_ROOT, '', $oGroupProfile->getUrl()), '{bx_url_root}');
+
+        return array(
+            'entry_sample' => $CNF['T']['txt_sample_single'],
+            'entry_url' => $sEntryUrl,
+            'entry_caption' => $oGroupProfile->getDisplayName(),
+            'subentry_sample' => $oProfile->getDisplayName(),
+            'subentry_url' => $sEntryUrl,
+            'lang_key' => $this->_oConfig->CNF['T']['txt_ntfs_join_invitation']
+        );
+    }
+
+    /**
+     * Notification about new member requst in the group
+     */
+    public function serviceGetNotificationsJoinRequest($aEvent)
+    {
+        return $this->_serviceGetNotification($aEvent, $this->_oConfig->CNF['T']['txt_ntfs_join_request']);
+    }
+
+	/**
+     * Notification about new member in the group
+     */
+    public function serviceGetNotificationsFanAdded($aEvent)
+    {
+        return $this->_serviceGetNotification($aEvent, $this->_oConfig->CNF['T']['txt_ntfs_fan_added']);
+    }
+
+    protected function _serviceGetNotification($aEvent, $sLangKey)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+        $iContentId = (int)$aEvent['object_id'];
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType((int)$iContentId, $this->getName());
+        if(!$oGroupProfile)
+            return array();
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return array();
+
+        $oProfile = BxDolProfile::getInstance((int)$aEvent['subobject_id']);
+        if(!$oProfile)
+            return array();
+
+        /*
+         * Note. Group Profile URL is used for both Entry and Subentry URLs, 
+         * because Subentry URL has higher display priority and notification
+         * should be linked to Group Profile (Group Profile -> Members tab) 
+         * instead of Personal Profile of a member, who performed an action.
+         */
+        if(empty($CNF['URL_ENTRY_FANS']))
+            $sEntryUrl = bx_absolute_url(str_replace(BX_DOL_URL_ROOT, '', $oGroupProfile->getUrl()), '{bx_url_root}');
+        else
+            $sEntryUrl = bx_absolute_url(BxDolPermalinks::getInstance()->permalink($CNF['URL_ENTRY_FANS'], [
+                'profile_id' => $oGroupProfile->id()
+            ]), '{bx_url_root}');
+
+        return [
+            'entry_sample' => $CNF['T']['txt_sample_single'],
+            'entry_url' => $sEntryUrl,
+            'entry_caption' => $oGroupProfile->getDisplayName(),
+            'entry_author' => $oGroupProfile->id(),
+            'subentry_sample' => $oProfile->getDisplayName(),
+            'subentry_url' => $sEntryUrl,
+            'lang_key' => $sLangKey
+        ];
+    }
+
+    /**
+     * Data for Timeline module
+     */
+    public function serviceGetTimelineData()
+    {
+        $aResult = BxBaseModGeneralModule::serviceGetTimelineData();
+
+        $sModule = $this->_aModule['name'];
+        $aResult['handlers'] = array_merge($aResult['handlers'], [
+            ['group' => $sModule . '_object', 'type' => 'update', 'alert_unit' => $sModule, 'alert_action' => 'context_cover_changed'],
+            ['group' => $sModule . '_object', 'type' => 'update', 'alert_unit' => $sModule, 'alert_action' => 'context_cover_deleted']
+        ]);
+        $aResult['alerts'] = array_merge($aResult['alerts'], [
+            ['unit' => $sModule, 'action' => 'context_cover_changed'],
+            ['unit' => $sModule, 'action' => 'context_cover_deleted']
+        ]);
+
+        return $aResult;
+    }
+
+    /**
+     * Entry post for Timeline module
+     */
+    public function serviceGetTimelinePost($aEvent, $aBrowseParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $a = parent::serviceGetTimelinePost($aEvent, $aBrowseParams);
+        if($a === false)
+            return false;
+
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($aEvent['object_id'], $this->getName());
+        $a['content']['url'] = $oGroupProfile->getUrl();
+        $a['content']['title'] = $oGroupProfile->getDisplayName();
+        
+        $oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS']);
+        if($oConnection)
+            $a['content']['members'] = $oConnection->getConnectedInitiatorsCount($oGroupProfile->id(), true);
+        
+        $a['content']['visibility'] = $aEvent['object_privacy_view'];
+
+        if(isset($CNF['FIELD_PUBLISHED'])) {
+            $aContentInfo = $this->_oDb->getContentInfoById($aEvent['object_id']);
+            if($aContentInfo[$CNF['FIELD_PUBLISHED']] > $a['date'])
+                $a['date'] = $aContentInfo[$CNF['FIELD_PUBLISHED']];
+        }
+
+        return $a;
+    }
+
+
+    // ====== PERMISSION METHODS
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedUsePaidJoin($isPerformAction = false)
+    {
+        // check ACL
+        $aCheck = checkActionModule($this->_iProfileId, 'use paid join', $this->getName(), $isPerformAction);
+        if($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedView ($aDataEntry, $isPerformAction = false)
+    {
+        return $this->serviceCheckAllowedViewForProfile ($aDataEntry, $isPerformAction);
+    }
+
+    public function serviceCheckAllowedViewForProfile ($aDataEntry, $isPerformAction = false, $iProfileId = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+        $iGroupContentId = (int)$aDataEntry[$CNF['FIELD_ID']];
+
+        $bInvited = false;
+        if(!empty($CNF['TABLE_INVITES'])) {
+            $iGroupProfileId = BxDolProfile::getInstanceByContentAndType($iGroupContentId, $this->getName())->id();
+
+            if(($sKey = bx_get('key')) !== false) {
+                $mixedInvited = $this->isInvited($sKey, $iGroupProfileId);
+                if($mixedInvited === true)
+                    $bInvited = true;
+            }
+            else {
+                $mixedInvited = $this->isInvitedByProfileId($iProfileId ? $iProfileId : bx_get_logged_profile_id(), $iGroupProfileId);
+                if($mixedInvited === true)
+                    $bInvited = true;
+            }
+        }
+
+        if ($this->isFan($iGroupContentId, $iProfileId) || $bInvited)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return parent::serviceCheckAllowedViewForProfile ($aDataEntry, $isPerformAction, $iProfileId);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedCompose(&$aDataEntry, $isPerformAction = false)
+    {
+        if(!$this->isFan($aDataEntry[$this->_oConfig->CNF['FIELD_ID']]))
+            return _t('_sys_txt_access_denied');
+
+        return parent::checkAllowedCompose ($aDataEntry, $isPerformAction);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedFanAdd(&$aDataEntry, $isPerformAction = false)
+    {
+        $mixedResult = $this->_modGroupsCheckAllowedFanAdd($aDataEntry, $isPerformAction);
+
+        // call alert to allow custom checks
+        bx_alert('system', 'check_allowed_fan_add', 0, 0, array(
+            'module' => $this->getName(), 
+            'content_info' => $aDataEntry, 
+            'profile_id' => bx_get_logged_profile_id(), 
+            'override_result' => &$mixedResult
+        ));
+
+        return $mixedResult;
+    }
+
+    public function _modGroupsCheckAllowedFanAdd (&$aDataEntry, $isPerformAction = false)
+    {
+        if ($this->isFan($aDataEntry[$this->_oConfig->CNF['FIELD_ID']]) || !isLogged())
+            return _t('_sys_txt_access_denied');
+
+        return $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, $this->_oConfig->CNF['OBJECT_CONNECTIONS'], false, false);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedFanRemove (&$aDataEntry, $isPerformAction = false)
+    {
+        if (CHECK_ACTION_RESULT_ALLOWED === $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, $this->_oConfig->CNF['OBJECT_CONNECTIONS'], false, true, true))
+            return CHECK_ACTION_RESULT_ALLOWED;
+        return $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, $this->_oConfig->CNF['OBJECT_CONNECTIONS'], false, true, false);
+    }
+
+    protected function _checkAllowedActionByFan($sAction, $aDataEntry, $iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $bRoles = $this->_oConfig->isRoles();
+        if(empty($iProfileId))
+            $iProfileId = bx_get_logged_profile_id();
+
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$CNF['FIELD_ID']], $this->getName());
+        if(!$oGroupProfile)
+            return $sAction == BX_BASE_MOD_GROUPS_ACTION_DELETE ? CHECK_ACTION_RESULT_ALLOWED : _t('_sys_txt_not_found');
+
+        $iGroupProfileId = $oGroupProfile->id();
+
+        if(!$bRoles && $this->_oDb->isAdmin($iGroupProfileId, $iProfileId, $aDataEntry))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        if($bRoles && $this->isAllowedActionByRole($sAction, $aDataEntry, $iGroupProfileId, $iProfileId))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return _t('_sys_txt_access_denied');
+    }
+
+    public function isAllowedActionByRole($mAction, $aDataEntry, $iGroupProfileId, $iProfileId)
+    {
+        $iProfileRole = $this->_oDb->getRole($iGroupProfileId, $iProfileId);
+
+        if (is_array($mAction)) {
+            $sAction = $mAction['action'];
+            $sActionModule = $mAction['module'];
+        } else {
+            $sAction = $mAction;
+            $sActionModule = $this->getName();
+        }
+
+        $bResult = false;
+        if($iProfileId)
+            $bResult = $this->isAllowedModuleActionByRole($sActionModule, $sAction, $iProfileRole);
+
+        // in case neither of the profile's roles are having permissions set explicitly then fallback to an old way
+        if ($bResult === NULL) {
+            $bResult = false;
+            if($this->isFanByGroupProfileId($iGroupProfileId)) {
+                switch ($sAction) {
+                    case BX_BASE_MOD_GROUPS_ACTION_DELETE:
+                    case BX_BASE_MOD_GROUPS_ACTION_EDIT:
+                    case BX_BASE_MOD_GROUPS_ACTION_CHANGE_COVER:
+                    case BX_BASE_MOD_GROUPS_ACTION_MANAGE_ROLES:
+                        if($this->isRole($iProfileRole, BX_BASE_MOD_GROUPS_ROLE_ADMINISTRATOR)) 
+                            $bResult = true;
+                        break;
+
+                    case BX_BASE_MOD_GROUPS_ACTION_MANAGE_FANS:
+                    case BX_BASE_MOD_GROUPS_ACTION_INVITE:
+                    case BX_BASE_MOD_GROUPS_ACTION_EDIT_CONTENT:
+                    case BX_BASE_MOD_GROUPS_ACTION_DELETE_CONTENT:
+                    case BX_BASE_MOD_GROUPS_ACTION_TIMELINE_POST_PIN:
+                        if($this->isRole($iProfileRole, BX_BASE_MOD_GROUPS_ROLE_ADMINISTRATOR) || $this->isRole($iProfileRole, BX_BASE_MOD_GROUPS_ROLE_MODERATOR)) 
+                            $bResult = true;
+                        break;
+
+                    default:
+                        $bResult = true;
+                }
+            }
+        }
+
+        // call alert to allow custom checks
+        bx_alert('system', 'check_allowed_action_by_role', 0, 0, [
+            'module' => $this->getName(), 
+            'multi_roles' => $this->_oConfig->isMultiRoles(),
+            'action' => $sAction,
+            'action_module' => $sActionModule,
+            'content_profile_id' => $iGroupProfileId, 
+            'content_info' => $aDataEntry, 
+            'profile_id' => $iProfileId, 
+            'profile_role' => $iProfileRole,
+            'override_result' => &$bResult
+        ]);
+
+        return $bResult;
+    }
+
+    public function isAllowedModuleActionByRole($sModule, $sAction, $iProfileRole)
+    {
+        static $aRoles;
+
+        if (!$aRoles && isset($this->_oConfig->CNF['OBJECT_PRE_LIST_ROLES']) && !empty($this->_oConfig->CNF['OBJECT_PRE_LIST_ROLES']))
+            $aRoles = BxBaseFormView::getDataItems($this->_oConfig->CNF['OBJECT_PRE_LIST_ROLES'], true, BX_DATA_VALUES_ALL);
+
+        if ($aRoles) {
+            foreach ($aRoles as $iRole => $aRoleData) {
+                if ($iRole == 0 && $iProfileRole == 0 || $iRole > 0 && $this->isRole($iProfileRole, $iRole)) {
+                    $mPermissions = isset($aRoles[$iRole]) && isset($aRoles[$iRole]['Data']) && !empty($aRoles[$iRole]['Data']) ? unserialize($aRoles[$iRole]['Data']) : false;
+                    if ($mPermissions && isset($mPermissions[$sModule])) {
+                        return isset($mPermissions[$sModule][$sAction]) && $mPermissions[$sModule][$sAction];
+                    }
+                }
+            }
+        }
+
+        return NULL;
+    }
+
+    public function isAllowedModuleActionByProfile($iContentId, $sPostModule, $sAction, $iProfileId = 0) {
+        if (!$iProfileId) $iProfileId = bx_get_logged_profile_id();
+
+        if ($iProfileId && $this->isFan($iContentId, $iProfileId)) {
+            $sModuleName = $this->getName();
+            $oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $sModuleName);
+            $aDataEntry = $this->_oDb->getContentInfoById($iContentId);
+            $bResult = $this->isAllowedActionByRole(['action' => $sAction, 'module' => $sPostModule], $aDataEntry, $oGroupProfile->id(), $iProfileId);
+
+            if ($bResult === true) return CHECK_ACTION_RESULT_ALLOWED;
+            if ($bResult === false) return _t('_sys_txt_access_denied');
+        }
+
+        return NULL; //undefined, because the profile is either not a fan or his role is not having permissions defined. So process the default way then.
+    }
+
+    public function isRole($iProfileRole, $iRole)
+    {
+        if(!$this->_oConfig->isMultiRoles())
+            return $iProfileRole == $iRole;
+        else 
+            return $iProfileRole & (1 << ($iRole - 1));
+    }
+
+    public function serviceIsRole($iProfileRole, $iRole)
+    {
+        return $this->isRole($iProfileRole, $iRole);
+    }
+
+    public function checkAllowedManageFans($mixedDataEntry, $isPerformAction = false)
+    {
+        $aDataEntry = array();
+        if(!is_array($mixedDataEntry)) {
+            $oGroupProfile = BxDolProfile::getInstance((int)$mixedDataEntry);
+            if($oGroupProfile && $this->getName() == $oGroupProfile->getModule())
+                $aDataEntry = $this->_oDb->getContentInfoById($oGroupProfile->getContentId());
+        }
+        else
+            $aDataEntry = $mixedDataEntry;
+
+        if($this->_checkAllowedActionByFan(BX_BASE_MOD_GROUPS_ACTION_MANAGE_FANS, $aDataEntry) === CHECK_ACTION_RESULT_ALLOWED)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return parent::checkAllowedEdit($aDataEntry, $isPerformAction);
+    }
+
+    public function checkAllowedManageAdmins($mixedDataEntry, $isPerformAction = false)
+    {
+        $aDataEntry = array();
+        if(!is_array($mixedDataEntry)) {
+            $oGroupProfile = BxDolProfile::getInstance((int)$mixedDataEntry);
+            if($oGroupProfile && $this->getName() == $oGroupProfile->getModule())
+                $aDataEntry = $this->_oDb->getContentInfoById($oGroupProfile->getContentId());
+        }
+        else
+            $aDataEntry = $mixedDataEntry;
+
+        if($this->_checkAllowedActionByFan(BX_BASE_MOD_GROUPS_ACTION_MANAGE_ROLES, $aDataEntry) === CHECK_ACTION_RESULT_ALLOWED)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return parent::checkAllowedEdit($aDataEntry, $isPerformAction);
+    }
+
+    public function checkAllowedEdit($aDataEntry, $isPerformAction = false)
+    {
+        if($this->_checkAllowedActionByFan(BX_BASE_MOD_GROUPS_ACTION_EDIT, $aDataEntry) === CHECK_ACTION_RESULT_ALLOWED)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return parent::checkAllowedEdit($aDataEntry, $isPerformAction);
+    }
+
+    public function checkAllowedInvite($aDataEntry, $isPerformAction = false)
+    {
+        if($this->_checkAllowedActionByFan(BX_BASE_MOD_GROUPS_ACTION_INVITE, $aDataEntry) === CHECK_ACTION_RESULT_ALLOWED)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return parent::checkAllowedEdit($aDataEntry, $isPerformAction);
+    }
+    
+    public function checkAllowedChangeCover($aDataEntry, $isPerformAction = false)
+    {
+        if($this->_checkAllowedActionByFan(BX_BASE_MOD_GROUPS_ACTION_CHANGE_COVER, $aDataEntry) === CHECK_ACTION_RESULT_ALLOWED)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return parent::checkAllowedChangeCover($aDataEntry, $isPerformAction);
+    }
+
+    public function checkAllowedDelete(&$aDataEntry, $isPerformAction = false)
+    {
+        if($this->_checkAllowedActionByFan(BX_BASE_MOD_GROUPS_ACTION_DELETE, $aDataEntry) === CHECK_ACTION_RESULT_ALLOWED)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return parent::checkAllowedDelete($aDataEntry, $isPerformAction);
+    }
+
+    public function checkAllowedJoin(&$aDataEntry, $isPerformAction = false)
+    {
+        if (bx_get('key')){
+            $sKey = bx_get('key');
+            $oGroupProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->getName());
+            $aData = $this->_oDb->getInviteByKey($sKey, $oGroupProfile->id());
+            if (isset($aData['invited_profile_id']) && $aData['invited_profile_id'] == bx_get_logged_profile_id()){
+                return CHECK_ACTION_RESULT_ALLOWED;
+            }
+        }   
+        return _t('_sys_txt_access_denied');
+    }   
+
+    public function checkAllowedSubscribeAdd(&$aDataEntry, $isPerformAction = false)
+    {
+        $mixedResult = $this->_modGroupsCheckAllowedSubscribeAdd($aDataEntry, $isPerformAction);
+
+        // call alert to allow custom checks
+        bx_alert('system', 'check_allowed_subscribe_add', 0, 0, array(
+            'module' => $this->getName(), 
+            'content_info' => $aDataEntry, 
+            'profile_id' => bx_get_logged_profile_id(), 
+            'override_result' => &$mixedResult
+        ));
+
+        return $mixedResult;
+    }
+
+    /**
+     * Note. Is mainly needed for internal usage. Access level is 'public' to allow outer calls from alerts.
+     */
+    public function _modGroupsCheckAllowedSubscribeAdd(&$aDataEntry, $isPerformAction = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$this->isFan($aDataEntry[$CNF['FIELD_ID']]) && (!isset($CNF['PARAM_SBS_WO_JOIN']) || getParam($CNF['PARAM_SBS_WO_JOIN']) != 'on'))
+            return _t('_sys_txt_access_denied');
+
+        return parent::_modProfileCheckAllowedSubscribeAdd($aDataEntry, $isPerformAction);
+    }
+
+    /**
+     * @deprecated since version 11.0.3 and can be removed in the next version.
+     */
+    public function _checkAllowedSubscribeAdd (&$aDataEntry, $isPerformAction = false)
+    {
+        return parent::checkAllowedSubscribeAdd ($aDataEntry, $isPerformAction);
+    }
+    
+    public function doAudit($iGroupProfileId, $iFanId, $sAction)
+    {
+        $oProfile = BxDolProfile::getInstance($iFanId);
+        
+        $iContentId = $oProfile->getContentId();
+        $sModule = $oProfile->getModule();
+        $oModule = BxDolModule::getInstance($sModule);
+        if (BxDolRequest::serviceExists($sModule, 'act_as_profile') && BxDolService::call($sModule, 'act_as_profile') && $oModule->_oConfig){
+            $CNF = $oModule->_oConfig->CNF;
+
+            $aContentInfo = BxDolRequest::serviceExists($sModule, 'get_all') ? BxDolService::call($sModule, 'get_all', array(array('type' => 'id', 'id' => $iContentId))) : array();
+        
+            $AuditParams = array(
+                'content_title' => (isset($CNF['FIELD_TITLE']) && isset($aContentInfo[$CNF['FIELD_TITLE']])) ? $aContentInfo[$CNF['FIELD_TITLE']] : '',
+                'context_profile_id' => $iGroupProfileId,
+                'context_profile_title' => BxDolProfile::getInstance($iGroupProfileId)->getDisplayName()
+            );
+        
+            bx_audit(
+                $iContentId, 
+                $sModule, 
+                $sAction,  
+                $AuditParams
+            );
+        }
+    }
+    
+    protected function _checkAllowedConnect (&$aDataEntry, $isPerformAction, $sObjConnection, $isMutual, $isInvertResult, $isSwap = false)
+    {
+        $sResult = $this->checkAllowedView($aDataEntry);
+
+        $oPrivacy = BxDolPrivacy::getObjectInstance($this->_oConfig->CNF['OBJECT_PRIVACY_VIEW']);
+
+        // if profile view isn't allowed but visibility is in partially visible groups 
+        // then display buttons to connect (befriend, join) to profile, 
+        // if other conditions (in parent::_checkAllowedConnect) are met as well
+        if (CHECK_ACTION_RESULT_ALLOWED !== $sResult && !in_array($aDataEntry[$this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']], array_merge($oPrivacy->getPartiallyVisiblePrivacyGroups(), array('s'))))
+            return $sResult;
+
+        return parent::_checkAllowedConnect ($aDataEntry, $isPerformAction, $sObjConnection, $isMutual, $isInvertResult, $isSwap);
+    }
+
+
+    // ====== COMMON METHODS
+    public function onUpdateImage($iContentId, $sFiledName, $sFiledValue, $iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+
+        $aField2Method = [
+            $CNF['FIELD_PICTURE'] => 'picture',
+            $CNF['FIELD_COVER'] => 'cover',
+        ];
+
+        if(!empty($aField2Method[$sFiledName]))
+            bx_alert($this->getName(), 'context_' . $aField2Method[$sFiledName] . '_changed', $iContentId, $iProfileId, $this->_alertParams($aContentInfo));
+    }
+
+    protected function _alertParams($aContentInfo)
+    {
+        $aParams = parent::_alertParams($aContentInfo);
+
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!empty($CNF['FIELD_CF']) && isset($aContentInfo[$CNF['FIELD_CF']]))
+            $aParams['cf'] = $aContentInfo[$CNF['FIELD_CF']];
+
+        return $aParams;
+    }
+
+    public function alertAfterAdd($aContentInfo)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $iId = (int)$aContentInfo[$CNF['FIELD_ID']];
+        $iAuthorId = (int)$aContentInfo[$CNF['FIELD_AUTHOR']];
+
+        $sAction = 'added';
+        if(isset($CNF['FIELD_STATUS_ADMIN']) && isset($aContentInfo[$CNF['FIELD_STATUS_ADMIN']]) && $aContentInfo[$CNF['FIELD_STATUS_ADMIN']] == BX_BASE_MOD_GENERAL_STATUS_PENDING)
+            $sAction = 'deferred';        
+
+        $sModule = $this->getName();
+        $aParams = $this->_alertParams($aContentInfo);
+        bx_alert('system', 'prepare_alert_params', 0, 0, [
+            'unit'=> $sModule, 
+            'action' => &$sAction, 
+            'object_id' => &$iId, 
+            'sender_id' => &$iAuthorId, 
+            'extras' => &$aParams
+        ]);
+        bx_alert($sModule, $sAction, $iId, false, $aParams);
+
+        $this->_processModerationNotifications($aContentInfo);
+    }
+
+    public function addFollower ($iProfileId1, $iProfileId2)
+    {
+        $oConnectionFollow = BxDolConnection::getObjectInstance('sys_profiles_subscriptions');
+        if($oConnectionFollow && !$oConnectionFollow->isConnected($iProfileId1, $iProfileId2)){
+            $oConnectionFollow->addConnection($iProfileId1, $iProfileId2);
+            return true;
+        }
+        return false;
+    }
+    
+    public function isFan ($iContentId, $iProfileId = false) 
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName());
+        if($oGroupProfile && isset($CNF['OBJECT_CONNECTIONS']))
+            return ($oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])) && $oConnection->isConnected($iProfileId ? $iProfileId : bx_get_logged_profile_id(), $oGroupProfile->id(), true);
+
+        return false;
+    }
+
+    public function isFanByGroupProfileId ($iGroupProfileId, $iProfileId = false) 
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $oGroupProfile = BxDolProfile::getInstance($iGroupProfileId);
+        if($oGroupProfile && isset($CNF['OBJECT_CONNECTIONS']))
+            return ($oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])) && $oConnection->isConnected($iProfileId ? $iProfileId : bx_get_logged_profile_id(), $oGroupProfile->id(), true);
+
+        return false;
+    }
+
+    public function isInvited ($sKey, $iGroupProfileId) 
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $aData = $this->_oDb->getInviteByKey($sKey,  $iGroupProfileId);
+        if (!isset($aData['invited_profile_id']))
+            return _t($CNF['T']['txt_invitation_popup_error_invitation_absent']);
+        
+        if ($aData['invited_profile_id'] != bx_get_logged_profile_id())
+            return _t($CNF['T']['txt_invitation_popup_error_wrong_user']);
+        
+        return true;
+    }
+
+    public function isInvitedByProfileId ($iProfileId, $iGroupProfileId) 
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aData = $this->_oDb->getInviteByInvited($iProfileId,  $iGroupProfileId);
+        if (!isset($aData['invited_profile_id']))
+            return _t($CNF['T']['txt_invitation_popup_error_invitation_absent']);
+
+        if ($aData['invited_profile_id'] != bx_get_logged_profile_id())
+            return _t($CNF['T']['txt_invitation_popup_error_wrong_user']);
+
+        return true;
+    }
+
+    public function serviceIsInvited($iGroupProfileId, $iProfileId = false, $sKey = '')
+    {
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+
+        if(empty($sKey) && ($sKey = bx_get('key')) !== false)
+            $sKey = bx_process_input($sKey);
+
+        $mixedInvited = false;
+        if(!empty($sKey))
+            $mixedInvited = $this->isInvited($sKey, $iGroupProfileId);
+        else if($iProfileId !== false)
+            $mixedInvited = $this->isInvitedByProfileId($iProfileId, $iGroupProfileId);
+
+        return $mixedInvited === true;
+    }
+
+    public function serviceIsNotInvited($iGroupProfileId, $iProfileId = false, $sKey = '')
+    {
+        return !$this->serviceIsInvited($iGroupProfileId, $iProfileId, $sKey);
+    }
+
+    public function serviceGetInvitedKey($iGroupProfileId, $iProfileId = false)
+    {
+        $sKey = '';
+        if(($sKey = bx_get('key')) !== false)
+            $sKey = bx_process_input($sKey);
+
+        if(!$sKey) {
+            if(!$iProfileId)
+                $iProfileId = bx_get_logged_profile_id();
+
+            if($iProfileId !== false) {
+                $aInvite = $this->_oDb->getInviteByInvited($iProfileId, $iGroupProfileId);
+                if(!empty($aInvite) && is_array($aInvite))
+                    $sKey = $aInvite['key'];
+            }
+        }
+
+        return $sKey;
+    }
+
+    public function getRole($iGroupProfileId, $iFanProfileId)
+    {
+        if(!$this->isFanByGroupProfileId($iGroupProfileId, $iFanProfileId))
+            return false;
+
+        return $this->_oDb->getRole($iGroupProfileId, $iFanProfileId);
+    }
+
+    public function setRole($iGroupProfileId, $iFanProfileId, $mixedRole, $mixedPeriod = false, $sOrder = '')
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!isset($CNF['OBJECT_CONNECTIONS']))
+            return false;
+
+        $oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS']);
+        $oGroupProfile = BxDolProfile::getInstance($iGroupProfileId);
+        if(!$oConnection || !$oGroupProfile)
+            return false;
+
+        if(!$oConnection->isConnected($iFanProfileId, $iGroupProfileId, true) && !$oConnection->addConnection($iFanProfileId, $iGroupProfileId))
+            return false;
+
+        if(!$this->_oDb->setRole($iGroupProfileId, $iFanProfileId, $mixedRole, $mixedPeriod, $sOrder))
+            return false;
+
+        $this->onSetRole($iGroupProfileId, $iFanProfileId, $mixedRole);
+
+        return true;
+    }
+
+    public function onSetRole($iGroupProfileId, $iFanProfileId, $mixedRole)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $iProfileId = bx_get_logged_profile_id();
+        $oGroupProfile = BxDolProfile::getInstance($iGroupProfileId);
+        $aGroupProfileInfo = $this->_oDb->getContentInfoById((int)$oGroupProfile->getContentId());
+        $aRoles = BxDolFormQuery::getDataItems($CNF['OBJECT_PRE_LIST_ROLES']);
+
+        // notify about admin status
+        if(!empty($CNF['EMAIL_FAN_SET_ROLE']) && $iFanProfileId != $iProfileId) {
+            $aSetRoles = is_array($mixedRole) ? $mixedRole : [$mixedRole];
+            $aRolesNames = [];
+            foreach ($aSetRoles as $iRole)
+                $aRolesNames[] = $aRoles[(int)$iRole];
+
+            sendMailTemplate($CNF['EMAIL_FAN_SET_ROLE'], 0, $iFanProfileId, array(
+                'EntryUrl' => $oGroupProfile->getUrl(),
+                'EntryTitle' => $oGroupProfile->getDisplayName(),
+                'Role' => implode(', ', $aRolesNames),
+            ), BX_EMAIL_NOTIFY);
+        }
+
+        bx_alert($this->getName(), 'set_role', $aGroupProfileInfo[$CNF['FIELD_ID']], $iGroupProfileId, array(
+            'object_author_id' => $iGroupProfileId,
+            'performer_id' => $iProfileId, 
+            'fan_id' => $iFanProfileId,
+
+            'content' => $aGroupProfileInfo, 
+            'role' => $mixedRole,
+
+            'group_profile' => $iGroupProfileId, 
+            'profile' => $iProfileId
+        ));
+
+        $this->doAudit($iGroupProfileId, $iFanProfileId, '_sys_audit_action_group_role_changed');
+    }
+
+    public function unsetRole($iGroupProfileId, $iFanProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!isset($CNF['OBJECT_CONNECTIONS']))
+            return false;
+
+        $oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS']);
+        $oGroupProfile = BxDolProfile::getInstance($iGroupProfileId);
+        if(!$oConnection || !$oGroupProfile)
+            return false;
+
+        if($oConnection->isConnected($iFanProfileId, $iGroupProfileId, true) && !$oConnection->removeConnection($iFanProfileId, $iGroupProfileId))
+            return false;
+
+        $iRole = $this->_oDb->getRole($iGroupProfileId, $iFanProfileId);
+
+        if(!$this->_oDb->unsetRole($iGroupProfileId, $iFanProfileId))
+            return false;
+
+        $this->onUnsetRole($iGroupProfileId, $iFanProfileId, $iRole);
+
+        return true;
+    }
+
+    public function onUnsetRole($iGroupProfileId, $iFanProfileId, $iRole)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $iProfileId = bx_get_logged_profile_id();
+        $oGroupProfile = BxDolProfile::getInstance($iGroupProfileId);
+        $aGroupProfileInfo = $this->_oDb->getContentInfoById((int)$oGroupProfile->getContentId());
+
+        bx_alert($this->getName(), 'set_role', $aGroupProfileInfo[$CNF['FIELD_ID']], $iGroupProfileId, array(
+            'object_author_id' => $iGroupProfileId,
+            'performer_id' => $iProfileId, 
+            'fan_id' => $iFanProfileId,
+
+            'content' => $aGroupProfileInfo,
+            'role' => $iRole,
+
+            'group_profile' => $iGroupProfileId, 
+            'profile' => $iProfileId
+        ));
+
+        $this->doAudit($iGroupProfileId, $iFanProfileId, '_sys_audit_action_group_role_changed');
+    }
+
+    public function getGroupsByFan($iProfileId, $mixedRole = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!isset($CNF['OBJECT_CONNECTIONS']))
+            return false;
+
+        if($mixedRole === false)
+            $mixedRole = BX_BASE_MOD_GROUPS_ROLE_COMMON;
+
+        if(!is_array($mixedRole))
+            $mixedRole = [$mixedRole];
+
+        $aResult = [];
+        foreach($mixedRole as $iRole) {
+            switch($iRole) {
+                case BX_BASE_MOD_GROUPS_ROLE_COMMON:
+                    $aIds = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])->getConnectedContent($iProfileId);
+                    break;
+
+                default:
+                    $aIds = $this->_oDb->getRoles([
+                        'type' => 'group_pids_by_fan_id', 
+                        'fan_id' => $iProfileId,
+                        'role' => $iRole
+                    ]);
+            }
+
+            $aResult = array_merge($aResult, $aIds);
+        }
+
+        return $aResult;
+    }
+
+    public function getMenuItemTitleByConnection($sConnection, $sAction, $iContentProfileId, $iInitiatorProfileId = 0)
+    {
+        $CNF = $this->_oConfig->getCNF();
+
+        $aResult = parent::getMenuItemTitleByConnection($sConnection, $sAction, $iContentProfileId, $iInitiatorProfileId);
+        if(!empty($aResult))
+            return $aResult;
+
+        $oConnection = BxDolConnection::getObjectInstance($sConnection);
+        if(!$oConnection)
+            return false;
+
+        if(!$iInitiatorProfileId)
+            $iInitiatorProfileId = bx_get_logged_profile_id();
+
+        $aResult = [];
+        if($oConnection->isConnectedNotMutual($iInitiatorProfileId, $iContentProfileId))
+            $aResult = [
+                'add' => _t(!empty($CNF['T']['menu_item_title_sm_join_requested']) ? $CNF['T']['menu_item_title_sm_join_requested'] : '_sys_menu_item_title_sm_join_requested'),
+                'remove' => _t(!empty($CNF['T']['menu_item_title_sm_leave_cancel']) ? $CNF['T']['menu_item_title_sm_leave_cancel'] : '_sys_menu_item_title_sm_leave_cancel'),
+            ];
+        else if($oConnection->isConnectedNotMutual($iContentProfileId, $iInitiatorProfileId))
+            $aResult = [
+                'add' => _t(!empty($CNF['T']['menu_item_title_sm_join_confirm']) ? $CNF['T']['menu_item_title_sm_join_confirm'] : '_sys_menu_item_title_sm_join_confirm'),
+                'remove' => _t(!empty($CNF['T']['menu_item_title_sm_leave_reject']) ? $CNF['T']['menu_item_title_sm_leave_reject'] : '_sys_menu_item_title_sm_leave_reject'),
+            ];
+        else if($oConnection->isConnected($iInitiatorProfileId, $iContentProfileId, true))
+            $aResult = [
+                'add' => '',
+                'remove' => _t(!empty($CNF['T']['menu_item_title_sm_leave']) ? $CNF['T']['menu_item_title_sm_leave'] : '_sys_menu_item_title_sm_leave'),
+            ];
+        else
+            $aResult = [
+                'add' => _t(!empty($CNF['T']['menu_item_title_sm_join']) ? $CNF['T']['menu_item_title_sm_join'] : '_sys_menu_item_title_sm_join'),
+                'remove' => '',
+            ];
+
+        return !empty($sAction) && isset($aResult[$sAction]) ? $aResult[$sAction] : $aResult;
+    }
+
+    protected function _getImagesForTimelinePost($aEvent, $aContentInfo, $sUrl, $aBrowseParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+        $iImageId = 0;
+        $sImageMd = $sImageXl = '';
+        if(isset($CNF['FIELD_COVER']) && !empty($aContentInfo[$CNF['FIELD_COVER']])) {
+            $iImageId = (int)$aContentInfo[$CNF['FIELD_COVER']];
+            $sImageMd = $this->_oConfig->getImageUrl($iImageId, ['OBJECT_IMAGES_TRANSCODER_GALLERY']);
+            $sImageXl = $this->_oConfig->getImageUrl($iImageId, ['OBJECT_IMAGES_TRANSCODER_COVER']);
+        }
+
+        if($sImageXl == '' && isset($CNF['FIELD_PICTURE']) && !empty($aContentInfo[$CNF['FIELD_PICTURE']])) {
+            $iImageId = (int)$aContentInfo[$CNF['FIELD_PICTURE']];
+            $sImageMd = $this->_oConfig->getImageUrl($iImageId, ['OBJECT_IMAGES_TRANSCODER_GALLERY']);
+            $sImageXl = $this->_oConfig->getImageUrl($iImageId, ['OBJECT_IMAGES_TRANSCODER_COVER']);
+        }
+
+        if(empty($sImageXl))
+            return [];
+
+        $aImage = [
+            'id' => $iImageId, 
+            'url' => $sUrl, 
+            'src' => $sImageXl, 
+            'src_orig' => $sImageXl
+        ];
+
+        if(!empty($sImageMd))
+            $aImage['src_medium'] = $sImageMd;
+
+        return [
+            $aImage
+        ];
+    }
+
+    protected function _prepareProfileAndGroupProfile($iGroupProfileId, $iInitiatorId)
+    {
+        if (!($oGroupProfile = BxDolProfile::getInstance($iGroupProfileId)))
+            return array(0, 0, null);
+
+        if ($oGroupProfile->getModule() == $this->getName()) {
+            $iProfileId = $iInitiatorId;
+            $iGroupProfileId = $oGroupProfile->id();
+        } else {
+            $iProfileId = $oGroupProfile->id();
+            $iGroupProfileId = $iInitiatorId;
+        }
+
+        return array($iProfileId, $iGroupProfileId, $oGroupProfile);
+    }
+}
+
+/** @} */

+ 150 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/groups/classes/BxBaseModGroupsPageEntry.php

@@ -0,0 +1,150 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Profile create/edit/delete pages.
+ */
+class BxBaseModGroupsPageEntry extends BxBaseModProfilePageEntry
+{
+    public function __construct($aObject, $oTemplate = false)
+    {
+        parent::__construct($aObject, $oTemplate);
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $this->_sCoverClass = 'bx-base-group-cover-wrapper ' . $this->_oModule->getName() . '_cover';
+
+        $bLoggedOwner = isset($this->_aContentInfo[$CNF['FIELD_AUTHOR']]) && $this->_aContentInfo[$CNF['FIELD_AUTHOR']] == bx_get_logged_profile_id();
+        $bLoggedModerator = $this->_oModule->checkAllowedEditAnyEntry() === CHECK_ACTION_RESULT_ALLOWED;
+
+        if(!empty($CNF['FIELD_CF']) && isset($this->_aContentInfo[$CNF['FIELD_CF']])) {
+            $oCf = BxDolContentFilter::getInstance();
+            if($oCf->isEnabled() && !$oCf->isAllowed($this->_aContentInfo[$CNF['FIELD_CF']])) {
+                $this->setPageCover(false);
+                return;
+            }
+        }
+
+        $aInformers = array ();
+        $oInformer = BxDolInformer::getInstance($this->_oTemplate);
+        if($oInformer && ($bLoggedOwner || $bLoggedModerator)) {
+            $sStatus = isset($CNF['FIELD_STATUS']) && isset($this->_aContentInfo[$CNF['FIELD_STATUS']]) ? $this->_aContentInfo[$CNF['FIELD_STATUS']] : '';
+            $sStatusAdmin = isset($CNF['FIELD_STATUS_ADMIN']) && isset($this->_aContentInfo[$CNF['FIELD_STATUS_ADMIN']]) ? $this->_aContentInfo[$CNF['FIELD_STATUS_ADMIN']] : '';
+
+            //--- Display 'approving' informer.
+            if(!empty($sStatusAdmin) && $sStatusAdmin != BX_BASE_MOD_GENERAL_STATUS_ACTIVE) {
+                if(!empty($CNF['INFORMERS']['approving']) && isset($CNF['INFORMERS']['approving']['map'][$sStatusAdmin])) {
+                    $aInformer = $CNF['INFORMERS']['approving'];
+                    $aInformers[] = ['name' => $aInformer['name'], 'msg' => _t($aInformer['map'][$sStatusAdmin]['msg']), 'type' => $aInformer['map'][$sStatusAdmin]['type']];
+                }
+            }
+
+            //--- Display 'scheduled' informer if an item wasn't published yet.
+            if(isset($CNF['FIELD_PUBLISHED'])) {
+                if(!empty($CNF['INFORMERS']['scheduled']) && isset($CNF['INFORMERS']['scheduled']['map'][$sStatus])) {
+                    $this->addMarkers(array(
+                        'date_publish_uf' => bx_time_js((int)$this->_aContentInfo[$CNF['FIELD_PUBLISHED']], BX_FORMAT_DATE, true)
+                    ));
+
+                    $aInformer = $CNF['INFORMERS']['scheduled'];
+                    $aInformers[] = array ('name' => $aInformer['name'], 'msg' => _t($aInformer['map'][$sStatus]['msg']), 'type' => $aInformer['map'][$sStatus]['type']);
+                }
+            }
+
+            if($aInformers)
+                foreach($aInformers as $aInformer)
+                    $oInformer->add($aInformer['name'], $this->_replaceMarkers($aInformer['msg']), $aInformer['type']);
+        }
+    }
+
+    protected function _getInvitationCode($aParams = [])
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($CNF['TABLE_INVITES']) || !$this->_oProfile)
+            return '';
+
+        $iProfileId = bx_get_logged_profile_id();
+        $iGroupProfileId = $this->_oProfile->id();
+        if(!$this->_oModule->serviceIsInvited($iGroupProfileId, $iProfileId)) 
+            return '';
+
+        $sId = $this->_oModule->getName() . '_popup_invite';
+        $sKey = $this->_oModule->serviceGetInvitedKey($iGroupProfileId, $iProfileId);
+
+        $sCode = $this->_oModule->_oTemplate->getJsCode('invite_popup', array(
+            'sPopupId' => $sId,
+            'sKey' => $sKey,
+            'sAcceptUrl' => bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $this->_aProfileInfo['content_id'])),
+            'sDeclineUrl' => BX_DOL_URL_ROOT,
+            'iGroupProfileId' => $this->_oProfile->id(),
+        ));
+
+        if(!isset($aParams['use_invitation_popup']) || $aParams['use_invitation_popup'] === true)
+            $sCode .= BxTemplFunctions::getInstance()->popupBox($sId, _t($CNF['T']['txt_invitation_popup_title']), $this->_oModule->_oTemplate->parseHtmlByName('popup_invite.html', array(
+                'popup_id' => $sId,
+                'text' => _t($CNF['T']['txt_invitation_popup_text']),
+                'button_accept' => _t($CNF['T']['txt_invitation_popup_accept_button']),
+                'button_decline' => _t($CNF['T']['txt_invitation_popup_decline_button']),
+            )), true);
+
+        $this->_oTemplate->addJs([
+            'modules/base/groups/js/|invite_popup.js', 
+            'invite_popup.js'
+        ]);
+        return $sCode;
+    }
+
+    public function getCode ()
+    {
+        return $this->_getInvitationCode() . parent::getCode();
+    }
+
+    protected function _isAvailablePage($a)
+    {
+        if(!$this->_aContentInfo || !$this->_oModule->isEntryActive($this->_aContentInfo))
+            return false;
+        
+        return parent::_isAvailablePage($a);
+    }
+
+    protected function _isVisiblePage ($a)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(($mixedAllowView = $this->_oModule->checkAllowedView($this->_aContentInfo)) !== CHECK_ACTION_RESULT_ALLOWED) {
+            $bPartiallyVisible = false;
+            if(!empty($CNF['OBJECT_PRIVACY_VIEW']) && ($oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW'])) !== false)
+                $bPartiallyVisible = $oPrivacy->isPartiallyVisible($this->_aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]);
+
+            /*
+             * If partially visible, replace current page with different set of blocks.
+             */
+            if($bPartiallyVisible && $this->_sObject == $CNF['OBJECT_PAGE_VIEW_ENTRY']) {
+                $aObject = BxDolPageQuery::getPageObject($CNF['OBJECT_PAGE_VIEW_ENTRY_CLOSED']);
+                $this->_sObject = $aObject['object'];
+                $this->_aObject = $aObject;
+                $this->_oQuery = new BxDolPageQuery($this->_aObject);
+            }
+            else
+                return $mixedAllowView;
+        }
+
+        if(!BxDolPage::_isVisiblePage($a))
+            return false;
+
+        $this->_oModule->checkAllowedView($this->_aContentInfo, true);
+
+        return true;
+    }
+}
+
+/** @} */

+ 89 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/groups/classes/BxBaseModGroupsPrivacyPost.php

@@ -0,0 +1,89 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+define('BX_BASE_MOD_GROUPS_PG_PARTICIPANTS', 'p');
+define('BX_BASE_MOD_GROUPS_PG_FOLLOWERS', 'f');
+
+class BxBaseModGroupsPrivacyPost extends BxBaseModProfilePrivacyPost
+{
+    public function __construct($aOptions, $oTemplate = false)
+    {
+        parent::__construct($aOptions, $oTemplate);
+
+        $this->_aGroupsExclude = [BX_DOL_PG_FRIENDS, BX_DOL_PG_FRIENDS_SELECTED, BX_DOL_PG_RELATIONS, BX_DOL_PG_RELATIONS_SELECTED];
+    }
+
+    /**
+     * Check whethere viewer is a member of dynamic group.
+     *
+     * @param  mixed   $mixedGroupId   dynamic group ID.
+     * @param  integer $iObjectOwnerId object owner ID.
+     * @param  integer $iViewerId      viewer ID.
+     * @return boolean result of operation.
+     */
+    public function isDynamicGroupMember($mixedGroupId, $iObjectOwnerId, $iViewerId, $iObjectId)
+    {
+        if($mixedGroupId == BX_BASE_MOD_GROUPS_PG_PARTICIPANTS)
+            return $this->isParticipantAccess($iObjectOwnerId, $iViewerId, $iObjectId);
+
+        if($mixedGroupId == BX_BASE_MOD_GROUPS_PG_FOLLOWERS)
+            return $this->isFollowersAccess($iObjectOwnerId, $iViewerId, $iObjectId);
+
+        return false;
+    }
+
+    public function isParticipantAccess ($iObjectOwnerId, $iViewerId, $iObjectId)
+    {
+        $oConnection = BxDolConnection::getObjectInstance($this->_oModule->_oConfig->CNF['OBJECT_CONNECTIONS']);
+        if(!$oConnection)
+            return false;
+
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($iObjectId, $this->_sModule);
+        return $oConnection->isConnected($iViewerId, $oGroupProfile->id(), true);
+    }
+
+    public function isFollowersAccess ($iObjectOwnerId, $iViewerId, $iObjectId)
+    {
+        $oConnection = BxDolConnection::getObjectInstance('sys_profiles_subscriptions');
+        if(!$oConnection)
+            return false;
+
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($iObjectId, $this->_sModule);
+        return $oConnection->isConnected($iViewerId, $oGroupProfile->id());
+    }
+
+    static function getGroupChooser ($sObject, $iOwnerId = 0, $aParams = array())
+    {
+        $oPrivacy = BxDolPrivacy::getObjectInstance($sObject);
+        if(empty($oPrivacy))
+            return array();
+
+        $CNF = &$oPrivacy->_oModule->_oConfig->CNF;
+
+        if (!$iOwnerId)
+            $iOwnerId = bx_get_logged_profile_id();
+
+        $aParams['dynamic_groups'] = array(
+            array ('key' => '', 'value' => '----', 'attrs' => array('disabled' => 'disabled')),
+            array ('key' => BX_BASE_MOD_GROUPS_PG_PARTICIPANTS, 'value' => _t(!empty($CNF['T']['txt_group_participants']) ? $CNF['T']['txt_group_participants'] : '_sys_ps_group_title_participants')),
+            array ('key' => BX_BASE_MOD_GROUPS_PG_FOLLOWERS, 'value' => _t(!empty($CNF['T']['txt_group_followers']) ? $CNF['T']['txt_group_followers'] : '_sys_ps_group_title_followers'))
+        );
+
+        return parent::getGroupChooser($sObject, $iOwnerId, $aParams);
+    }
+
+    protected function getObjectInfo($sAction, $iObjectId)
+    {
+        return BxDolPrivacy::getObjectInfo($sAction, $iObjectId);
+    }
+}
+
+/** @} */

+ 103 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/notifications/classes/BxBaseModNotificationsResponse.php

@@ -0,0 +1,103 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseNotifications Base classes for Notifications like modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModNotificationsResponse extends BxDolAlertsResponse
+{
+    protected $_sModule;
+    protected $_oModule;
+
+    public function __construct()
+    {
+        parent::__construct();
+
+        $this->_oModule = BxDolModule::getInstance($this->_sModule);
+    }
+
+    /**
+     * Note. Check priority is IMPORTANT!
+     * Don't swap the checks without a REAL need.
+     */
+    protected function _getObjectOwnerId($aExtras)
+    {
+        if(!is_array($aExtras))
+            return 0;
+
+        if(isset($aExtras['parent_author_id']))
+    		return (int)$aExtras['parent_author_id'];
+
+        if(isset($aExtras['object_author_id']))
+    		return (int)$aExtras['object_author_id'];
+
+        return 0;
+    }
+
+    protected function _getObjectPrivacyView($aExtras)
+    {
+        return $this->_oModule->getObjectPrivacyView($aExtras);
+    }
+
+    protected function _getObjectCf($aExtras)
+    {
+        return $this->_oModule->getObjectCf($aExtras);
+    }
+
+    /**
+     * Note. Check priority is NOT IMPORTANT!
+     */
+    protected function _getSubObjectId($aExtras)
+    {
+        if(!is_array($aExtras))
+            return 0;
+
+    	if(isset($aExtras['comment_id']))
+            return (int)$aExtras['comment_id'];
+
+        if(isset($aExtras['vote_id']))
+            return (int)$aExtras['vote_id'];
+
+        if(isset($aExtras['score_id']))
+            return (int)$aExtras['score_id'];
+
+        if(isset($aExtras['repost_id']))
+            return (int)$aExtras['repost_id'];
+
+        if(isset($aExtras['timeline_post_id']))
+            return (int)$aExtras['timeline_post_id'];
+
+        if(isset($aExtras['performer_id']))
+            return (int)$aExtras['performer_id'];
+        
+        if(isset($aExtras['subobject_id']))
+            return (int)$aExtras['subobject_id'];
+
+        if(isset($aExtras['subobjects_ids']))
+            return is_array($aExtras['subobjects_ids']) ? $aExtras['subobjects_ids'] : array($aExtras['subobjects_ids']);
+
+        return 0;
+    }
+
+    protected function _getEventUpdate(&$oAlert, &$aHandler)
+    {
+        $aHandlers = $this->_oModule->_oDb->getHandlers([
+            'type' => 'by_group_key_type', 
+            'group' => $aHandler['group']
+        ]);
+
+        return $this->_oModule->_oDb->getEvents([
+            'browse' => 'descriptor', 
+            'type' => $oAlert->sUnit, 
+            'action' => $aHandlers[BX_BASE_MOD_NTFS_HANDLER_TYPE_INSERT]['alert_action'],
+            'object_id' => $oAlert->iObject
+        ]);
+    }
+}
+
+/** @} */

+ 330 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/profile/classes/BxBaseModProfileGridAdministration.php

@@ -0,0 +1,330 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseProfile Base classes for profile modules
+ * @ingroup     UnaModules
+ * 
+ * @{
+ */
+
+class BxBaseModProfileGridAdministration extends BxBaseModGeneralGridAdministration
+{
+    protected $_sFilter1Name;
+    protected $_sFilter1Value;
+    protected $_aFilter1Values;
+
+    protected $_bSetAclWithDuration;
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        parent::__construct ($aOptions, $oTemplate);
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $this->_aQueryReset = array($this->_aOptions['paginate_get_start'], $this->_aOptions['paginate_get_per_page']);
+
+        $this->_sStatusField = !empty($CNF['FIELD_STATUS']) ? $CNF['FIELD_STATUS'] : 'status';
+        $this->_aStatusValues = array('active', 'pending', 'suspended');
+
+        $this->_sFilter1Name = 'filter1';
+        $this->_aFilter1Values = array(
+            'active' => $CNF['T']['filter_item_active'],
+            'pending' => $CNF['T']['filter_item_pending'],
+            'suspended' => $CNF['T']['filter_item_suspended'],
+        );
+
+    	$sFilter1 = bx_get($this->_sFilter1Name);
+        if(!empty($sFilter1)) {
+            $this->_sFilter1Value = bx_process_input($sFilter1);
+            $this->_aQueryAppend[$this->_sFilter1Name] = $this->_sFilter1Value;
+        }
+
+        $this->_bSetAclWithDuration = true;
+    }
+
+    public function performActionSetAclLevel()
+    {
+    	$oMenu = BxDolMenu::getObjectInstance('sys_set_acl_level');
+
+    	$aIds = bx_get('ids');
+        if(!$aIds || !is_array($aIds) || !$oMenu)
+            return echoJson([]);
+
+        $aIdsResult = [];
+        foreach($aIds as $iId) {
+            $aContentInfo = $this->_oModule->_oDb->getContentInfoById($iId);
+            if($this->_oModule->checkAllowedSetMembership($aContentInfo) !== CHECK_ACTION_RESULT_ALLOWED)
+                continue;
+
+            $aIdsResult[] = $this->_getProfileId($iId);
+        }
+
+        if(empty($aIdsResult))
+            return echoJson([]);
+
+        if(count($aIdsResult) == 1)
+            $aIdsResult = $aIdsResult[0];
+
+        $oMenu->setWithDuration($this->_bSetAclWithDuration);
+        $oMenu->setHiddenByDefault(!$this->_bSetAclWithDuration);
+        $sContent = $oMenu->getCode($aIdsResult);
+        if(!$this->_bSetAclWithDuration)
+            $sContent = BxTemplFunctions::getInstance()->transBox($this->_oModule->_oConfig->getName() . 'set_acl_level_popup', $this->_oTemplate->parseHtmlByName('set_acl_popup.html', [
+                'content' => $sContent
+            ]));
+
+    	echoJson(['popup' => $sContent]);
+    }
+
+    public function performActionManageCf()
+    {
+        if(!$this->_oModule->_isModerator())
+            return echoJson([]);
+
+        $sModule = $this->_oModule->_oConfig->getName();
+
+        $iContentId = $this->_getId();
+        if($iContentId === false)
+            return echoJson([]);
+
+        $aProfileInfo = BxDolProfileQuery::getInstance()->getProfileByContentAndType($iContentId, $sModule, true);
+        if(!$aProfileInfo || !is_array($aProfileInfo))
+            return echoJson([]);
+
+        $sAction = 'manage_cf';
+
+        $sForm = 'sys_profile';
+        $sFormDisplay = 'sys_profile_cf_manage';
+        $oForm = BxDolForm::getObjectInstance($sForm, $sFormDisplay);
+        if(!$oForm)
+            return echoJson([]);
+
+        $oForm->setAction(BX_DOL_URL_ROOT . 'grid.php', ['o' => $this->_sObject, 'a' => $sAction, 'id' => $iContentId]);
+        $oForm->initChecker($aProfileInfo);
+        if($oForm->isSubmittedAndValid()) {
+            if(!$oForm->update($aProfileInfo['id']))
+                $aResult = ['msg' => _t('_sys_txt_error_profile_update')];
+            else
+                $aResult = ['msg' => _t('_' . $sFormDisplay . '_successfully_submitted')];
+
+            return echoJson($aResult);
+        }
+
+        $sPopupTitle = _t('_' . $sFormDisplay . '_popup');
+        $sPopupContent = $this->_oTemplate->parseHtmlByName('content_filter_manage_popup.html', [
+            'form_id' => $oForm->getId(),
+            'form' => $oForm->getCode()
+        ]);
+
+        echoJson(['popup' => [
+            'html' => BxTemplFunctions::getInstance()->popupBox($sModule . '_manage_cf_popup',  $sPopupTitle, $sPopupContent),
+            'options' => [
+                'closeOnOuterClick' => false,
+                'removeOnClose' => true
+            ]
+        ]]);
+    }
+
+    public function performActionDeleteWithContent()
+    {
+    	$this->performActionDelete(array('with_content' => true));
+    }
+
+    protected function _getActionSetAclLevel($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+    	if($this->_sManageType == BX_DOL_MANAGE_TOOLS_ADMINISTRATION && $this->_oModule->checkAllowedSetMembership($aRow) !== CHECK_ACTION_RESULT_ALLOWED)
+            return '';
+
+        return $this->_getActionDefault($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);	
+    }
+
+    protected function _getActionManageCf($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        return '';
+    }
+
+    protected function _getActionAuditProfile($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if (!getParam('sys_audit_enable') || getParam('sys_audit_acl_levels') == '')
+            return;
+        
+        $iProfileId = bx_get_logged_profile_id();
+        if (!BxDolAcl::getInstance()->isMemberLevelInSet(explode(',', getParam('sys_audit_acl_levels')), $iProfileId))
+            return;
+    	
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+        $oProfile = $this->_getProfileObject($aRow[$CNF['FIELD_ID']]);
+        $sUrl = BX_DOL_URL_ROOT . 'page/audit-administration?profile=' . $oProfile->id();
+
+    	$a['attr'] = array_merge($a['attr'], array(
+            "onclick" => "window.open('" . $sUrl . "','_audit');"
+    	));
+
+    	return $this->_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+    
+    protected function _getActionSettings($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if (bx_is_api()) {
+            return array_merge($a, ['name' => 'delete', 'type' => 'callback', 'action' => 'delete','on_callback' => 'hide_row', 'confirm' => 1,  'params' => '&id=' . $aRow[$this->_aOptions['field_id']] ]);
+        }
+
+        return parent::_getActionSettings($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getActionDeleteWithContent($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if($this->_sManageType != BX_DOL_MANAGE_TOOLS_ADMINISTRATION || !$this->_oModule->_isAdministrator())
+            return '';
+
+    	return $this->_getActionDefault($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+    
+    protected function _switcherChecked2State($isChecked)
+    {
+        return $isChecked ? 'active' : 'suspended';
+    }
+
+    protected function _enable ($mixedId, $isChecked)
+    {
+    	$oProfile = $this->_getProfileObject($mixedId);
+
+    	if($oProfile instanceof BxDolProfileUndefined)
+            return false;
+
+        $iAction = BX_PROFILE_ACTION_MANUAL;
+        $bSendEmailNotification = $this->_oModule->serviceActAsProfile();
+    	return $isChecked ? $oProfile->activate($iAction, 0, $bSendEmailNotification) : $oProfile->suspend($iAction, 0, $bSendEmailNotification);
+    }
+
+    protected function _getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage)
+    {
+        $aFilterParts = explode($this->_sParamsDivider, $sFilter);
+        switch (substr_count($sFilter, $this->_sParamsDivider)) {
+            case 1:
+                list($this->_sFilter1Value, $sFilter) = $aFilterParts;
+                break;
+
+            case 2:
+                list($this->_sFilter1Value, $this->_sFilter2Value, $sFilter) = $aFilterParts;
+                break;
+        }
+
+    	if(!empty($this->_sFilter1Value))
+            $this->_aOptions['source'] .= $this->_oModule->_oDb->prepareAsString(" AND `tp`.`status`=?", $this->_sFilter1Value);
+
+        return $this->_getDataSqlInner($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage);
+    }
+
+    protected function _getDataSqlInner($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage)
+    {
+        return parent::_getDataSql($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage);
+    }
+
+    //--- Layout methods ---//
+    protected function _getFilterControls()
+    {
+        parent::_getFilterControls();
+
+        return  $this->_getFilterSelectOne($this->_sFilter1Name, $this->_sFilter1Value, $this->_aFilter1Values) . $this->_getSearchInput();
+    }
+
+    protected function _getFilterControlsAPI($aFilters = [])
+    {
+        $aFilters = [
+            $this->_sFilter1Name => []
+        ];
+
+        return parent::_getFilterControlsAPI($aFilters);
+    }
+
+    protected function _getCellSwitcher ($mixedValue, $sKey, $aField, $aRow)
+    {
+        if (isset($aRow['profile_id']))
+            if (!$this->_oModule->isAllowDeleteOrDisable(bx_get_logged_profile_id(), $aRow['profile_id']))
+                return parent::_getCellDefault('', $sKey, $aField, $aRow);    
+        
+        return parent::_getCellSwitcher ($mixedValue, $sKey, $aField, $aRow);
+    }
+    
+    protected function _getCellFullname($mixedValue, $sKey, $aField, $aRow)
+    {
+    	$oProfile = $this->_getProfileObject($aRow['id']);
+        
+        if (bx_is_api()){
+            return ['type' => 'profile', 'data' => BxDolProfile::getData($oProfile->id())];
+        }
+
+        return parent::_getCellDefault($oProfile->getUnit(), $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellLastOnline($mixedValue, $sKey, $aField, $aRow)
+    {
+        if (bx_is_api()){
+            return ['type' => 'time', 'data' => $mixedValue];
+        }
+        return parent::_getCellDefault(bx_time_js($mixedValue), $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellAddedTs($mixedValue, $sKey, $aField, $aRow)
+    {
+        return $this->_getCellLastOnline($mixedValue, $sKey, $aField, $aRow);
+    }
+
+	protected function _getCellAccount($mixedValue, $sKey, $aField, $aRow)
+    {
+    	$sManageAccountUrl = $this->_getManageAccountUrl($aRow[$sKey]);
+    	if(!empty($sManageAccountUrl)) {
+    		$mixedValue = $this->_oTemplate->parseHtmlByName('account_link.html', array(
+    			'href' => $sManageAccountUrl,
+    			'title' => _t($this->_oModule->_oConfig->CNF['T']['grid_txt_account_manager']),
+    			'content' => $mixedValue, 
+                'class' => ''
+    		));
+    	}
+
+        return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getProfileObject($iId)
+    {
+        return $this->_oModule->getProfileObject($iId);
+    }
+
+    protected function _getProfileId($iId)
+    {
+    	return $this->_getProfileObject($iId)->id();
+    }
+
+    protected function _doDelete($iId, $aParams = array())
+    {
+        $oProfile = $this->_getProfileObject($iId);
+        if(!$oProfile || $oProfile instanceof BxDolProfileUndefined)
+            return false;
+
+        if(!$this->_oModule->isAllowDeleteOrDisable(bx_get_logged_profile_id(), $oProfile->id()))
+            return false;
+
+    	if($this->_oModule->checkMyself($iId))
+            return false;
+
+        return $oProfile->delete($oProfile->id(), isset($aParams['with_content']) && $aParams['with_content'] === true);
+    }
+
+    protected function _getId()
+    {
+        $aIds = bx_get('ids');
+        if(!empty($aIds) && is_array($aIds)) 
+            return (int)reset($aIds);
+
+        $iId = bx_get('id');
+        if($iId !== false) 
+            return (int)$iId;
+
+        return false;
+    }
+}
+
+/** @} */

+ 2326 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/profile/classes/BxBaseModProfileModule.php

@@ -0,0 +1,2326 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseProfile Base classes for profile modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+define('BX_DOL_PROFILE_REDIRECT_PROFILE', 'profile');
+define('BX_DOL_PROFILE_REDIRECT_LAST', 'last');
+define('BX_DOL_PROFILE_REDIRECT_CUSTOM', 'custom');
+define('BX_DOL_PROFILE_REDIRECT_HOMEPAGE', 'homepage');
+
+define('BX_DOL_PROFILE_ACTIVATE_ALWAYS', 'on');
+define('BX_DOL_PROFILE_ACTIVATE_NEVER', 'off');
+define('BX_DOL_PROFILE_ACTIVATE_ADD', 'add');
+define('BX_DOL_PROFILE_ACTIVATE_EDIT', 'edit');
+
+/**
+ * Base class for profile modules.
+ */
+class BxBaseModProfileModule extends BxBaseModGeneralModule implements iBxDolContentInfoService, iBxDolProfileService
+{
+    protected $_iAccountId;
+
+    function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!empty($CNF['FIELD_ALLOW_POST_TO']))
+            $this->_aSearchableNamesExcept[] = $CNF['FIELD_ALLOW_POST_TO'];
+
+        if(!empty($CNF['FIELD_ALLOW_CONTACT_TO']))
+            $this->_aSearchableNamesExcept[] = $CNF['FIELD_ALLOW_CONTACT_TO'];
+
+        $this->_iAccountId = getLoggedId();
+    }
+
+    public function actionDeleteProfileImg($iFileId, $iContentId, $sFieldPicture) 
+    {
+        $aResult = array();
+        $CNF = &$this->_oConfig->CNF;
+
+        $oSrorage = BxDolStorage::getObjectInstance($CNF['OBJECT_STORAGE']);
+        if (!($aFile = $oSrorage->getFile((int)$iFileId)) || !($aContentInfo = $this->_oDb->getContentInfoById($iContentId)) || $aContentInfo[$sFieldPicture] != (int)$iFileId)
+            $aResult = array('error' => 1, 'msg' => _t('_sys_storage_err_file_not_found'));
+
+        $oAccountProfile = BxDolProfile::getInstanceAccountProfile();
+        if ($oAccountProfile)
+            $iAccountProfileId = $oAccountProfile->id();
+
+        if ((!$aResult && !isLogged()) || (!$aResult && $aFile['profile_id'] != $iAccountProfileId && !$this->_isModerator()))           
+            $aResult = array('error' => 2, 'msg' => _t('_Access denied'));
+
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD'], $this->_oTemplate);
+
+        if (!$aResult && !$oForm->_deleteFile($iContentId, $sFieldPicture, (int)$iFileId, true))
+            $aResult = array('error' => 3, 'msg' => _t('_Failed'));
+        elseif (!$aResult)            
+            $aResult = array('error' => 0, 'msg' => '');
+
+        header('Content-Type: application/json; charset=utf-8');
+        echo json_encode($aResult);
+    }
+	
+	public function actionProfileCalendarData()
+    {
+		$iProfileId = (int)bx_get('profile_id');
+        $aEntries = array();
+		$BxDolModuleQuery = BxDolModuleQuery::getInstance();
+        $aModules = $BxDolModuleQuery->getModulesBy(array('type' => 'modules', 'active' => 1));
+        foreach($aModules as $aModule){
+			$sModuleName = $aModule['name'];
+            $oModule = BxDolModule::getInstance($sModuleName);
+            if($oModule instanceof iBxDolCalendarService){
+				if(BxDolRequest::serviceExists($sModuleName, 'get_calendar_entries')){
+					$aEntries2 = BxDolService::call($sModuleName, 'get_calendar_entries', array($iProfileId));
+					$aEntries = array_merge($aEntries, $aEntries2);
+				}
+            }
+        }
+		
+        header('Content-Type: application/json; charset=utf-8');
+        echo json_encode($aEntries);
+    }
+    
+    public function actionEmbed($iContentId, $sUnitTemplate = '', $sAddCode = '')
+    {
+        return parent::actionEmbed($iContentId, 'unit_with_cover.html');
+    }
+
+    // ====== SERVICE METHODS
+
+    public function serviceGetSafeServices()
+    {
+        $a = parent::serviceGetSafeServices();
+        return array_merge($a, array (
+            'ProfileUnitSafe' => '',
+            'ProfileUrl' => '',
+            'EntityCreate' => '',
+            'BrowseRecommended' => '',
+            'BrowseRecentProfiles' => '',
+            'BrowseActiveProfiles' => '',
+            'BrowseTopProfiles' => '',
+            'BrowseOnlineProfiles' => '',
+            'BrowseConnections' => '',
+            'BrowseConnectionsEverywhere' => '',
+            'BrowseByAcl' => '',
+        ));
+    }
+
+    public function serviceGetOptionsRedirectAfterAdd()
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aResult = array();
+        $aChoices = array(BX_DOL_PROFILE_REDIRECT_PROFILE, BX_DOL_PROFILE_REDIRECT_LAST, BX_DOL_PROFILE_REDIRECT_CUSTOM, BX_DOL_PROFILE_REDIRECT_HOMEPAGE);
+        foreach($aChoices as $sChoice) 
+            $aResult[] = array('key' => $sChoice, 'value' => _t($CNF['T']['option_redirect_aadd_' . $sChoice]));
+
+        return $aResult;
+    }
+    
+    public function serviceGetOptionsActivation()
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aResult = array();
+        $aChoices = array(BX_DOL_PROFILE_ACTIVATE_ALWAYS, BX_DOL_PROFILE_ACTIVATE_NEVER, BX_DOL_PROFILE_ACTIVATE_ADD, BX_DOL_PROFILE_ACTIVATE_EDIT);
+        foreach($aChoices as $sChoice) 
+            $aResult[] = array('key' => $sChoice, 'value' => _t($CNF['T']['option_activation_' . $sChoice]));
+
+        return $aResult;
+    }
+
+    public function serviceGetThumb ($iContentId, $sTranscoder = '') 
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+        if(bx_is_api()){
+            $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+            return bx_api_get_image($CNF['OBJECT_STORAGE'], $aContentInfo[$CNF['FIELD_PICTURE']]);
+        }
+
+        if(empty($sTranscoder) && !empty($CNF['OBJECT_IMAGES_TRANSCODER_THUMB']))
+            $sTranscoder = $CNF['OBJECT_IMAGES_TRANSCODER_THUMB'];
+
+        $mixedResult = $this->_getFieldValueThumb('FIELD_PICTURE', $iContentId, $sTranscoder);
+        return $mixedResult !== false ? $mixedResult : '';
+    }
+    
+    public function serviceGetCover ($iContentId, $sTranscoder = '') 
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+        if(bx_is_api()){
+            $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+            $aTmp = bx_api_get_image($CNF['OBJECT_STORAGE'], $aContentInfo[$CNF['FIELD_COVER']]);
+            if (!$aTmp){
+                return ['storage' => $CNF['OBJECT_STORAGE']];
+            }
+            return $aTmp;
+        }
+
+        if(empty($sTranscoder) && !empty($CNF['OBJECT_IMAGES_TRANSCODER_COVER']))
+            $sTranscoder = $CNF['OBJECT_IMAGES_TRANSCODER_COVER'];
+
+        $mixedResult = $this->_getFieldValueThumb('FIELD_COVER', $iContentId, $sTranscoder);
+        return $mixedResult !== false ? $mixedResult : '';
+    }
+
+    public function serviceGetSearchResultUnit ($iContentId, $sUnitTemplate = '')
+    {
+        if(empty($sUnitTemplate))
+            $sUnitTemplate = 'unit_with_cover.html';
+
+        return parent::serviceGetSearchResultUnit($iContentId, $sUnitTemplate);
+    }
+
+    public function serviceGetSearchableFieldsExtended($aInputsAdd = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aResult = parent::serviceGetSearchableFieldsExtended($aInputsAdd);
+        if(!$this->serviceActAsProfile())
+            return $aResult;
+
+        if(!in_array('online', $this->_aSearchableNamesExcept))
+            $aResult['online'] = array(
+                'type' => 'checkbox', 
+                'caption' => $CNF['T']['form_field_online'],
+                'info' => '',
+            	'value' => '1',
+                'values' => '',
+                'pass' => ''
+            );
+
+        if(!empty($CNF['FIELD_PICTURE']) && !in_array($CNF['FIELD_PICTURE'], $this->_aSearchableNamesExcept))
+            $aResult[$CNF['FIELD_PICTURE']] = array(
+                'type' => 'checkbox', 
+                'caption' => $CNF['T']['form_field_picture'],
+                'info' => '',
+            	'value' => '1',
+                'values' => '',
+                'pass' => '',
+                'search_operator' => '>=' 
+            );
+
+        return $aResult;
+    }
+
+	
+	/**
+     * @page service Service Calls
+     * @section bx_base_profile Base Profile
+     * @subsection bx_base_profile-general General
+     * @subsubsection bx_base_profile-entity_calendar entity_calendar
+     * 
+     * @code bx_srv('bx_persons', 'entity_calendar', [...]); @endcode
+     * 
+     * Get calendar block
+     * @param $iContentId content ID
+     * 
+     * @see BxBaseModProfileModule::serviceEntityCalendar
+     */
+    /** 
+     * @ref bx_base_profile-entity_calendar "entity_calendar"
+     */
+	public function serviceEntityCalendar($iContentId = 0, $sTemplate = 'calendar.html')
+    {
+		$iContentId = $this->_getContent($iContentId, false);
+        $oProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->_oConfig->getName());
+        if($oProfile){
+			$o = new BxTemplCalendar(array(
+				'eventSources' => array (
+					bx_append_url_params(BX_DOL_URL_ROOT . $this->_oConfig->getBaseUri() . 'profile_calendar_data', array('profile_id' => $oProfile->id())),
+				),
+        	), $this->_oTemplate);
+        	return $o->display($sTemplate);			     
+		}
+		return '';	
+    }
+	
+	public function servicePrivateProfileMsg()
+    {
+        $mixedContent = $this->_getContent();
+        if ($mixedContent) {
+            list($iContentId, $aContentInfo) = $mixedContent;
+            return $this->checkAllowedView($aContentInfo);
+        }
+        return MsgBox(_t('_sys_access_denied_to_private_content'));
+    }
+    
+    public function serviceGetContentInfoById($iContentId)
+    {
+        return $this->_oDb->getContentInfoById((int)$iContentId);
+    }
+    
+    public function serviceGetContentInfoByProfileId($iProfileId)
+    {
+        return $this->_oDb->getContentInfoByProfileId((int)$iProfileId);
+    }
+
+    public function serviceGetMenuAddonManageTools()
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+		$iNumTotal = $this->_oDb->getEntriesNumByParams();
+
+        $iNum1 = $this->_oDb->getEntriesNumByParams([
+            [
+                'key' => 'status', 
+                'table' => 'sys_profiles',
+                'value' => BX_PROFILE_STATUS_PENDING, 
+                'operator' => '='
+            ]
+        ]);
+        
+        $iNum2 = 0;
+        if (isset($CNF['OBJECT_REPORTS'])){
+            $iNum2 = $this->_oDb->getEntriesNumByParams([
+                [
+                    'key' => 'reports',
+                    'value' => '0', 
+                    'operator' => '>'
+                ]
+            ]);
+        }
+        
+        return array('counter1_value' => $iNum1, 'counter2_value' => $iNum2, 'counter3_value' => $iNumTotal, 'counter1_caption' => _t('_sys_menu_dashboard_manage_tools_addon_counter1_caption_profile_default'));
+    }
+
+    public function serviceGetMenuAddonManageToolsProfileStats($iProfileId = 0)
+    {
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+
+        $aAccountInfo = BxDolProfileQuery::getInstance()->getAccountInfoByProfileId($iProfileId);
+        if(empty($aAccountInfo) || !is_array($aAccountInfo))
+            return 0;
+
+        bx_import('SearchResult', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'SearchResult';
+        $o = new $sClass();
+        $o->fillFilters([
+            'account_id' => $aAccountInfo['id'],
+            'perofileStatus' => ''
+        ]);
+        $o->unsetPaginate();
+
+        return $o->getNum();
+    }
+
+    public function serviceGetMenuAddonFavoritesProfileStats()
+    {
+    	bx_import('SearchResult', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'SearchResult';
+        $o = new $sClass('favorite', array('user' => bx_get_logged_profile_id()));
+        $o->unsetPaginate();
+
+        return $o->getNum();
+    }
+
+    public function serviceGetSubmenuObject ()
+    {
+        return $this->_oConfig->CNF['OBJECT_MENU_SUBMENU_VIEW_ENTRY'];
+    }
+
+    public function serviceGetMenuSetNameForMenuTrigger ($sMenuTriggerName)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if (isset($CNF['TRIGGER_MENU_PROFILE_VIEW_SUBMENU']) && $CNF['TRIGGER_MENU_PROFILE_VIEW_SUBMENU'] == $sMenuTriggerName)
+            return $CNF['OBJECT_MENU_SUBMENU_VIEW_ENTRY'];
+        elseif (isset($CNF['TRIGGER_MENU_PROFILE_SNIPPET_META']) && $CNF['TRIGGER_MENU_PROFILE_SNIPPET_META'] == $sMenuTriggerName)
+            return $CNF['OBJECT_MENU_SNIPPET_META'];
+        else if (isset($CNF['TRIGGER_MENU_PROFILE_VIEW_ACTIONS']) && $CNF['TRIGGER_MENU_PROFILE_VIEW_ACTIONS'] == $sMenuTriggerName) {
+            if(empty($CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY_ALL']))
+                return $CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY'];
+            else
+                return array($CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY'], $CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY_ALL']);
+        }
+
+        return '';
+    }
+
+    public function serviceGetSnippetMenuVars($iProfileId, $bPublic = null, $aParams = [])
+    {
+        return $this->_oTemplate->getSnippetMenuVars($iProfileId, $bPublic, $aParams);
+    }
+
+    public function serviceGetPageObjectForPageTrigger ($sPageTriggerName)
+    {
+        if (isset($this->_oConfig->CNF['TRIGGER_PAGE_VIEW_ENTRY']) && $this->_oConfig->CNF['TRIGGER_PAGE_VIEW_ENTRY'] == $sPageTriggerName)
+        	return $this->_oConfig->CNF['OBJECT_PAGE_VIEW_ENTRY'];
+
+        return '';
+    }
+
+    public function serviceProfilesSearch ($sTerm, $iLimit)
+    {
+		$aRet = array();
+
+        $a = $this->_oDb->searchByTerm($sTerm, $iLimit);
+        foreach ($a as $r) {
+            $oProfile = BxDolProfile::getInstance($r['profile_id']);
+
+            $aRet[] = array (
+            	'label' => $this->serviceProfileName($r['content_id']), 
+                'value' => $r['profile_id'], 
+                'url' => $oProfile->getUrl(),
+            	'thumb' => $oProfile->getThumb(),
+                'unit' => $oProfile->getUnit(0, ['template' => ['name' => 'unit_wo_info', 'size' => 'icon']])
+            );
+        }
+
+        return $aRet;
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_profile Base Profile
+     * @subsection bx_base_profile-general General
+     * @subsubsection bx_base_profile-profile_unit_safe profile_unit_safe
+     * 
+     * @code bx_srv('bx_persons', 'profile_unit_safe', [...]); @endcode
+     * 
+     * Get profile unit
+     * @param $iContentId content ID
+     * 
+     * @see BxBaseModProfileModule::serviceProfileUnitSafe
+     */
+    /** 
+     * @ref bx_base_profile-profile_unit_safe "profile_unit_safe"
+     */
+    public function serviceProfileUnitSafe ($iContentId, $aParams = array())
+    {
+        unset($aParams['check_private_content']);
+        return $this->serviceProfileUnit ($iContentId, $aParams);
+    }
+
+    public function serviceProfileUnit ($iContentId, $aParams = [])
+    {
+        $mixedContent = $this->_getContent($iContentId);
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        $bCheckPrivateContent = isset($aParams['check_private_content']) ? (bool)$aParams['check_private_content'] : true;
+
+        $aUnitTemplate = ['name' => 'unit.html', 'size' => false, 'vars' => []];
+        if(!empty($aParams['template'])) {
+            $mixedTemplate = $aParams['template'];
+            unset($aParams['template']);
+
+            if(is_string($mixedTemplate))
+                $aUnitTemplate['name'] = $mixedTemplate . '.html';
+            else if(is_array($mixedTemplate)) {
+                if(!empty($mixedTemplate['name']))
+                    $aUnitTemplate['name'] = $mixedTemplate['name'] . '.html';
+
+                if(!empty($mixedTemplate['size']))
+                    $aUnitTemplate['size'] = $mixedTemplate['size'];
+
+                if(!empty($mixedTemplate['vars']))
+                    $aUnitTemplate['vars'] = $mixedTemplate['vars'];
+            }
+        }
+
+        $aUnitParams = [];
+        if(!empty($aParams))
+            $aUnitParams = $aParams;
+
+        return $this->_oTemplate->unit($aContentInfo, $bCheckPrivateContent, array_values($aUnitTemplate), $aUnitParams);
+    }
+
+    public function serviceProfileUnitApi ($iContentId, $aParams = [])
+    {
+        $mixedContent = $this->_getContent($iContentId);
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        return $this->_oTemplate->unitAPI($aContentInfo, $aParams);
+    }
+
+    public function serviceHasImage ($iContentId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $mixedContent = $this->_getContent($iContentId, 'getContentInfoById');
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        return !empty($aContentInfo[$CNF['FIELD_PICTURE']]);
+    }
+
+    public function serviceProfilePicture ($iContentId)
+    {
+        return $this->_serviceTemplateFunc('urlPicture', $iContentId);
+    }
+
+    public function serviceProfileAvatar ($iContentId)
+    {
+        return $this->_serviceTemplateFunc('urlAvatar', $iContentId);
+    }
+
+    public function serviceProfileAvatarBig ($iContentId)
+    {
+        return $this->_serviceTemplateFunc('urlAvatarBig', $iContentId);
+    }
+
+    public function serviceProfileCover ($iContentId)
+    {
+        return $this->_serviceTemplateFunc('urlCover', $iContentId);
+    }
+
+    public function serviceProfileUnitCover ($iContentId)
+    {
+        return $this->_serviceTemplateFunc('urlCoverUnit', $iContentId);
+    }
+
+    public function serviceProfileEditUrl ($iContentId)
+    {
+        return bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $this->_oConfig->CNF['URI_EDIT_ENTRY'] . '&id=' . $iContentId));
+    }
+
+    public function serviceProfileThumb ($iContentId)
+    {
+        return $this->_serviceTemplateFunc('urlThumb', $iContentId);
+    }
+
+    public function serviceProfileIcon ($iContentId)
+    {
+        return $this->_serviceTemplateFunc('urlIcon', $iContentId);
+    }
+
+    public function serviceProfileName ($iContentId)
+    {
+        if (!$iContentId)
+            return false;
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if (!$aContentInfo)
+            return false;
+        
+        $sDisplayName = $this->getProfileName($aContentInfo);
+        bx_alert($this->getName(), 'profile_name', $aContentInfo['profile_id'], 0, array('info' => $aContentInfo, 'display_name' => &$sDisplayName));
+        
+        return $sDisplayName;
+    }
+    
+    public function serviceProfileSettings ($iContentId)
+    {
+        if (!$iContentId)
+            return false;
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if (!$aContentInfo)
+            return false;
+        
+       if (isset($aContentInfo['settings']))
+           return json_decode($aContentInfo['settings']);
+    }
+    
+    public function serviceProfileSettingsSet ($iContentId, $oValue)
+    {
+        $CNF = $this->_oConfig->CNF;
+        $this->_oDb->updateEntriesBy(['settings' => json_encode($oValue)], [$CNF['FIELD_ID'] => $iContentId]);
+    }
+  
+    public function serviceProfileCreateUrl ($bAbsolute = true)
+    {
+    	$CNF = $this->_oConfig->CNF;
+    	if(empty($CNF['URL_CREATE']))
+    		return false;
+
+    	return $bAbsolute ? bx_absolute_url(BxDolPermalinks::getInstance()->permalink($CNF['URL_CREATE'])) : $CNF['URL_CREATE'];
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_profile Base Profile
+     * @subsection bx_base_profile-general General
+     * @subsubsection bx_base_profile-profile_url profile_url
+     * 
+     * @code bx_srv('bx_persons', 'profile_url', [...]); @endcode
+     * 
+     * Get profile URL
+     * @param $iContentId content ID
+     * 
+     * @see BxBaseModProfileModule::serviceProfileUrl
+     */
+    /** 
+     * @ref bx_base_profile-profile_url "profile_url"
+     */
+    public function serviceProfileUrl ($iContentId)
+    {
+        if (!$iContentId)
+            return false;
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if (!$aContentInfo)
+            return false;
+        $CNF = $this->_oConfig->CNF;
+        return bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]));
+    }
+
+    /**
+     * @see iBxDolProfileService::serviceGetSpaceTitle
+     */ 
+    public function serviceGetSpaceTitle()
+    {
+        return _t('_sys_ps_space_title_friend');
+    }
+
+    /**
+     * @see iBxDolProfileService::serviceGetParticipatingProfiles
+     */ 
+    public function serviceGetParticipatingProfiles($iProfileId, $aConnectionObjects = false)
+    {
+        if (false === $aConnectionObjects)
+            $aConnectionObjects = array('sys_profiles_friends');
+
+        $a = array();
+        foreach ($aConnectionObjects as $sConnectionObject) {
+            if (!($o = BxDolConnection::getObjectInstance($sConnectionObject)))
+                continue;
+
+            if (BX_CONNECTIONS_TYPE_MUTUAL == $o->getType())
+                $a = array_merge($a, $o->getConnectedContentByType($iProfileId, $this->getName(), true));
+            else
+                $a = array_merge($a, $o->getConnectedContentByType($iProfileId, $this->getName()));
+        }
+        $aRet = array_unique($a);
+
+        // the check for an ability to post in profile has been moved to BxDolPrivacy because we need to have an extended check
+        // with an ability to check for extended roles taking into account the type of content which is going be posted/edited
+        // so now this service returns just a fans, not filtered by an ability to post into an entry's context
+        /*foreach ($a as $iConnectedProfileId) {
+            if (!($oConnectedProfile = BxDolProfile::getInstance($iConnectedProfileId)))
+                continue;
+
+             if (CHECK_ACTION_RESULT_ALLOWED === bx_srv($oConnectedProfile->getModule(), 'check_allowed_post_in_profile', array($oConnectedProfile->getContentId(), $iProfileId)))
+                $aRet[] = $iConnectedProfileId;
+        }*/
+
+        bx_alert('system', 'get_participating_profiles', $iProfileId, false, array(
+            'module' => $this->_oConfig->getName(),
+            'profiles' => &$aRet
+        ));
+
+        return $aRet;
+    }
+
+    /**
+     * Prepare fields from some universal set of fields to fields in particular profile module. 
+     * By default only 'name' and 'description' fields are supported.
+     * After fields convertion it can be used in @see BxBaseModGeneralModule::serviceEntityAdd
+     * @param $aFieldsProfile fields in soem universal format.
+     * @return array which is ready to use for particular module
+     */ 
+    public function servicePrepareFields ($aFieldsProfile)
+    {        
+        bx_alert($this->getName(), 'prepare_fields', 0, 0, array('fields_orig' => $aFieldsProfile, 'fields_result' => &$aFieldsProfile));
+
+        return $aFieldsProfile;
+    }
+
+    protected function _servicePrepareFields ($aFieldsProfile, $aFieldsDefault, $aMap)
+    {
+        $aFieldsOrig = $aFieldsProfile;
+
+        bx_import('BxDolPrivacy');
+        $aFieldsDefault2 = array(
+            'allow_view_to' => BX_DOL_PG_ALL,
+            'allow_contact_to' => BX_DOL_PG_ALL, 
+            'allow_post_to' => BX_DOL_PG_FRIENDS,
+        );
+        $aFieldsProfile = array_merge($aFieldsDefault2, $aFieldsDefault, $aFieldsProfile);
+
+        foreach ($aMap as $k => $v) {
+            if (isset($aFieldsProfile[$v]))
+                $aFieldsProfile[$k] = $aFieldsProfile[$v];
+            if ($k != $v && !isset($aMap[$v]))
+                unset($aFieldsProfile[$v]);
+        }
+        
+        bx_alert($this->getName(), 'prepare_fields', 0, 0, array('fields_orig' => $aFieldsOrig, 'fields_result' => &$aFieldsProfile));
+
+        return $aFieldsProfile;
+    }
+
+    public function serviceFormsHelper ()
+    {
+        return parent::serviceFormsHelper ();
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_profile Base Profile
+     * @subsection bx_base_profile-forms Forms
+     * @subsubsection bx_base_profile-entity_create entity_create
+     * 
+     * @code bx_srv('bx_persons', 'entity_create', [...]); @endcode
+     * 
+     * Get profile creation form
+     * @param $sDisplay optional form display name
+     * 
+     * @see BxBaseModProfileModule::serviceEntityCreate
+     */
+    /** 
+     * @ref bx_base_profile-entity_create "entity_create"
+     */
+    public function serviceEntityCreate ($sDisplay = false)
+    {
+	    BxDolInformer::getInstance($this->_oTemplate)->setEnabled(false);
+
+	    return parent::serviceEntityCreate ($sDisplay);
+    }
+
+    public function serviceActAsProfile ()
+    {
+        return true;
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_profile Base Profile
+     * @subsection bx_base_profile-browse Browse
+     * @subsubsection bx_base_profile-browse_recommended browse_recommended
+     * 
+     * @code bx_srv('bx_persons', 'browse_recommended', [...]); @endcode
+     * 
+     * Browse recommended profiles
+     * @param $sUnitView optional, unit view, for example - showcase
+     * @param $bEmptyMessage optional, display or not "empty" message when there is no content
+     * @param $bAjaxPaginate optional, use AJAX paginate or not
+     * 
+     * @see BxBaseModProfileModule::serviceBrowseRecommended
+     */
+    /** 
+     * @ref bx_base_profile-browse_recommended "browse_recommended"
+     */
+    public function serviceBrowseRecommended ($sUnitView = false, $bEmptyMessage = false, $bAjaxPaginate = true)
+    {
+        if (!isLogged())
+            return '';
+        return $this->_serviceBrowse ('recommended', $sUnitView ? array('unit_view' => $sUnitView) : false, BX_DB_PADDING_DEF, $bEmptyMessage, $bAjaxPaginate);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_profile Base Profile
+     * @subsection bx_base_profile-browse Browse
+     * @subsubsection bx_base_profile-browse_recent_profiles browse_recent_profiles
+     * 
+     * @code bx_srv('bx_persons', 'browse_recent_profiles', [...]); @endcode
+     * 
+     * Browse recently joined profiles
+     * @param $bEmptyMessage optional, display or not "empty" message when there is no content
+     * @param $bAjaxPaginate optional, use AJAX paginate or not
+     * 
+     * @see BxBaseModProfileModule::serviceBrowseRecentProfiles
+     */
+    /** 
+     * @ref bx_base_profile-browse_recent_profiles "browse_recent_profiles"
+     */
+    public function serviceBrowseRecentProfiles ($bDisplayEmptyMsg = false, $bAjaxPaginate = true)
+    {
+        return $this->_serviceBrowse ('recent', false, BX_DB_PADDING_DEF, $bDisplayEmptyMsg, $bAjaxPaginate);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_profile Base Profile
+     * @subsection bx_base_profile-browse Browse
+     * @subsubsection bx_base_profile-browse_active_profiles browse_active_profiles
+     * 
+     * @code bx_srv('bx_persons', 'browse_active_profiles', [...]); @endcode
+     * 
+     * Browse recently active profiles
+     * @param $bEmptyMessage optional, display or not "empty" message when there is no content
+     * @param $bAjaxPaginate optional, use AJAX paginate or not
+     * 
+     * @see BxBaseModProfileModule::serviceBrowseActiveProfiles
+     */
+    /** 
+     * @ref bx_base_profile-browse_active_profiles "browse_active_profiles"
+     */
+    public function serviceBrowseActiveProfiles ($sUnitView = false, $bEmptyMessage = false, $bAjaxPaginate = false)
+    {
+        return $this->_serviceBrowse ('active', $sUnitView ? array('unit_view' => $sUnitView) : false, BX_DB_PADDING_DEF, $bEmptyMessage, $bAjaxPaginate);
+    }
+    
+    /**
+     * @page service Service Calls
+     * @section bx_base_profile Base Profile
+     * @subsection bx_base_profile-browse Browse
+     * @subsubsection bx_base_profile-browse_familiar_profiles browse_familiar_profiles
+     * 
+     * @code bx_srv('bx_persons', 'browse_familiar_profiles', [...]); @endcode
+     * 
+     * Browse profiles you might be familiar with. By default using 'Profile Friends' connection.
+     * @param $bEmptyMessage optional, display or not "empty" message when there is no content
+     * @param $bAjaxPaginate optional, use AJAX paginate or not
+     * 
+     * @see BxBaseModProfileModule::serviceBrowseFamiliarProfiles
+     */
+    /** 
+     * @ref bx_base_profile-browse_familiar_profiles "browse_familiar_profiles"
+     */
+    public function serviceBrowseFamiliarProfiles ($sConnection = '', $sUnitView = false, $bEmptyMessage = false, $bAjaxPaginate = false)
+    {
+        $aParams = [
+            'object' => 'sys_profiles_friends',
+            'profile' => bx_get_logged_profile_id(),
+        ];
+
+        if(!empty($sConnection)) {
+            if(is_string($sConnection))
+                $aParams['object'] = $sConnection;
+            else if(is_array($sConnection))
+                $aParams = array_merge($aParams, $sConnection);
+        }
+
+        if($sUnitView)
+            $aParams['unit_view'] = $sUnitView;
+
+        return $this->_serviceBrowse ('familiar', $aParams, BX_DB_PADDING_DEF, $bEmptyMessage, $bAjaxPaginate);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_profile Base Profile
+     * @subsection bx_base_profile-browse Browse
+     * @subsubsection bx_base_profile-browse_top_profiles browse_top_profiles
+     * 
+     * @code bx_srv('bx_persons', 'browse_top_profiles', [...]); @endcode
+     * 
+     * Browse top profiles
+     * @param $bEmptyMessage optional, display or not "empty" message when there is no content
+     * @param $bAjaxPaginate optional, use AJAX paginate or not
+     * 
+     * @see BxBaseModProfileModule::serviceBrowseTopProfiles
+     */
+    /** 
+     * @ref bx_base_profile-browse_top_profiles "browse_top_profiles"
+     */
+    public function serviceBrowseTopProfiles ($bDisplayEmptyMsg = false, $bAjaxPaginate = false)
+    {
+        return $this->_serviceBrowse ('top', false, BX_DB_PADDING_DEF, $bDisplayEmptyMsg, $bAjaxPaginate);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_profile Base Profile
+     * @subsection bx_base_profile-browse Browse
+     * @subsubsection bx_base_profile-browse_online_profiles browse_online_profiles
+     * 
+     * @code bx_srv('bx_persons', 'browse_online_profiles', [...]); @endcode
+     * 
+     * Browse online profiles
+     * @param $bEmptyMessage optional, display or not "empty" message when there is no content
+     * @param $bAjaxPaginate optional, use AJAX paginate or not
+     * 
+     * @see BxBaseModProfileModule::serviceBrowseOnlineProfiles
+     */
+    /** 
+     * @ref bx_base_profile-browse_online_profiles "browse_online_profiles"
+     */
+    public function serviceBrowseOnlineProfiles ($bDisplayEmptyMsg = false, $bAjaxPaginate = false)
+    {
+        return $this->_serviceBrowse ('online', false, BX_DB_PADDING_DEF, $bDisplayEmptyMsg, $bAjaxPaginate);
+    }
+
+    public function serviceBrowseConnections ($iProfileId, $sObjectConnections = 'sys_profiles_friends', $sConnectionsType = 'content', $iMutual = false, $iDesignBox = BX_DB_PADDING_DEF, $iProfileId2 = 0)
+    {
+        return $this->_serviceBrowse (
+            'connections', [
+                'object' => $sObjectConnections,
+                'type' => $sConnectionsType,
+                'mutual' => $iMutual,
+                'profile' => (int)$iProfileId,
+                'profile2' => (int)$iProfileId2
+            ], $iDesignBox
+        );
+    }
+
+    public function serviceBrowseRelationsQuick ($iProfileId, $sObjectConnections = 'sys_profiles_friends', $sConnectionsType = 'content', $iMutual = false, $iProfileId2 = 0)
+    {
+        // get connections object
+        $oConnection = BxDolConnection::getObjectInstance($sObjectConnections);
+        if (!$oConnection)
+            return '';       
+
+        // set some vars
+        $iStart = (int)bx_get('start');
+        $iLimit = empty($this->_oConfig->CNF['PARAM_NUM_CONNECTIONS_QUICK']) ? 4 : getParam($this->_oConfig->CNF['PARAM_NUM_CONNECTIONS_QUICK']);
+        if (!$iLimit)
+            $iLimit = 4;
+
+        // get connections array
+        bx_import('BxDolConnection');
+        $aConnections = $oConnection->getConnectionsAsArrayExt($sConnectionsType, $iProfileId, $iProfileId2, $iMutual, $iStart, $iLimit + 1, BX_CONNECTIONS_ORDER_ADDED_DESC);
+        if(empty($aConnections) || !is_array($aConnections))
+            return '';
+
+        $aResult = array();
+        foreach($aConnections as $iProfile => $aConnection)
+            $aResult[] = array(
+                'id' => $iProfile,
+                'info' => array(
+                    'addon' => $oConnection->getRelationTranslation($aConnection['relation'])
+                )
+            );
+
+        return $this->_serviceBrowseQuick($aResult, $iStart, $iLimit);
+    }
+
+    public function serviceBrowseConnectionsQuick ($iProfileId, $sObjectConnections = 'sys_profiles_friends', $sConnectionsType = 'content', $iMutual = false, $iProfileId2 = 0)
+    {
+        // get connections object
+        $oConnection = BxDolConnection::getObjectInstance($sObjectConnections);
+        if (!$oConnection)
+            return '';
+
+        // set some vars
+        $iLimit = empty($this->_oConfig->CNF['PARAM_NUM_CONNECTIONS_QUICK']) ? 4 : getParam($this->_oConfig->CNF['PARAM_NUM_CONNECTIONS_QUICK']);
+        if (!$iLimit)
+            $iLimit = 4;
+        $iStart = (int)bx_get('start');
+
+        // get connections array
+        bx_import('BxDolConnection');
+        $a = $oConnection->getConnectionsAsArray ($sConnectionsType, $iProfileId, $iProfileId2, $iMutual, (int)bx_get('start'), $iLimit + 1, BX_CONNECTIONS_ORDER_ADDED_DESC);
+        if (!$a)
+            return '';
+
+        return $this->_serviceBrowseQuick($a, $iStart, $iLimit);
+    }
+
+    public function serviceBrowseConnectionsByType ($aParamsCnn, $aParamsBrs = array())
+    {
+        $sAll = 'all';
+
+        $aParamsCnn['object'] = !empty($aParamsCnn['object']) ? $aParamsCnn['object'] : 'sys_profiles_friends';
+        $aParamsCnn['type'] = !empty($aParamsCnn['type']) ? $aParamsCnn['type'] : 'content';
+        $aParamsCnn['profile_id2'] = !empty($aParamsCnn['profile_id2']) ? (int)$aParamsCnn['profile_id2'] : 0;
+        $aParamsCnn['mutual'] = isset($aParamsCnn['mutual']) ? $aParamsCnn['mutual'] : false;
+
+        $oConnection = BxDolConnection::getObjectInstance($aParamsCnn['object']);
+        if(!$oConnection)
+            return '';
+
+        $aSQLParts = $oConnection->getConnectionsAsSQLParts ($aParamsCnn['type'], 'sys_profiles', 'id', $aParamsCnn['profile_id'], $aParamsCnn['profile_id2'], $aParamsCnn['mutual']);
+
+        $sType = !empty($aParamsBrs['type']) ? $aParamsBrs['type'] : $sAll;
+        if(bx_get('type') !== false)
+            $sType = bx_process_input(bx_get('type'));
+
+        $iStart = (int)bx_get('start');
+
+        $iLimitDefault = 4;
+        $iLimit = !empty($this->_oConfig->CNF['PARAM_NUM_CONNECTIONS_QUICK']) ? getParam($this->_oConfig->CNF['PARAM_NUM_CONNECTIONS_QUICK']) : $iLimitDefault;
+        if(!empty($aParamsBrs['per_page']))
+            $iLimit = (int)$aParamsBrs['per_page'];
+        if(!$iLimit)
+            $iLimit = $iLimitDefault;
+
+        $aProfiles = BxDolProfileQuery::getInstance()->getConnectedProfilesByType($aSQLParts, ($sType != $sAll ? $sType : ''), $iStart, $iLimit + 1);
+
+        $mixedMenu = '';
+        if(!isset($aParamsBrs['filter_menu']) || $aParamsBrs['filter_menu'] === true) {
+            $aModules = array(
+                array('name' => $sAll)
+            );
+            $aModules = array_merge($aModules, BxDolModuleQuery::getInstance()->getModulesBy(array('type' => 'modules', 'active' => 1)));
+
+            $aMenuItems = array();
+            foreach($aModules as $aModule)
+                if($aModule['name'] == $sAll || BxDolRequest::serviceExists($aModule['name'], 'act_as_profile'))
+                    $aMenuItems[] = array(
+                    	'id' => $aModule['name'], 
+                    	'name' => $aModule['name'], 
+                    	'class' => '', 
+                    	'link' => 'javascript:void(0)', 
+                    	'onclick' => "return !loadDynamicBlockAutoPaginate(this, " . $iStart . ", " . $iLimit . ", " . bx_js_string(json_encode(array('type' => $aModule['name']))) . ");", 
+                    	'target' => '_self', 
+                    	'title' => _t('_' . $aModule['name']), 
+                    	'active' => 1
+                    );
+    
+            if(!empty($aMenuItems)) {
+                $mixedMenu = new BxTemplMenu(array('template' => 'menu_vertical.html', 'menu_id'=> $this->_oConfig->getName() . '-connections-by-type', 'menu_items' => $aMenuItems));
+                $mixedMenu->setSelected('', $sType);
+            }
+        }
+
+        return array(
+            'menu' => $mixedMenu,
+            'content' => $this->_serviceBrowseQuick(array_keys($aProfiles), $iStart, $iLimit, array('type' => $sType))
+        );
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_profile Base Profile
+     * @subsection bx_base_profile-browse Browse
+     * @subsubsection bx_base_profile-browse_by_acl browse_by_acl
+     * 
+     * @code bx_srv('bx_persons', 'browse_by_acl', [...]); @endcode
+     * 
+     * Browse profiles by membership(ACL) level
+     * @param $mixedLevelId membership level, or array of membership levels
+     * @param $iDesignBox optional, design box identifier
+     * 
+     * @see BxBaseModProfileModule::serviceBrowseByAcl
+     */
+    /** 
+     * @ref bx_base_profile-browse_by_acl "browse_by_acl"
+     */
+	public function serviceBrowseByAcl ($mixedLevelId, $iDesignBox = BX_DB_PADDING_DEF)
+    {
+        return $this->_serviceBrowse (
+            'acl',
+            array(
+                'level' => $mixedLevelId,
+			),
+            $iDesignBox,
+            true
+        );
+    }
+
+    public function serviceEntityEditCover ($iContentId = 0)
+    {
+        return $this->_serviceEntityForm ('editDataForm', $iContentId, $this->_oConfig->CNF['OBJECT_FORM_ENTRY_DISPLAY_EDIT_COVER']);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_profile Base Profile
+     * @subsection bx_base_profile-page_blocks Page Blocks
+     * @subsubsection bx_base_profile-entity_cover entity_cover
+     * 
+     * @code bx_srv('bx_persons', 'entity_cover', [...]); @endcode
+     * 
+     * Get cover
+     * @param $iContentId content ID
+     * 
+     * @see BxBaseModProfileModule::serviceEntityCover
+     */
+    /** 
+     * @ref bx_base_general-entity_cover "entity_cover"
+     */
+    public function serviceEntityCover ($iContentId = 0, $aParams = [])
+    {
+        $mixedContent = $this->_getContent($iContentId);
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+        if(bx_is_api()){
+            $aContentInfo['cover'] = $this->serviceGetCover($iContentId);
+            $aContentInfo['image'] = $this->serviceGetThumb($iContentId);
+            
+            $CNF = $this->_oConfig->CNF;
+            if(!empty($CNF['OBJECT_MENU_VIEW_ENTRY_META'])){
+                $oMetaMenu = BxTemplMenu::getObjectInstance($CNF['OBJECT_MENU_VIEW_ENTRY_META']);
+                $aContentInfo['meta_menu'] =  $oMetaMenu->getCodeAPI();
+            }
+            if(!empty($CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY'])){
+                $oActionMenu = BxTemplMenu::getObjectInstance($CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY_ALL']);
+                 $aContentInfo['actions_menu'] = $oActionMenu->getCodeAPI();
+            }  
+            $oProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->MODULE);
+            $aContentInfo['profile'] = BxDolProfile::getData($this->_aProfileInfo['id'], [
+                'get_avatar' => 'getAvatarBig',
+                'with_info' => false
+            ]);
+            
+            return [bx_api_get_block('entity_cover', $aContentInfo)];
+        }
+        
+        return $this->_oTemplate->getBlockCover($aContentInfo, $aParams);
+    }
+    
+	/**
+     * Entry comments
+     */
+    public function serviceEntityCommentsByProfile ($iProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if(empty($CNF['OBJECT_COMMENTS']))
+            return '';
+
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if(!$oProfile)
+            return '';
+
+        return $this->_entityComments($CNF['OBJECT_COMMENTS'], $oProfile->getContentId());
+    }
+
+    /**
+     * Entry social sharing block
+     */
+    public function serviceEntitySocialSharing ($mixedContent = false, $aParams = array())
+    {
+        if(!empty($mixedContent)) {
+            if(!is_array($mixedContent))
+               $mixedContent = array((int)$mixedContent, array());
+        }
+        else {
+            $mixedContent = $this->_getContent();
+            if($mixedContent === false)
+                return false;
+        }
+
+        list($iContentId, $aContentInfo) = $mixedContent;    
+
+        return parent::serviceEntitySocialSharing(array($iContentId, $aContentInfo), array(
+            'id_thumb' => !empty($CNF['FIELD_PICTURE']) && !empty($aContentInfo[$CNF['FIELD_PICTURE']]) ? $aContentInfo[$CNF['FIELD_PICTURE']] : 0, 
+        ));
+    }
+
+    public function serviceProfileMembership ($iContentId = 0)
+    {
+    	$mixedContent = $this->_getContent($iContentId);
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+		return BxDolAcl::getInstance()->getProfileMembership($aContentInfo['profile_id']);
+    }
+
+    public function serviceProfileFriends ($iContentId = 0)
+    {
+        $mixedContent = $this->_getContent($iContentId);
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        bx_import('BxDolConnection');
+        $s = $this->serviceBrowseConnectionsQuick ($aContentInfo['profile_id'], 'sys_profiles_friends', BX_CONNECTIONS_CONTENT_TYPE_CONTENT, true);
+        if (!$s)
+            return MsgBox(_t('_sys_txt_empty'));
+
+        return $s;
+    }
+
+    public function serviceProfileFriendsMutual ($iContentId = 0)
+    {
+        $mixedContent = $this->_getContent($iContentId);
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        $iViewer = bx_get_logged_profile_id();
+        $iOwner = (int)$aContentInfo['profile_id'];
+        if($iViewer == $iOwner)
+            return false;
+
+        bx_import('BxDolConnection');
+        $s = $this->serviceBrowseConnectionsQuick ($iOwner, 'sys_profiles_friends', BX_CONNECTIONS_CONTENT_TYPE_COMMON, true, $iViewer);
+        if(!$s)
+            return MsgBox(_t('_sys_txt_empty'));
+
+        return $s;
+    }
+
+    public function serviceProfileSubscriptions ($iContentId = 0, $aParams = array())
+    {
+        $mixedContent = $this->_getContent($iContentId);
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        bx_import('BxDolConnection');
+        $aResult = $this->serviceBrowseConnectionsByType(array(
+        	'profile_id' => $aContentInfo['profile_id'],  
+        	'object' => 'sys_profiles_subscriptions', 
+            'type' => BX_CONNECTIONS_CONTENT_TYPE_CONTENT
+        ), array(
+            'type' => isset($aParams['type']) ? $aParams['type'] : '',
+            'filter_menu' => isset($aParams['filter_menu']) ? $aParams['filter_menu'] : true,
+        ));
+        if(empty($aResult['content']))
+            $aResult['content'] = MsgBox(_t('_sys_txt_empty'));
+
+        return $aResult;
+    }
+
+    public function serviceProfileSubscribedMe ($iContentId = 0)
+    {
+        $mixedContent = $this->_getContent($iContentId);
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        bx_import('BxDolConnection');
+        $s = $this->serviceBrowseConnectionsQuick ($aContentInfo['profile_id'], 'sys_profiles_subscriptions', BX_CONNECTIONS_CONTENT_TYPE_INITIATORS);
+        if (!$s)
+            return MsgBox(_t('_sys_txt_empty'));
+
+        return $s;
+    }
+
+    public function serviceProfileRelations ($iContentId = 0, $aParams = array())
+    {
+        if(!BxDolRelation::isEnabled())
+            return false;
+
+        $mixedContent = $this->_getContent($iContentId);
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        bx_import('BxDolConnection');
+        $s = $this->serviceBrowseRelationsQuick ($aContentInfo['profile_id'], 'sys_profiles_relations', BX_CONNECTIONS_CONTENT_TYPE_CONTENT, 1);
+        if (!$s)
+            return MsgBox(_t('_sys_txt_empty'));
+
+        return $s;
+    }
+
+    public function serviceProfileRelatedMe ($iContentId = 0)
+    {
+        if(!BxDolRelation::isEnabled())
+            return false;
+
+        $mixedContent = $this->_getContent($iContentId);
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        bx_import('BxDolConnection');
+        $s = $this->serviceBrowseRelationsQuick ($aContentInfo['profile_id'], 'sys_profiles_relations', BX_CONNECTIONS_CONTENT_TYPE_INITIATORS, 1);
+        if (!$s)
+            return MsgBox(_t('_sys_txt_empty'));
+
+        return $s;
+    }
+
+    /**
+     * check enabled profile activation letter
+     */
+    public function  serviceIsEnableProfileActivationLetter()
+    {
+        $CNF = &$this->_oConfig->CNF;
+        return isset($CNF['PARAM_ENABLE_ACTIVATION_LETTER']) ? (bool)getParam($CNF['PARAM_ENABLE_ACTIVATION_LETTER']) : true;
+    }
+
+    public function serviceIsEnableRelations()
+    {
+        if(!BxDolRelation::isEnabled())
+            return false;
+
+        $sModule = $this->_oConfig->getName();
+        $oRelations = BxDolConnection::getObjectInstance('sys_profiles_relations');
+        return $oRelations->isRelationAvailableWithProfile($sModule) || $oRelations->isRelationAvailableFromProfile($sModule);
+    }
+
+    /**
+     * For internal usage only.
+     */
+    public function serviceDeleteEntityService ($iContentId, $bDeleteWithContent = false)
+    {
+        return parent::serviceDeleteEntity ($iContentId, 'deleteDataService');
+    }
+
+	/**
+     * Data for Notifications module
+     */
+    public function serviceGetNotificationsData()
+    {
+        $a = parent::serviceGetNotificationsData();
+
+        $sModule = $this->_aModule['name'];
+        
+        $a['handlers'][] = array('group' => $sModule . '_timeline_post_common', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'timeline_post_common', 'module_name' => $sModule, 'module_method' => 'get_notifications_timeline_post_common', 'module_class' => 'Module');
+
+        $a['settings'][] = array('group' => 'timeline_post', 'unit' => $sModule, 'action' => 'timeline_post_common', 'types' => array('follow_member'));
+
+        $a['alerts'][] = array('unit' => $sModule, 'action' => 'timeline_post_common');
+
+        return $a;
+    }
+
+    public function serviceGetNotificationsPost($aEvent)
+    {
+        $aResult = parent::serviceGetNotificationsVote($aEvent);
+        if(empty($aResult) || !is_array($aResult) || !$this->serviceActAsProfile())
+            return $aResult;
+
+        $oProfile = BxDolProfile::getInstanceByContentAndType((int)$aEvent['object_id'], $this->_oConfig->getName());
+        if($oProfile !== false)
+            $aResult['entry_author'] = $oProfile->id();
+
+        return $aResult;
+    }
+
+    public function serviceGetNotificationsVote($aEvent)
+    {
+        $aResult = parent::serviceGetNotificationsVote($aEvent);
+        if(empty($aResult) || !is_array($aResult) || !$this->serviceActAsProfile())
+            return $aResult;
+
+        $oProfile = BxDolProfile::getInstanceByContentAndType((int)$aEvent['object_id'], $this->_oConfig->getName());
+        if($oProfile !== false)
+            $aResult['entry_author'] = $oProfile->id();
+
+        return $aResult;
+    }
+
+    /**
+     * Notification about new member requst in the group
+     */
+    public function serviceGetNotificationsTimelinePostCommon($aEvent)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $iContentId = (int)$aEvent['object_id'];
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType((int)$iContentId, $this->getName());
+        if(!$oGroupProfile)
+            return [];
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return [];
+            
+        $aSubcontentInfo = BxDolService::call('bx_timeline', 'get_info', array((int)$aEvent['subobject_id'], false));
+        if(empty($aSubcontentInfo) || !is_array($aSubcontentInfo))
+            return [];
+
+        $sEntrySummary = isset($aContentInfo[$CNF['FIELD_TEXT']]) ? $aContentInfo[$CNF['FIELD_TEXT']] : '';
+
+        $sSubentryUrl = bx_absolute_url(str_replace(BX_DOL_URL_ROOT, '', BxDolService::call('bx_timeline', 'get_link', array((int)$aEvent['subobject_id']))), '{bx_url_root}');
+        $sSubentrySample = $aSubcontentInfo['title'];
+        if(empty($sSubentrySample))
+            $sSubentrySample = strmaxtextlen($aSubcontentInfo['description'], 20, '...');
+        $sSubentrySummary = $aSubcontentInfo['description'];
+
+        return [
+            'entry_sample' => $CNF['T']['txt_sample_single'],
+            'entry_url' => bx_absolute_url(str_replace(BX_DOL_URL_ROOT, '', $oGroupProfile->getUrl()), '{bx_url_root}'),
+            'entry_caption' => $oGroupProfile->getDisplayName(),
+            'entry_summary' => $sEntrySummary,
+            'entry_author' => $oGroupProfile->id(),
+            'subentry_sample' => $sSubentrySample,
+            'subentry_url' => $sSubentryUrl,
+            'subentry_summary' => $sSubentrySummary,
+            'lang_key' => $CNF['T']['txt_ntfs_timeline_post_common'],
+        ];
+    }
+
+    /**
+     * Data for Timeline module
+     */
+    public function serviceGetTimelineData()
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $sModule = $this->_aModule['name'];
+
+        $aAlerts = array();
+        $aHandlers = array();
+        if(!empty($CNF['FIELD_PICTURE'])) {
+            $aAlerts = array_merge($aAlerts, array(
+                array('unit' => $sModule, 'action' => 'profile_picture_changed'),
+                array('unit' => $sModule, 'action' => 'profile_picture_deleted')
+            ));
+            $aHandlers = array_merge($aHandlers, array(
+                array('group' => $sModule . '_profile_picture', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'profile_picture_changed', 'module_name' => $sModule, 'module_method' => 'get_timeline_profile_picture', 'module_class' => 'Module',  'groupable' => 0, 'group_by' => ''),
+                array('group' => $sModule . '_profile_picture', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'profile_picture_deleted')
+            ));
+        }
+
+        if(!empty($CNF['FIELD_COVER'])) {
+            $aAlerts = array_merge($aAlerts, array(
+                array('unit' => $sModule, 'action' => 'profile_cover_changed'),
+                array('unit' => $sModule, 'action' => 'profile_cover_deleted')
+            ));
+            $aHandlers = array_merge($aHandlers, array(
+                array('group' => $sModule . '_profile_cover', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'profile_cover_changed', 'module_name' => $sModule, 'module_method' => 'get_timeline_profile_cover', 'module_class' => 'Module',  'groupable' => 0, 'group_by' => ''),
+                array('group' => $sModule . '_profile_cover', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'profile_cover_deleted')
+            ));
+        }
+
+    	$aResult = array();
+        if(!empty($aAlerts) && !empty($aHandlers))
+            $aResult = array(
+            	'handlers' => $aHandlers,
+            	'alerts' => $aAlerts
+            );
+
+        return $aResult;
+    }
+
+    /**
+     * Entry post for Timeline module
+     */
+    public function serviceGetTimelineProfilePicture($aEvent, $aBrowseParams = [])
+    {
+        $aResult = $this->_serviceGetTimelineProfileImage($aEvent, $aBrowseParams, [
+            'stg' => 'OBJECT_STORAGE',
+            'trans' => ['OBJECT_IMAGES_TRANSCODER_GALLERY', 'OBJECT_IMAGES_TRANSCODER_AVATAR'],
+            'trans_orig' => ['OBJECT_IMAGES_TRANSCODER_PICTURE', 'OBJECT_IMAGES_TRANSCODER_GALLERY'],
+            'txt_ss' => 'txt_sample_pp_single',
+            'txt_sswa' => 'txt_sample_pp_single_with_article',
+            'txt_sa' => 'txt_sample_pi_action',
+            'txt_sau' => 'txt_sample_pi_action_user'
+        ]);
+
+        if($aResult !== false)
+            $aResult['allowed_view'] = ['module' => $this->_oConfig->getName(), 'method' => 'get_timeline_profile_picture_allowed_view'];
+
+        return $aResult;
+    }
+
+    public function serviceGetTimelineProfilePictureAllowedView($aEvent)
+    {
+        return $this->_serviceGetTimelineProfileImageAllowedView($aEvent);
+    }
+
+    public function serviceGetTimelineProfileCover($aEvent, $aBrowseParams = [])
+    {
+        $aResult = $this->_serviceGetTimelineProfileImage($aEvent, $aBrowseParams, [
+            'stg' => 'OBJECT_STORAGE_COVER',
+            'trans' => ['OBJECT_IMAGES_TRANSCODER_GALLERY', 'OBJECT_IMAGES_TRANSCODER_COVER_THUMB'],
+            'trans_orig' => ['OBJECT_IMAGES_TRANSCODER_COVER', 'OBJECT_IMAGES_TRANSCODER_GALLERY'],
+            'txt_ss' => 'txt_sample_pc_single',
+            'txt_sswa' => 'txt_sample_pc_single_with_article',
+            'txt_sa' => 'txt_sample_pi_action',
+            'txt_sau' => 'txt_sample_pi_action_user'
+        ]);
+
+        if($aResult !== false)
+            $aResult['allowed_view'] = ['module' => $this->_oConfig->getName(), 'method' => 'get_timeline_profile_cover_allowed_view'];
+
+        return $aResult;
+    }
+    
+    public function serviceGetTimelineProfileCoverAllowedView($aEvent)
+    {
+        return $this->_serviceGetTimelineProfileImageAllowedView($aEvent);
+    }
+
+    public function serviceGetMenuItemTitleByConnection($sConnection, $sAction, $iContentProfileId, $iInitiatorProfileId = 0)
+    {
+        return $this->getMenuItemTitleByConnection($sConnection, $sAction, $iContentProfileId, $iInitiatorProfileId);
+    }
+
+
+    // ====== PERMISSION METHODS
+    /**
+     * @see iBxDolProfileService::serviceCheckAllowedProfileView
+     */ 
+    public function serviceCheckAllowedProfileView($iContentId)
+    {        
+        return $this->serviceCheckAllowedWithContent('View', $iContentId);
+    }
+
+    /**
+     * @see iBxDolProfileService::serviceCheckAllowedProfileContact
+     */ 
+    public function serviceCheckAllowedProfileContact($iContentId)
+    {        
+        return $this->serviceCheckAllowedWithContent('Contact', $iContentId);
+    }
+
+    /**
+     * @see iBxDolProfileService::serviceCheckAllowedPostInProfile
+     */
+    public function serviceCheckAllowedPostInProfile($iContentId, $sPostModule = '')
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!empty($CNF['FIELD_ALLOW_VIEW_TO']) && getParam('sys_treat_cxt_in_cxt_as_cnt') == 'on') {
+            $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+            if($aContentInfo && isset($aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]) && (int)$aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']] < 0)
+                return _t('_sys_txt_access_denied');
+        }
+
+        // for groups based profiles we do have a Role permissions which have a higher priority than the site-wide permissions.
+        if (method_exists($this, 'isAllowedModuleActionByProfile')) {
+            $bResult = $this->isAllowedModuleActionByProfile($iContentId, $sPostModule, 'post');
+            // if a profile is having a role and a role is having permissions set then it overrides the site-wide setting.
+            if ($bResult !== NULL) return $bResult;
+        }
+
+        return $this->serviceCheckAllowedWithContent('Post', $iContentId);
+    }
+
+    /**
+     * @see iBxDolProfileService::serviceCheckAllowedModuleActionInProfile
+     */
+    public function serviceCheckAllowedModuleActionInProfile($iContentId, $sPostModule, $sActionName)
+    {
+        if (method_exists($this, 'isAllowedModuleActionByProfile')) {
+            $bResult = $this->isAllowedModuleActionByProfile($iContentId, $sPostModule, $sActionName);
+            if ($bResult !== NULL) return $bResult;
+        }
+
+        return _t('_Access denied');
+    }
+
+    /**
+     * @see iBxDolProfileService::serviceCheckSpacePrivacy
+     */ 
+    public function serviceCheckSpacePrivacy($iContentId)
+    {
+        return $this->serviceCheckAllowedProfileView($iContentId);
+    }
+
+    /**
+     * Check if the profile can be viewed.
+     * 
+     * NOTE. This service should be used if it's needed to pass some specific values in 
+     * $isPerformAction and $iProfileId parameters, otherwise it's recommended to use 
+     * BxBaseModProfileModule::serviceCheckAllowedProfileView service method or 
+     * BxDolProfile::checkAllowedProfileView method.
+     * 
+     * @param type $aDataEntry - entry which the action will be performed for
+     * @param type $isPerformAction - perform or just check the action
+     * @param type $iProfileId - performer's profile ID
+     * @return integer - one of CHECK_ACTION_RESULT_XXX constants.
+     */
+    public function serviceCheckAllowedViewForProfile ($aDataEntry, $isPerformAction = false, $iProfileId = false)
+    {
+        if (!$iProfileId)
+            $iProfileId = $this->_iProfileId;
+
+        if(empty($aDataEntry) || !is_array($aDataEntry))
+            return _t('_sys_txt_not_found');
+
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->getName());
+        if ($oProfile && $oProfile->id() == $iProfileId)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return parent::serviceCheckAllowedViewForProfile ($aDataEntry, $isPerformAction, $iProfileId);
+    }
+
+    /**
+     * Check if posting (comment, post in Timeline) is available.
+     * 
+     * NOTE. This service should be used if it's needed to pass some specific values in 
+     * $isPerformAction and $iProfileId parameters, otherwise it's recommended to use 
+     * BxBaseModProfileModule::serviceCheckAllowedPostInProfile service method or 
+     * BxDolProfile::checkAllowedPostInProfile method.
+     * 
+     * @param type $aDataEntry - entry which the action will be performed for
+     * @param type $isPerformAction - perform or just check the action
+     * @param type $iProfileId - performer's profile ID
+     * @return integer - one of CHECK_ACTION_RESULT_XXX constants.
+     */
+    public function serviceCheckAllowedPostForProfile ($aDataEntry, $isPerformAction = false, $iProfileId = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$iProfileId)
+            $iProfileId = $this->_iProfileId;
+
+        // check is view allowed
+        if(($mixedResult = $this->serviceCheckAllowedViewForProfile($aDataEntry, $isPerformAction, $iProfileId)) !== CHECK_ACTION_RESULT_ALLOWED)
+            return $mixedResult;
+
+        // moderator and owner always have access
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$CNF['FIELD_ID']], $this->getName());
+        if(($oProfile && $oProfile->id() == $iProfileId) || $this->_isModerator($isPerformAction))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // check privacy
+        if(!empty($CNF['OBJECT_PRIVACY_POST'])) {
+            $oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_POST']);
+            if($oPrivacy && !$oPrivacy->check($aDataEntry[$CNF['FIELD_ID']], $iProfileId))
+                return _t('_sys_access_denied_to_private_content');
+        }
+
+        // check alert to allow custom checks
+        $mixedResult = null;
+        bx_alert('system', 'check_allowed_post', 0, 0, array('module' => $this->getName(), 'content_info' => $aDataEntry, 'profile_id' => $iProfileId, 'override_result' => &$mixedResult));
+        if($mixedResult !== null)
+            return $mixedResult;
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * Check if profile can be contacted.
+     * 
+     * NOTE. This service should be used if it's needed to pass some specific values in 
+     * $isPerformAction and $iProfileId parameters, otherwise it's recommended to use 
+     * BxBaseModProfileModule::serviceCheckAllowedProfileContact service method or 
+     * BxDolProfile::checkAllowedProfileContact method.
+     * 
+     * @param type $aDataEntry - entry which the action will be performed for
+     * @param type $isPerformAction - perform or just check the action
+     * @param type $iProfileId - performer's profile ID
+     * @return integer - one of CHECK_ACTION_RESULT_XXX constants.
+     */
+    public function serviceCheckAllowedContactForProfile ($aDataEntry, $isPerformAction = false, $iProfileId = false)
+    {
+        $mixedResult = $this->_serviceCheckAllowedContactForProfile($aDataEntry, $isPerformAction, $iProfileId);
+
+        // call alert to allow custom checks
+        bx_alert('system', 'check_allowed_contact', 0, 0, array(
+            'module' => $this->getName(), 
+            'content_info' => $aDataEntry, 
+            'profile_id' => $iProfileId, 
+            'override_result' => &$mixedResult
+        ));
+
+        return $mixedResult;
+    }
+
+    public function serviceSetViewProfileCover($oPage, $aProfileInfo)
+    {
+        $this->_oTemplate->setCover($oPage,$aProfileInfo);
+    }
+    
+    /** 
+     * Returns list of members by mode with limited number of records for React Jot
+     * @param string $sMode
+     * @param int $iStart
+     * @param int $iPerPage
+     * @return mixed
+     */
+    public function serviceGetMembers($sMode = 'active', $iStart = 0, $iPerPage = 10){
+        bx_import('SearchResult', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'SearchResult';
+        $o = new $sClass($sMode);
+
+        $o -> aCurrent['paginate'] = array('perPage' => $iPerPage, 'forceStart' => $iStart);
+        return $o -> getSearchData();
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedAdd ($isPerformAction = false)
+    {
+        $oAccount = BxDolAccount::getInstance();
+        if (!$oAccount || ($this->serviceActAsProfile() && $oAccount->isProfilesLimitReached()))
+            return _t('_sys_txt_access_denied');
+        return parent::checkAllowedAdd ($isPerformAction);
+    }   
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedView ($aDataEntry, $isPerformAction = false)
+    {
+        return $this->serviceCheckAllowedViewForProfile ($aDataEntry, $isPerformAction);
+    }
+    
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedViewProfileImage ($aDataEntry, $isPerformAction = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        // check privacy
+        if (empty($CNF['OBJECT_PRIVACY_VIEW'])) 
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        $oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW']);
+        if ($oPrivacy && !$oPrivacy->check($aDataEntry[$CNF['FIELD_ID']]) && !$oPrivacy->isPartiallyVisible($aDataEntry[$CNF['FIELD_ALLOW_VIEW_TO']]))
+            return _t('_sys_access_denied_to_private_content');
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedViewCoverImage ($aDataEntry, $isPerformAction = false)
+    {
+        return $this->checkAllowedViewProfileImage($aDataEntry);
+    }
+    
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedPost ($aDataEntry, $isPerformAction = false)
+    {
+        return $this->serviceCheckAllowedPostForProfile ($aDataEntry, $isPerformAction);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedContact ($aDataEntry, $isPerformAction = false)
+    {
+        return $this->serviceCheckAllowedContactForProfile ($aDataEntry, $isPerformAction);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedEdit ($aDataEntry, $isPerformAction = false)
+    {
+        if(empty($aDataEntry) || !is_array($aDataEntry))
+            return _t('_sys_txt_not_found');
+
+        // moderator always has access
+        if ($this->_isModerator($isPerformAction))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // owner (checked by account instead of author!) always have access
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->_aModule['name']);
+        if (!$oProfile)
+            return _t('_sys_txt_error_occured');
+
+        if ($oProfile->id() == $this->_iProfileId || $oProfile->getAccountId() == $this->_iAccountId)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * Check if user can change cover image
+     */
+    public function checkAllowedChangeCover ($aDataEntry, $isPerformAction = false)
+    {
+        // moderator always has access
+        if ($this->_isModerator($isPerformAction))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // owner (checked by account instead of author!) always have access
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->_aModule['name']);
+        if (!$oProfile)
+            return _t('_sys_txt_error_occured');
+
+        if ($oProfile->id() == $this->_iProfileId || $oProfile->getAccountId() == $this->_iAccountId)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedDelete (&$aDataEntry, $isPerformAction = false)
+    {
+        // moderator always has access
+        if ($this->_isModerator($isPerformAction))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // owner (checked by account instead of author!) always have access
+        $aCheck = checkActionModule($this->_iProfileId, 'delete entry', $this->getName(), $isPerformAction);
+
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->_aModule['name'], $isPerformAction);
+        if (!$oProfile)
+            return _t('_sys_txt_error_occured');
+
+        if (($oProfile->id() == $this->_iProfileId || $oProfile->getAccountId() == $this->_iAccountId) && $aCheck[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedViewMoreMenu (&$aDataEntry, $isPerformAction = false)
+    {
+        $oMenu = BxTemplMenu::getObjectInstance($this->_oConfig->CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY_MORE']);
+        if (!$oMenu || !$oMenu->getCode())
+            return _t('_sys_txt_access_denied');
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * Compose differs from Contact. For example, a user should NOT be able to start conversation (compose) 
+     * with himself but he should be able to view conversation page where he is one of the participants.
+     * 
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedCompose(&$aDataEntry, $isPerformAction = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$CNF['FIELD_ID']], $this->getName());
+        if($oProfile && $oProfile->id() == $this->_iProfileId)
+            return _t('_sys_txt_access_denied');
+
+        return $this->checkAllowedContact($aDataEntry, $isPerformAction);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedFriendAdd (&$aDataEntry, $isPerformAction = false)
+    {
+        return $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, 'sys_profiles_friends', false, false);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedFriendRemove (&$aDataEntry, $isPerformAction = false)
+    {
+        if (CHECK_ACTION_RESULT_ALLOWED === $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, 'sys_profiles_friends', false, true, true))
+            return CHECK_ACTION_RESULT_ALLOWED;
+        return $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, 'sys_profiles_friends', false, true, false);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedRelationAdd (&$aDataEntry, $isPerformAction = false)
+    {
+        $sResult = _t('_sys_txt_access_denied');
+        if(!BxDolRelation::isEnabled())
+            return $sResult;
+
+        if(($sMsg = $this->checkAllowedView($aDataEntry)) !== CHECK_ACTION_RESULT_ALLOWED)
+            return $sMsg;
+
+        return $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, 'sys_profiles_relations', false, false);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedRelationRemove (&$aDataEntry, $isPerformAction = false)
+    {
+        $sResult = _t('_sys_txt_access_denied');
+        if(!BxDolRelation::isEnabled())
+            return $sResult;
+
+        return $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, 'sys_profiles_relations', false, true);
+    }
+
+    public function checkAllowedRelationsView (&$aDataEntry, $isPerformAction = false)
+    {
+        $sResult = _t('_sys_txt_access_denied');
+        if(!BxDolRelation::isEnabled())
+            return $sResult;
+
+        $sModule = $this->_oConfig->getName();
+        $oRelations = BxDolConnection::getObjectInstance('sys_profiles_relations');
+        if(!$oRelations->isRelationAvailableWithProfile($sModule) && !$oRelations->isRelationAvailableFromProfile($sModule))
+            return $sResult;
+
+        if(empty($aDataEntry) || !is_array($aDataEntry))
+            return $sResult;
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedSubscribeAdd(&$aDataEntry, $isPerformAction = false)
+    {
+        $mixedResult = $this->_modProfileCheckAllowedSubscribeAdd($aDataEntry, $isPerformAction);
+
+        // call alert to allow custom checks
+        bx_alert('system', 'check_allowed_subscribe_add', 0, 0, array(
+            'module' => $this->getName(), 
+            'content_info' => $aDataEntry, 
+            'profile_id' => bx_get_logged_profile_id(), 
+            'override_result' => &$mixedResult
+        ));
+
+        return $mixedResult;
+    }
+
+    /**
+     * Note. Is mainly needed for internal usage. Access level is 'public' to allow outer calls from alerts.
+     */
+    public function _modProfileCheckAllowedSubscribeAdd(&$aDataEntry, $isPerformAction = false)
+    {
+        if(($sMsg = $this->checkAllowedView($aDataEntry)) !== CHECK_ACTION_RESULT_ALLOWED)
+            return $sMsg;
+
+        return $this->_checkAllowedConnect($aDataEntry, $isPerformAction, 'sys_profiles_subscriptions', false, false);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedSubscribeRemove (&$aDataEntry, $isPerformAction = false)
+    {
+        return $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, 'sys_profiles_subscriptions', false, true);
+    }
+
+    public function checkAllowedSubscriptionsView (&$aDataEntry, $isPerformAction = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sResult = _t('_sys_txt_access_denied');
+        if(empty($aDataEntry) || !is_array($aDataEntry))
+            return $sResult;
+
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$CNF['FIELD_ID']], $this->_aModule['name']);
+        if(!$oProfile || ($oProfile->id() != $this->_iProfileId && $this->_oDb->getParam($CNF['PARAM_PUBLIC_SBSN']) != 'on' && $this->_oDb->getParam($CNF['PARAM_PUBLIC_SBSD']) != 'on'))
+            return $sResult;
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    public function checkMyself($iContentId)
+    {
+		$iLogged = (int)bx_get_logged_profile_id();
+    	if(empty($iLogged))
+    		return false;
+
+    	$oProfile = BxDolProfile::getInstanceByContentAndType((int)$iContentId, $this->_oConfig->getName());
+    	if(!$oProfile)
+    		return false;
+
+		return $oProfile->id() == $iLogged;
+    }
+    
+    public function isAllowDeleteOrDisable($iActorProfileId, $iTargetProfileId)
+    {
+        if (BxDolAcl::getInstance()->isMemberLevelInSet(array(MEMBERSHIP_ID_MODERATOR), $iActorProfileId) && BxDolAcl::getInstance()->isMemberLevelInSet(array(MEMBERSHIP_ID_MODERATOR, MEMBERSHIP_ID_ADMINISTRATOR), $iTargetProfileId))
+            return false;
+        
+        return true;    
+    }
+    
+    public function getProfileObject($iContentId)
+    {
+    	$oProfile = BxDolProfile::getInstanceByContentAndType((int)$iContentId, $this->_oConfig->getName());
+        if (!$oProfile) 
+            $oProfile = BxDolProfileUndefined::getInstance();
+
+        return $oProfile;
+    }
+
+    protected function _serviceCheckAllowedContactForProfile($aDataEntry, $isPerformAction = false, $iProfileId = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$iProfileId)
+            $iProfileId = $this->_iProfileId;
+
+        /*
+         * allow to 'contact myself'. It's needed to allow a user to view conversation page 
+         * where he is one of the participants.
+         */
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$CNF['FIELD_ID']], $this->getName());
+        if($oProfile && $oProfile->id() == $iProfileId)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // moderator always has access
+        if($this->_isModeratorForProfile($isPerformAction, $iProfileId))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // check privacy
+        if(!empty($CNF['OBJECT_PRIVACY_CONTACT'])) {
+            $oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_CONTACT']);
+            if($oPrivacy && !$oPrivacy->check($aDataEntry[$CNF['FIELD_ID']], $iProfileId))
+                return _t('_sys_access_denied_to_private_content');
+        }
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    // ====== COMMON METHODS
+    public function onUpdateImage($iContentId, $sFiledName, $sFiledValue, $iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sModule = $this->getName();
+
+        if(!$iProfileId && ($oProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $sModule)) !== false)
+            $iProfileId = $oProfile->id();
+
+        $aField2Method = [
+            $CNF['FIELD_PICTURE'] => 'picture',
+            $CNF['FIELD_COVER'] => 'cover',
+        ];
+
+        if(!empty($aField2Method[$sFiledName]))
+            bx_alert($sModule, 'profile_' . $aField2Method[$sFiledName] . '_changed', $sFiledValue, $iProfileId, [
+                'object_author_id' => $iProfileId, 
+                'content' => $iContentId, 
+                'field' => $sFiledName
+            ]);
+    }
+
+    protected function _alertParams($aContentInfo)
+    {
+        $aParams = parent::_alertParams($aContentInfo);
+
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!empty($CNF['FIELD_STATUS']) && isset($aContentInfo[$CNF['FIELD_STATUS']]))
+            $aParams['status'] = $aContentInfo[$CNF['FIELD_STATUS']];
+
+        if(!empty($CNF['FIELD_STATUS_ADMIN']) && isset($aContentInfo[$CNF['FIELD_STATUS_ADMIN']]))
+            $aParams['status_admin'] = $aContentInfo[$CNF['FIELD_STATUS_ADMIN']];
+
+        if(!empty($CNF['FIELD_ALLOW_VIEW_TO']) && isset($aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]))
+            $aParams['privacy_view'] = $aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']];
+
+        return $aParams;
+    }
+
+    public function alertAfterAdd($aContentInfo)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $iContentId = (int)$aContentInfo[$CNF['FIELD_ID']];
+
+        $aParams = $this->_alertParams($aContentInfo);
+        bx_alert($this->getName(), 'added', $iContentId, false, $aParams);
+    }
+
+    public function alertAfterEdit($aContentInfo)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sModule = $this->getName();
+        $iContentId = (int)$aContentInfo[$CNF['FIELD_ID']];
+
+        $aParams = $this->_alertParams($aContentInfo);
+        bx_alert($sModule, 'edited', $iContentId, false, $aParams);
+
+        $oProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $sModule);
+        bx_alert('profile', 'edit', $oProfile->id(), 0, array('content' => $iContentId, 'module' => $sModule));
+    }
+
+    public function getProfileByCurrentUrl ()
+    {
+        $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        
+        if ($iProfileId)
+            return  BxDolProfile::getInstance($iProfileId);
+
+        $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if ($iContentId)
+            return BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName());
+        
+        return false;
+    }
+    
+    public function getEntryImageData($aContentInfo, $sField = 'FIELD_PICTURE', $aTranscoders = array())
+    {
+        return parent::getEntryImageData($aContentInfo, $sField, $aTranscoders);
+    }
+    
+    public function getProfileName ($aContentInfo)
+    {
+        return bx_process_output($aContentInfo[$this->_oConfig->CNF['FIELD_NAME']]);
+    }
+    
+    public function getMenuItemTitleByConnection($sConnection, $sAction, $iContentProfileId, $iInitiatorProfileId = 0)
+    {
+        $oConnection = BxDolConnection::getObjectInstance($sConnection);
+        if(!$oConnection)
+            return false;
+        
+        if(!$iInitiatorProfileId)
+            $iInitiatorProfileId = bx_get_logged_profile_id();
+
+        $aResult = [];
+        switch($sConnection) {
+            case 'sys_profiles_friends':
+                $aResult = $this->_getMenuItemTitleProfilesFriends($sAction, $iContentProfileId, $iInitiatorProfileId, $oConnection);
+                break;
+
+            case 'sys_profiles_subscriptions':
+                $aResult = $this->_getMenuItemTitleProfilesSubscriptions($sAction, $iContentProfileId, $iInitiatorProfileId, $oConnection);
+                break;
+        }
+
+        return $aResult;
+    }
+
+    protected function _getMenuItemTitleProfilesFriends($sAction, $iContentProfileId, $iInitiatorProfileId, &$oConnection)
+    {
+        $CNF = $this->_oConfig->getCNF();
+
+        $aResult = [];
+        if($oConnection->isConnectedNotMutual($iInitiatorProfileId, $iContentProfileId))
+            $aResult = [
+                'add' => _t(!empty($CNF['T']['menu_item_title_befriend_sent']) ? $CNF['T']['menu_item_title_befriend_sent'] : '_sys_menu_item_title_sm_befriend_sent'),
+                'remove' => _t(!empty($CNF['T']['menu_item_title_unfriend_cancel']) ? $CNF['T']['menu_item_title_unfriend_cancel'] : '_sys_menu_item_title_sm_unfriend_cancel'),
+            ];
+        else if($oConnection->isConnectedNotMutual($iContentProfileId, $iInitiatorProfileId))
+            $aResult = [
+                'add' => _t(!empty($CNF['T']['menu_item_title_befriend_confirm']) ? $CNF['T']['menu_item_title_befriend_confirm'] : '_sys_menu_item_title_sm_befriend_confirm'),
+                'remove' => _t(!empty($CNF['T']['menu_item_title_unfriend_reject']) ? $CNF['T']['menu_item_title_unfriend_reject'] : '_sys_menu_item_title_sm_unfriend_reject'),
+            ];
+        else if($oConnection->isConnected($iInitiatorProfileId, $iContentProfileId, true))
+            $aResult = [
+                'add' => '',
+                'remove' => _t(!empty($CNF['T']['menu_item_title_unfriend']) ? $CNF['T']['menu_item_title_unfriend'] : '_sys_menu_item_title_sm_unfriend'),
+            ];
+        else
+            $aResult = [
+                'add' => _t(!empty($CNF['T']['menu_item_title_befriend']) ? $CNF['T']['menu_item_title_befriend'] : '_sys_menu_item_title_sm_befriend'),
+                'remove' => '',
+            ];
+
+        return !empty($sAction) && isset($aResult[$sAction]) ? $aResult[$sAction] : $aResult;
+    }
+
+    protected function _getMenuItemTitleProfilesSubscriptions($sAction, $iContentProfileId, $iInitiatorProfileId, &$oConnection)
+    {
+        $CNF = $this->_oConfig->getCNF();
+
+        $aResult = [];
+        if($oConnection->isConnected($iInitiatorProfileId, $iContentProfileId))
+            $aResult = [
+                'add' => '',
+                'remove' => _t(!empty($CNF['T']['menu_item_title_unsubscribe']) ? $CNF['T']['menu_item_title_unsubscribe'] : '_sys_menu_item_title_sm_unsubscribe'),
+            ];
+        else
+            $aResult = [
+                'add' => _t(!empty($CNF['T']['menu_item_title_subscribe']) ? $CNF['T']['menu_item_title_subscribe'] : '_sys_menu_item_title_sm_subscribe'),
+                'remove' => '',
+            ];
+
+        return !empty($sAction) && !empty($aResult[$sAction]) ? $aResult[$sAction] : $aResult;
+    }
+    
+    public function decodeDataAPI($aData, $aParams = [])
+    {
+        $CNF = $this->_oConfig->CNF;
+
+        $sModule = $this->getName();
+        $iId = (int)$aData[$CNF['FIELD_ID']];
+        $oProfile = BxDolProfile::getInstanceByContentAndType($iId, $sModule);
+
+        $aResult = [
+            'id' => $iId, 
+            'module' => $sModule,
+            'module_title' => _t($CNF['T']['txt_sample_single']),
+            'added' => $aData[$CNF['FIELD_ADDED']],
+            'author' => $aData[$CNF['FIELD_AUTHOR']],
+            'author_data' => $oProfile !== false ? BxDolProfile::getData($oProfile->id()) : '',
+            'title' => $aData[$CNF['FIELD_TITLE']],
+            $CNF['FIELD_TITLE'] => $aData[$CNF['FIELD_TITLE']],
+            'url' => bx_api_get_relative_url($this->serviceGetLink($iId)),
+            'image' => $this->serviceGetThumb($iId),
+            'cover' => $this->serviceGetCover($iId),
+        ];
+
+        if(isset($aParams['template']) && $aParams['template'] == 'unit_wo_info')
+            return $aResult;
+
+        if(isset($aParams['extended']) && $aParams['extended'] === true)
+            $aResult['text'] = $aData[$CNF['FIELD_TEXT']];
+
+        if(getParam('sys_api_conn_in_prof_units') == 'on' && $oProfile !== false) {
+            $iProfileId = $oProfile->id();
+            $aResult['title'] = $oProfile->getDisplayName();
+            if(($oConnection = BxDolConnection::getObjectInstance('sys_profiles_friends')) !== false && isset($CNF['URI_VIEW_FRIENDS'])) {
+                $aResult['friends_count'] = $oConnection->getConnectedContentCount($iProfileId, true);
+                $aResult['friends_list'] = $oConnection->getConnectedListAPI($iProfileId, true, BX_CONNECTIONS_CONTENT_TYPE_CONTENT);
+                if(isLogged()) {
+                    $iLoggedId = bx_get_logged_profile_id();
+                    $aResult['mutual_friends_count'] = $oConnection->getCommonContentCount($iProfileId, $iLoggedId, true);
+                    $aResult['mutual_friends_list'] = $oConnection->getCommonListAPI($iProfileId, $iLoggedId, true);
+                }
+            }
+
+            if(($oConnection = BxDolConnection::getObjectInstance('sys_profiles_subscriptions')) !== false){
+                $aResult['followers_count'] = $oConnection->getConnectedInitiatorsCount($iProfileId);
+                $aResult['followers_list'] = $oConnection->getConnectedListAPI($iProfileId, false, BX_CONNECTIONS_CONTENT_TYPE_INITIATORS);
+            }
+        }
+
+        $sKey = 'OBJECT_MENU_SNIPPET_META';
+        if(!empty($CNF[$sKey]) && ($oMetaMenu = BxDolMenu::getObjectInstance($CNF[$sKey], $this->_oTemplate)) !== false) {
+            $oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW']);
+            $bPublic = !$oPrivacy || $oPrivacy->check($iId) || $oPrivacy->isPartiallyVisible($aData[$CNF['FIELD_ALLOW_VIEW_TO']]);
+
+            $oMetaMenu->setContentId($iId);
+            $oMetaMenu->setContentPublic($bPublic);
+            if(isset($aParams['context']))
+                $oMetaMenu->setContext($aParams['context']);
+
+            $aResult['meta'] = $oMetaMenu->getCodeAPI();
+        }
+
+        return $aResult;
+    }
+
+
+    // ====== PROTECTED METHODS
+    protected function _checkAllowedConnect (&$aDataEntry, $isPerformAction, $sObjConnection, $isMutual, $isInvertResult, $isSwap = false)
+    {
+        if (!$this->_iProfileId)
+            return _t('_sys_txt_access_denied');
+
+        $CNF = &$this->_oConfig->CNF;
+
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$CNF['FIELD_ID']], $this->_aModule['name']);
+        if (!$oProfile || $oProfile->id() == $this->_iProfileId)
+            return _t('_sys_txt_access_denied');
+
+        return BxDolConnection::getObjectInstance($sObjConnection)->checkAllowedConnect ($this->_iProfileId, $oProfile->id(), $isPerformAction, $isMutual, $isInvertResult, $isSwap);
+    }
+
+    protected function _buildRssParams($sMode, $aArgs)
+    {
+        $aParams = array ();
+        $sMode = bx_process_input($sMode);
+        switch ($sMode) {
+            case 'connections':
+                $aParams = array(
+                    'object' => isset($aArgs[0]) ? $aArgs[0] : '',
+                    'type' => isset($aArgs[1]) ? $aArgs[1] : '',
+                    'profile' => isset($aArgs[2]) ? (int)$aArgs[2] : 0,
+                    'mutual' => isset($aArgs[3]) ? (int)$aArgs[3] : 0,
+                    'profile2' => isset($aArgs[4]) ? (int)$aArgs[4] : 0,
+                );
+                break;
+        }
+
+        return $aParams;
+    }
+
+    protected function _serviceGetTimelineProfileImage($aEvent, $aBrowseParams, $aBuildParams)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aFileInfo = BxDolStorage::getObjectInstance($CNF[$aBuildParams['stg']])->getFile((int)$aEvent['object_id']);
+        if(empty($aFileInfo) || !is_array($aFileInfo))
+            return false;
+            
+        $aEventContent = unserialize($aEvent['content']);
+        if(!is_array($aEventContent) || empty($aEventContent['content']))
+            return false;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($aEventContent['content']);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return false;
+
+        $oProfile = BxDolProfile::getInstanceMagic($aContentInfo['profile_id']);
+        if (!$oProfile->isActive())
+            return false;
+            
+        $sUserName = $oProfile->getDisplayName();
+
+        $sSample = isset($CNF['T'][$aBuildParams['txt_sswa']]) ? $CNF['T'][$aBuildParams['txt_sswa']] : $CNF['T'][$aBuildParams['txt_ss']];
+
+        //--- Title & Description
+        $sTitle = !empty($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : '';
+        if(empty($sTitle) && !empty($aContentInfo[$CNF['FIELD_TEXT']]))
+            $sTitle = $aContentInfo[$CNF['FIELD_TEXT']];
+
+        $sDescription = _t($CNF['T'][$aBuildParams['txt_sau']], $sUserName, _t($sSample));
+
+        return array(
+            'owner_id' => $aEvent['owner_id'],
+            'object_owner_id' => $aContentInfo['profile_id'],
+            'icon' => !empty($CNF['ICON']) ? $CNF['ICON'] : '',
+            'sample' => $sSample,
+            'sample_wo_article' => $CNF['T'][$aBuildParams['txt_ss']],
+            'sample_action' => isset($CNF['T'][$aBuildParams['txt_sa']]) ? $CNF['T'][$aBuildParams['txt_sa']] : '',
+            'url' => bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']])),
+            'content' => $this->_getContentForTimelineProfileImage($aEvent, $aBrowseParams, $aBuildParams, $aContentInfo, $aFileInfo), //a string to display or array to parse default template before displaying.
+            'date' => $aContentInfo[$CNF['FIELD_ADDED']],
+            'views' => '',
+            'votes' => '',
+            'scores' => '',
+            'reports' => '',
+            'comments' => '',
+            'title' => $sTitle, //may be empty.
+            'description' => $sDescription //may be empty.
+        );
+    }
+
+    protected function _serviceGetTimelineProfileImageAllowedView($aEvent)
+    {
+        $sError = _t('_sys_access_denied_to_private_content');
+
+        if(empty($aEvent['content']) || !is_array($aEvent['content']) || empty($aEvent['content']['id']))
+            return $sError;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($aEvent['content']['id']);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return $sError;
+
+        return $this->serviceCheckAllowedViewForProfile($aContentInfo);
+    }
+
+    protected function _getContentForTimelineProfileImage($aEvent, $aBrowseParams, $aBuildParams, $aContentInfo, $aFileInfo)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	$sUrl = bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]));
+
+    	//--- Image(s)
+        $sImage = $this->_oConfig->getImageUrl($aFileInfo['id'], $aBuildParams['trans']);
+        $sImageOrig = $this->_oConfig->getImageUrl($aFileInfo['id'], $aBuildParams['trans_orig']);
+        if(!empty($sImage)) {
+            if(empty($sImageOrig))
+                $sImageOrig = $sImage;
+
+            $a = ['id' => $aFileInfo['id'], 'url' => $sUrl, 'src' => $sImage];
+            if(CHECK_ACTION_RESULT_ALLOWED === $this->checkAllowedView($aContentInfo)) {
+                $a['src_orig'] = $sImageOrig;
+
+                $sCnfKey = 'OBJECT_IMAGES_TRANSCODER_GALLERY';
+                if(!empty($CNF[$sCnfKey]) && ($sSrcMedium = $this->_oConfig->getImageUrl($aFileInfo['id'], [$sCnfKey])) != '')
+                    $a['src_medium'] = $sSrcMedium;
+            }
+            $aImages = [$a];
+        }
+
+    	return array(
+            'sample' => isset($CNF['T'][$aBuildParams['txt_sswa']]) ? $CNF['T'][$aBuildParams['txt_sswa']] : $CNF['T'][$aBuildParams['txt_ss']],
+            'sample_wo_article' => $CNF['T'][$aBuildParams['txt_ss']],
+            'sample_action' => isset($CNF['T'][$aBuildParams['txt_sa']]) ? $CNF['T'][$aBuildParams['txt_sa']] : '',
+            'id' => $aContentInfo[$CNF['FIELD_ID']],
+            'url' => $sUrl,
+            'title' =>  '',
+            'text' => '',
+            'images' => $aImages,
+            'videos' => array()
+        );
+    }
+
+    protected function _entityComments($sObject, $iId = 0)
+    {
+        if(!$iId)
+            $iId = bx_process_input(bx_get('id'), BX_DATA_INT);
+
+        if(!$iId)
+            $iId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+
+        if(!$iId)
+            return false;
+
+        $oCmts = BxDolCmts::getObjectInstance($sObject, $iId);
+        if(!$oCmts || !$oCmts->isEnabled())
+            return false;
+
+        return $oCmts->getCommentsBlock(array(), array('in_designbox' => false));
+    }
+
+    protected function _getContent($iContentId = 0, $sFuncGetContent = 'getContentInfoById')
+    {
+        if(!$iContentId && bx_get('id') === false && bx_get('profile_id') !== false) {
+            $oProfile = BxDolProfile::getInstance((int)bx_get('profile_id'));
+            if($oProfile)
+                $iContentId = $oProfile->getContentId();
+        }
+
+        return parent::_getContent($iContentId, $sFuncGetContent);
+    }
+}
+
+/** @} */

+ 159 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/profile/classes/BxBaseModProfilePageEntry.php

@@ -0,0 +1,159 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseProfile Base classes for profile modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Profile create/edit/delete pages.
+ */
+class BxBaseModProfilePageEntry extends BxBaseModGeneralPageEntry
+{
+    protected $_aProfileInfo;
+    protected $_oProfile;
+
+    public function __construct($aObject, $oTemplate = false)
+    {
+        parent::__construct($aObject, $oTemplate);
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // get profile info
+        $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        $iContentId = 0;
+        if ('mine' == bx_get('id')) {
+            $o = BxDolProfile::getInstance();
+            if ($o && $iContentId = $o->getContentId())
+                $_GET['id'] = $_REQUEST['id'] = $iContentId;
+        } else {
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        }
+
+        if ($iProfileId)
+            $this->_oProfile = BxDolProfile::getInstance($iProfileId);
+
+        if (!$this->_oProfile && $iContentId)
+            $this->_oProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->MODULE);
+
+        if ($this->_oProfile) {
+            $this->_aProfileInfo = $this->_oProfile->getInfo();
+            $this->_aContentInfo = $this->_oModule->_oDb->getContentInfoById($this->_aProfileInfo['content_id']);
+        }
+
+        if (!$this->_isAvailablePage($this->_aObject) || !$this->_oProfile) {
+            $this->setPageCover(false);
+            return;
+        }
+
+        $bLoggedOwner = $this->_oProfile->id() == bx_get_logged_profile_id();
+        $bLoggedModerator = $this->_oModule->checkAllowedEditAnyEntry() === CHECK_ACTION_RESULT_ALLOWED;
+        if(!$this->_oProfile->isActive() && !$bLoggedOwner && !$bLoggedModerator) {
+            $this->setPageCover(false);
+            return;
+        }
+
+        // select view profile submenu
+        $oMenuSubmenu = BxDolMenu::getObjectInstance('sys_site_submenu');
+        if($oMenuSubmenu)
+            $oMenuSubmenu->setObjectSubmenu($CNF['OBJECT_MENU_SUBMENU_VIEW_ENTRY'], array (
+                'title' => $this->_oProfile->getDisplayName(),
+                'link' => $this->_oProfile->getUrl(),
+                'icon' => $CNF['ICON'],
+            ));
+
+        // add replaceable markers
+        $this->addMarkers($this->_aContentInfo);
+        $this->addMarkers($this->_aProfileInfo); // every content field can be used as marker
+        $this->addMarkers(array('profile_id' => $this->_oProfile->id())); // profile id field
+        $this->addMarkers(array('display_name' => $this->_oProfile->getDisplayName())); // profile display name
+        $this->addMarkers(array('profile_link' => $this->_oProfile->getUrl())); // profile link
+
+        $aInformers = array ();
+        $oInformer = BxDolInformer::getInstance($this->_oTemplate);
+        if($oInformer) {
+            // display message to profile author if profile isn't active
+            if ($bLoggedOwner && !empty($CNF['INFORMERS']['status'])) {
+                $sStatus = $this->_aContentInfo['profile_status'];
+
+                $aInformer = $CNF['INFORMERS']['status'];
+                if (isset($aInformer['map'][$sStatus]))
+                    $aInformers[] = array ('name' => $aInformer['name'], 'msg' => _t($aInformer['map'][$sStatus]), 'type' => BX_INFORMER_ALERT);
+            }
+
+            // display message to moderator/administrator if profile isn't active
+            if (!$bLoggedOwner && $bLoggedModerator && !empty($CNF['INFORMERS']['status_moderation'])) {
+                $sStatus = $this->_aContentInfo['profile_status'];
+                $sManageUrl = '#';
+                if(!empty($CNF['FIELD_TITLE']) && !empty($CNF['URL_MANAGE_ADMINISTRATION']))
+                    $sManageUrl = bx_absolute_url(BxDolPermalinks::getInstance()->permalink($CNF['URL_MANAGE_ADMINISTRATION'], array('filter' => urlencode($this->_aContentInfo[$CNF['FIELD_TITLE']]))));
+
+                $aInformer = $CNF['INFORMERS']['status_moderation'];
+                if (isset($aInformer['map'][$sStatus]))
+                    $aInformers[] = array ('name' => $aInformer['name'], 'msg' => _t($aInformer['map'][$sStatus], $sManageUrl), 'type' => BX_INFORMER_ALERT);
+            }
+
+            // add informers
+            if ($aInformers)
+                foreach ($aInformers as $a)
+                    $oInformer->add($a['name'], $this->_replaceMarkers($a['msg']), $a['type']);
+        }
+
+        // display message if it is possible to switch to this profile
+        if ($this->_oModule->serviceActAsProfile()) {
+            $oProfile = $this->_aContentInfo ? BxDolProfile::getInstanceByContentAndType($this->_aContentInfo['id'], $this->MODULE) : false;
+            if ($oProfile)
+                $oProfile->checkSwitchToProfile($this->_oTemplate);
+        }
+
+        // set cover
+        $this->_oModule->_oTemplate->setCover($this, $this->_aContentInfo);
+    }
+
+    public function getCode ()
+    {
+        // check if profile is active
+        if (!$this->_oProfile || (!$this->_oProfile->isActive() && $this->_oProfile->id() != bx_get_logged_profile_id() && $this->_oModule->checkAllowedEditAnyEntry() !== CHECK_ACTION_RESULT_ALLOWED)) {
+            $this->_oTemplate->displayPageNotFound();
+            exit;
+        }
+
+        $this->_oTemplate->addCss('main.css');
+
+        return parent::getCode();
+    }
+
+    protected function _isVisiblePage ($a)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        bx_import('BxDolPrivacy');
+
+        if(($mixedAllowView = $this->_oModule->checkAllowedView($this->_aContentInfo)) !== CHECK_ACTION_RESULT_ALLOWED) {
+            /*
+             * If visible by privacy, replace current page with different set of blocks.
+             */
+            if(!in_array($this->_aContentInfo['allow_view_to'], [BX_DOL_PG_HIDDEN, BX_DOL_PG_MEONLY]) && $this->_sObject == $CNF['OBJECT_PAGE_VIEW_ENTRY']) {
+                $aObject = BxDolPageQuery::getPageObject($CNF['OBJECT_PAGE_VIEW_ENTRY_CLOSED']);
+                $this->_sObject = $aObject['object'];
+                $this->_aObject = $aObject;
+                $this->_oQuery = new BxDolPageQuery($this->_aObject);
+            }
+            else 
+                return $mixedAllowView;
+        }
+
+        if(!BxDolPage::_isVisiblePage($a))
+            return false;
+        
+        $this->_oModule->checkAllowedView($this->_aContentInfo, true);
+        
+        return true;
+    }
+}
+
+/** @} */

+ 140 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/text/classes/BxBaseModTextConfig.php

@@ -0,0 +1,140 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseText Base classes for text modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+bx_import('BxDolInformer');
+
+class BxBaseModTextConfig extends BxBaseModGeneralConfig
+{
+    /**
+     * Determine whether Timeline post will use common content (header image + text)
+     * or content received from attachments (header image + text + images + videos + polls).
+     * By default the first variant is used. 
+     * Note. The variable can be removed in future, when all Text Based modules 
+     * implement 'attachments' related parameters. 
+     */
+    protected $_bAttachmentsInTimeline;
+    
+    protected $_aPregPatterns;
+
+    function __construct($aModule)
+    {
+        parent::__construct($aModule);
+
+        $this->CNF = array(
+            // modules icon
+            'ICON_POLLS_ANSWERS' => 'vote-yea',
+            'ICON_POLLS_RESULTS' => 'poll-h',
+
+            // database tables
+            'TABLE_POLLS' => $aModule['db_prefix'] . 'polls',
+            'TABLE_POLLS_ANSWERS' => $aModule['db_prefix'] . 'polls_answers',
+            'TABLE_POLLS_ANSWERS_VOTES' => $aModule['db_prefix'] . 'polls_answers_votes',
+            'TABLE_POLLS_ANSWERS_VOTES_TRACK' => $aModule['db_prefix'] . 'polls_answers_votes_track',
+            
+            'TABLE_LINKS' => $aModule['db_prefix'] . 'links',
+            'TABLE_LINKS2CONTENT' => $aModule['db_prefix'] . 'links2content',
+
+            // database fields
+            'FIELD_POLL_ID' => 'id',
+            'FIELD_POLL_AUTHOR_ID' => 'author_id',
+            'FIELD_POLL_CONTENT_ID' => 'content_id',
+            'FIELD_POLL_TEXT' => 'text',
+            'FIELD_POLL_ANSWERS' => 'answers',
+            
+            'FIELD_ATTACH_LINK_CONTROLS' => 'controls',
+
+            // some params
+            'PARAM_POLL_ENABLED' => true,
+            'PARAM_POLL_HIDDEN_RESULTS' => false,
+            'PARAM_POLL_ANONYMOUS_VOTING' => true,
+            'PARAM_MULTICAT_ENABLED' => false,
+            'PARAM_LINKS_ENABLED' => false,
+            'PARAM_LINKS_LIMIT' => 'sys_attach_links_max',
+
+            // objects
+            'OBJECT_VOTES_POLL_ANSWERS' => $this->_sName . '_poll_answers',
+            'OBJECT_FORM_ATTACH_LINK' => $this->_sName . '_attach_link',
+            'OBJECT_FORM_DISPLAY_ATTACH_LINK_ADD' => $this->_sName . '_attach_link_add',
+            
+
+            // styles
+            'STYLES_POLLS_EMBED_CLASS' => 'body.bx-page-iframe.bx-def-color-bg-page',
+            'STYLES_POLLS_EMBED_CONTENT' => array(
+                'background-color' => '#ffffff'
+            ),
+        );
+
+        $sPrefix = str_replace('_', '-', $this->_sName);
+        $this->_aPrefixes = [
+            'style' => $sPrefix,
+        ];
+
+        $this->_aJsClasses = array(
+            'poll' => $this->_sClassPrefix . 'Polls',
+            'links' => $this->_sClassPrefix . 'Links',
+            'categories' => $this->_sClassPrefix . 'Categories'
+        );
+
+        $this->_aJsObjects = array(
+            'poll' => 'o' . $this->_sClassPrefix . 'Polls',
+            'links' => 'o' . $this->_sClassPrefix . 'Links',
+            'categories' => 'o' . $this->_sClassPrefix . 'Categories'
+        );
+
+        $this->_aHtmlIds = array(
+            'add_poll_popup' =>  $sPrefix . '-add-poll-popup',
+            'add_poll_form_field' => $sPrefix . '-add-poll-form-field',
+            'polls_showcase' => $sPrefix . '-polls-showcase-',
+            'poll' => $sPrefix . '-poll-',
+            'poll_content' => $sPrefix . '-poll-content-',
+            'poll_view_menu' => $sPrefix . '-poll-view-menu',
+            'poll_view_link_answers' => $sPrefix . '-poll-view-answers-',
+            'poll_view_link_results' => $sPrefix . '-poll-view-results-',
+            
+            'attach_link_popup' =>  $sPrefix . '-attach-link-popup',
+            'attach_link_form_field' => $sPrefix . '-attach-link-form-field-',
+            'attach_link_item' => $sPrefix . '-attach-link-item-',
+        );
+        
+        $this->_aPregPatterns = array(
+            "meta_title" => "/<title>(.*)<\/title>/",
+            "meta_description" => "/<meta[\s]+[^>]*?name[\s]?=[\s\"\']+description[\s\"\']+content[\s]?=[\s\"\']+(.*?)[\"\']+.*?>/",
+            "url" => "/(([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%#\/\.\w\-_!\(\)]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?/"
+        );
+
+        $this->_bAttachmentsInTimeline = false;
+    }
+
+    public function isAttachLinks()
+    {
+        return !empty($this->CNF['TABLE_LINKS']) && $this->CNF['PARAM_LINKS_ENABLED'] == true;
+    }
+
+    public function isAttachmentsInTimeline()
+    {
+        return $this->_bAttachmentsInTimeline;
+    }
+    
+    public function getPregPattern($sType)
+    {
+        return $this->_aPregPatterns[$sType];
+    }
+
+    public function getHtmlIds($sKey = '')
+    {
+        if(empty($sKey))
+            return $this->_aHtmlIds;
+
+        return isset($this->_aHtmlIds[$sKey]) ? $this->_aHtmlIds[$sKey] : '';
+    }
+}
+
+/** @} */

+ 498 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/text/classes/BxBaseModTextDb.php

@@ -0,0 +1,498 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseText Base classes for text modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/*
+ * Module database queries
+ */
+class BxBaseModTextDb extends BxBaseModGeneralDb
+{
+    public function __construct(&$oConfig)
+    {
+        parent::__construct($oConfig);
+    }
+
+    public function searchByAuthorTerm($iAuthor, $sTerm, $iLimit)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if (empty($CNF['FIELDS_QUICK_SEARCH']))
+            return array();
+
+		$aBindings = array(
+		    'author' => $iAuthor
+		);
+
+        $sWhere = '';
+        foreach ($CNF['FIELDS_QUICK_SEARCH'] as $sField) {
+        	$aBindings[$sField] = '%' . $sTerm . '%';
+
+            $sWhere .= " OR `c`.`$sField` LIKE :" . $sField;
+        }
+
+        $sOrderBy = $this->prepareAsString(" ORDER BY `c`.`added` DESC LIMIT ?", (int)$iLimit);
+
+        $sQuery = "SELECT `c`.* FROM `" . $this->_oConfig->CNF['TABLE_ENTRIES'] . "` AS `c` WHERE `c`.`" . $CNF['FIELD_AUTHOR'] . "`=:author AND (0 $sWhere)" . $sOrderBy;
+        return $this->getAll($sQuery, $aBindings);
+    }
+    
+    /**
+     * 
+     * Internal Polls related methods. 
+     * 
+     */
+    public function isPollPerformed($iPollId, $iAuthorId, $iAuthorIp = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($CNF['TABLE_POLLS_ANSWERS_VOTES_TRACK']))
+            return false;
+
+        $iAuthorId = (int)$iAuthorId;
+
+        $aBindings = array('author_id' => $iAuthorId);
+        $sWhereClause = "AND `author_id`=:author_id";
+
+        if(empty($iAuthorId)) {
+            $aBindings['author_nip'] = $iAuthorIp;
+            $sWhereClause .= " AND `author_nip`=:author_nip";
+        }
+
+        $aAnswers = $this->getPollAnswers(array('type' => 'poll_id_pairs', 'poll_id' => $iPollId));
+        return (int)$this->getOne("SELECT `object_id` FROM `" . $CNF['TABLE_POLLS_ANSWERS_VOTES_TRACK'] . "` WHERE `object_id` IN (" . $this->implode_escape(array_keys($aAnswers)) . ") " . $sWhereClause . " LIMIT 1", $aBindings) != 0;
+    }
+
+    public function getPolls($aParams)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($CNF['TABLE_POLLS']))
+            return false;
+
+    	$aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sJoinClause = $sWhereClause = $sOrderByClause = "";
+
+        $sSelectClause = "`tp`.*";
+        switch($aParams['type']) {
+            case 'id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'id' => $aParams['id']
+                );
+
+                $sWhereClause .= " AND `tp`.`id`=:id";
+                break;
+
+            case 'answer_id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'answer_id' => $aParams['answer_id']
+                );
+
+                $sJoinClause = "LEFT JOIN `" . $CNF['TABLE_POLLS_ANSWERS'] . "` AS `ta` ON `tp`.`" . $CNF['FIELD_POLL_ID'] . "`=`ta`.`poll_id`";
+                $sWhereClause .= " AND `ta`.`id`=:answer_id";
+                break;
+
+            case 'content_id':
+                $aMethod['params'][1] = array(
+                    'content_id' => $aParams['content_id']
+                );
+
+                $sWhereClause .= " AND `tp`.`content_id`=:content_id";
+                break;
+            
+            case 'content_id_ids':
+                $aMethod['name'] = 'getColumn';
+                $aMethod['params'][1] = array(
+                    'content_id' => $aParams['content_id']
+                );
+
+                $sSelectClause = "`tp`.`" . $CNF['FIELD_POLL_ID'] . "`";
+                $sWhereClause .= " AND `tp`.`content_id`=:content_id";
+                break;
+
+            case 'author_id':
+                $aMethod['params'][1] = array(
+                    'author_id' => $aParams['author_id']
+                );
+
+                $sWhereClause .= " AND `tp`.`author_id`=:author_id";
+
+                if(isset($aParams['unused']) && $aParams['unused'] === true) {
+                    $aMethod['params'][1]['content_id'] = 0;
+
+                    $sWhereClause .= " AND `tp`.`content_id`=:content_id";
+                }
+                break;
+        }
+
+        if(!empty($sOrderByClause))
+            $sOrderByClause = "ORDER BY " . $sOrderByClause;
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . " FROM `" . $CNF['TABLE_POLLS'] . "` AS `tp` " . $sJoinClause . " WHERE 1 " . $sWhereClause . $sOrderByClause;
+
+        return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+
+    public function getPollInfoById($iId)
+    {
+        return $this->getPolls(array('type' => 'id', 'id' => $iId));
+    }
+
+    public function updatePolls($aParamsSet, $aParamsWhere)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($CNF['TABLE_POLLS']) || empty($aParamsSet) || empty($aParamsWhere))
+            return false;
+
+        return $this->query("UPDATE `" . $CNF['TABLE_POLLS'] . "` SET " . $this->arrayToSQL($aParamsSet) . " WHERE " . $this->arrayToSQL($aParamsWhere, " AND "));
+    }
+
+    public function deletePolls($aParams)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($CNF['TABLE_POLLS']))
+            return false;
+
+        $sWhereClause = $this->arrayToSQL($aParams, " AND ");
+        $aPolls = $this->getAll("SELECT * FROM `" . $CNF['TABLE_POLLS'] . "` WHERE " . $sWhereClause);
+        if(empty($aPolls) || !is_array($aPolls))
+            return true;
+
+        return $this->_deletePolls($aPolls);
+    }
+
+    public function deletePollsByIds($mixedId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($CNF['TABLE_POLLS']))
+            return false;
+
+        $sWhereClause = "`id` IN (" . $this->implode_escape(is_array($mixedId) ? $mixedId : array($mixedId)) . ")";
+        $aPolls = $this->getAll("SELECT * FROM `" . $CNF['TABLE_POLLS'] . "` WHERE " . $sWhereClause);
+        if(empty($aPolls) || !is_array($aPolls))
+            return true;
+
+        return $this->_deletePolls($aPolls);
+    }
+
+    public function getPollAnswers($aParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($CNF['TABLE_POLLS_ANSWERS']))
+            return false;
+
+    	$aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sWhereClause = $sOrderByClause = "";
+
+        $sSelectClause = "*";
+        switch($aParams['type']) {
+            case 'id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                	'id' => $aParams['id']
+                );
+
+                $sWhereClause .= " AND `id`=:id";
+                break;
+
+            case 'poll_id_pairs':
+                $aMethod['name'] = 'getPairs';
+                $aMethod['params'][1] = 'id';
+                $aMethod['params'][2] = 'title';
+                $aMethod['params'][3] = array(
+                    'poll_id' => $aParams['poll_id']
+                );
+
+                $sWhereClause .= " AND `poll_id`=:poll_id";
+                break;
+
+            case 'poll_id':
+                $aMethod['params'][1] = array(
+                    'poll_id' => $aParams['poll_id']
+                );
+
+                $sWhereClause .= " AND `poll_id`=:poll_id";
+                break;
+
+            case 'poll_id_max_order':
+                $aMethod['name'] = 'getOne';
+                $aMethod['params'][1] = array(
+                    'poll_id' => $aParams['poll_id']
+                );
+
+                $sSelectClause = "MAX(`order`)";
+                $sWhereClause .= " AND `poll_id`=:poll_id";
+                break;
+
+            case 'all':
+            	break;
+        }
+
+        $sOrderByClause = " ORDER BY " . (isset($aParams['order_by']) ? '`' . $aParams['order_by'] . '`' : '`order`');
+        $sOrderByClause .= " " . (isset($aParams['order_way']) ? strtoupper($aParams['order_way']) : 'ASC');
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . " FROM `" . $CNF['TABLE_POLLS_ANSWERS'] . "` WHERE 1 " . $sWhereClause . $sOrderByClause;
+
+        return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+
+    public function insertPollAnswer($aParamsSet)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($CNF['TABLE_POLLS_ANSWERS']) || empty($aParamsSet))
+            return false;
+
+        return $this->query("INSERT INTO `" . $CNF['TABLE_POLLS_ANSWERS'] . "` SET " . $this->arrayToSQL($aParamsSet));
+    }
+
+    public function updatePollAnswers($aParamsSet, $aParamsWhere)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($CNF['TABLE_POLLS_ANSWERS']) || empty($aParamsSet) || empty($aParamsWhere))
+            return false;
+
+        return $this->query("UPDATE `" . $CNF['TABLE_POLLS_ANSWERS'] . "` SET " . $this->arrayToSQL($aParamsSet) . " WHERE " . $this->arrayToSQL($aParamsWhere, " AND "));
+    }
+
+    public function deletePollAnswers($aParams)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($CNF['TABLE_POLLS_ANSWERS']))
+            return false;
+
+        $sWhereClause = $this->arrayToSQL($aParams, " AND ");
+        $aAnswers = $this->getAll("SELECT * FROM `" . $CNF['TABLE_POLLS_ANSWERS'] . "` WHERE " . $sWhereClause);
+        if(empty($aAnswers) || !is_array($aAnswers))
+            return true;
+
+        return $this->_deletePollAnswers($aAnswers);
+    }
+
+    public function deletePollAnswersByIds($mixedId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($CNF['TABLE_POLLS_ANSWERS']))
+            return false;
+
+        $sWhereClause = "`id` IN (" . $this->implode_escape(is_array($mixedId) ? $mixedId : array($mixedId)) . ")";
+        $aAnswers = $this->getAll("SELECT * FROM `" . $CNF['TABLE_POLLS_ANSWERS'] . "` WHERE " . $sWhereClause);
+        if(empty($aAnswers) || !is_array($aAnswers))
+            return true;
+
+        return $this->_deletePollAnswers($aAnswers);
+    }
+
+    protected function _deletePolls(&$aPolls)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aAffected = array();           
+        foreach($aPolls as $aPoll) {
+            if(!$this->deletePollAnswers(array('poll_id' => $aPoll[$CNF['FIELD_POLL_ID']])))
+                continue;
+
+            $aAffected[] = $aPoll[$CNF['FIELD_POLL_ID']];
+        }
+
+        return $this->query("DELETE FROM `" . $CNF['TABLE_POLLS'] . "` WHERE `id` IN (" . $this->implode_escape($aAffected) . ")");
+    }
+
+    protected function _deletePollAnswers(&$aAnswers)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aAffected = array();
+        foreach($aAnswers as $aAnswer) {
+            BxDolVote::getObjectInstance($CNF['OBJECT_VOTES_POLL_ANSWERS'], $aAnswer['id'])->onObjectDelete();
+
+            $aAffected[] = $aAnswer['id'];
+        }
+
+        return $this->query("DELETE FROM `" . $CNF['TABLE_POLLS_ANSWERS'] . "` WHERE `id` IN (" . $this->implode_escape($aAffected) . ")");
+    }
+    
+    //--- Link attach related methods ---//
+    public function getUnusedLinks($iUserId)
+    {
+        return $this->getLinksBy(array(
+            'type' => 'unused',
+            'profile_id' => $iUserId
+        ));
+    }
+
+    public function deleteUnusedLinks($iUserId, $iLinkId = 0)
+    {
+        if(!$this->_oConfig->isAttachLinks())
+            return false;
+
+        $CNF = &$this->_oConfig->CNF;
+        
+    	$aBindings = [
+            'profile_id' => $iUserId
+    	];
+
+        $sWhereAddon = '';
+        if(!empty($iLinkId)) {
+        	$aBindings['id'] = $iLinkId;
+
+            $sWhereAddon = " AND `id`=:id";
+        }
+
+        return $this->query("DELETE FROM `tl`, `tlc` USING `" . $CNF['TABLE_LINKS'] . "` AS `tl` LEFT JOIN `" . $CNF['TABLE_LINKS2CONTENT'] . "` AS `tlc` ON `tl`.`id`=`tlc`.`link_id` WHERE `tl`.`profile_id`=:profile_id AND ISNULL(`tlc`.`content_id`)" . $sWhereAddon, $aBindings);
+    }
+
+    public function saveLink($iContentId, $iLinkId)
+    {
+        if(!$this->_oConfig->isAttachLinks())
+            return false;
+
+        $CNF = &$this->_oConfig->CNF;
+
+        $aBindings = array(
+            'content_id' => $iContentId,
+            'link_id' => $iLinkId
+        );
+
+        $iId = $this->getOne("SELECT `id` FROM `" . $CNF['TABLE_LINKS2CONTENT'] . "` WHERE `content_id`=:content_id AND `link_id`=:link_id LIMIT 1", $aBindings);
+        if(!empty($iId))
+            return true;
+
+        return (int)$this->query("INSERT INTO `" . $CNF['TABLE_LINKS2CONTENT'] . "` SET `content_id`=:content_id, `link_id`=:link_id", $aBindings) > 0;
+    }
+
+    public function deleteLink($iId)
+    {
+        if(!$this->_oConfig->isAttachLinks())
+            return false;
+
+        $CNF = &$this->_oConfig->CNF;
+
+        return (int)$this->query("DELETE FROM `tl`, `tlc` USING `" . $CNF['TABLE_LINKS'] . "` AS `tl` LEFT JOIN `" . $CNF['TABLE_LINKS2CONTENT'] . "` AS `tlc` ON `tl`.`id`=`tlc`.`link_id` WHERE `tl`.`id` = :id", array(
+            'id' => $iId
+        )) > 0;
+    }
+
+    public function deleteLinks($iContentId)
+    {
+        if(!$this->_oConfig->isAttachLinks())
+            return false;
+
+        $CNF = &$this->_oConfig->CNF;
+
+        return (int)$this->query("DELETE FROM `tl`, `tlc` USING `" . $CNF['TABLE_LINKS'] . "` AS `tl` LEFT JOIN `" . $CNF['TABLE_LINKS2CONTENT'] . "` AS `tlc` ON `tl`.`id`=`tlc`.`link_id` WHERE `tlc`.`content_id` = :content_id", array(
+            'content_id' => $iContentId
+        )) > 0;
+    }
+
+    public function getLinks($iContentId)
+    {
+        return $this->getLinksBy(array('type' => 'content_id', 'content_id' => $iContentId));
+    }
+    
+    public function getStatByProfile($iAuthorId)
+    {
+        $aBindings = array(
+            'author' => $iAuthorId
+        );
+        $CNF = &$this->_oConfig->CNF;
+        
+        return $this->getRow("SELECT COUNT(id) AS count, SUM(views) AS views, SUM(votes) AS votes, SUM(rvotes) AS rvotes FROM `" . $CNF['TABLE_ENTRIES'] . "` WHERE `author`=:author", $aBindings);
+    }
+
+    public function getLinksBy($aParams = array())
+    {
+        if(!$this->_oConfig->isAttachLinks())
+            return [];
+
+        $CNF = &$this->_oConfig->CNF;
+    	$aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+
+    	$sSelectClause = "`tl`.*";
+    	$sJoinClause = $sWhereClause = $sGroupClause = $sOrderClause = $sLimitClause = "";
+        switch($aParams['type']) {
+            case 'id':
+            	$aMethod['name'] = 'getRow';
+            	$aMethod['params'][1] = array(
+                    'id' => $aParams['id']
+                );
+
+                $sWhereClause = " AND `tl`.`id`=:id";
+
+                if(!empty($aParams['profile_id'])) {
+                    $aMethod['params'][1]['profile_id'] = $aParams['profile_id'];
+
+                    $sWhereClause .= " AND `tl`.`profile_id`=:profile_id";
+                }
+                break;
+
+            case 'content_id':
+            	$aMethod['params'][1] = array(
+                    'content_id' => $aParams['content_id']
+                );
+
+                $sJoinClause = "LEFT JOIN `" . $CNF['TABLE_LINKS2CONTENT'] . "` AS `tlc` ON `tl`.`id`=`tlc`.`link_id`";
+                $sWhereClause = " AND `tlc`.`content_id`=:content_id";
+                break;
+
+            case 'unused':
+                $aBindings = array(
+                    'profile_id' => $aParams['profile_id']
+                );
+
+                if(isset($aParams['short']) && $aParams['short'] === true) {
+                    $aMethod['name'] = 'getPairs';
+                    $aMethod['params'][1] = 'url';
+                    $aMethod['params'][2] = 'id';
+                    $aMethod['params'][3] = $aBindings;
+                }
+                else
+                    $aMethod['params'][1] = $aBindings;
+
+                $sJoinClause = "LEFT JOIN `" . $CNF['TABLE_LINKS2CONTENT'] . "` AS `tlc` ON `tl`.`id`=`tlc`.`link_id`";
+                $sWhereClause = " AND `tl`.`profile_id`=:profile_id AND ISNULL(`tlc`.`content_id`)";
+                $sOrderClause = "`tl`.`added` DESC";
+                break;
+        }
+
+        $sOrderClause = !empty($sOrderClause) ? "ORDER BY " . $sOrderClause : $sOrderClause;
+        $sLimitClause = !empty($sLimitClause) ? "LIMIT " . $sLimitClause : $sLimitClause;
+
+        $aMethod['params'][0] = "SELECT
+                " . $sSelectClause . "
+            FROM `" . $CNF['TABLE_LINKS'] . "` AS `tl` " . $sJoinClause . "
+            WHERE 1" . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause . " " . $sLimitClause;
+
+        return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+    
+    protected function _getEntriesBySearchIds($aParams, &$aMethod, &$sSelectClause, &$sJoinClause, &$sWhereClause, &$sOrderClause, &$sLimitClause)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if($CNF['FIELD_STATUS'])
+            $sWhereClause .= " AND `" . $CNF['TABLE_ENTRIES'] . "`.`" . $CNF['FIELD_STATUS'] . "`='active'";
+        
+        if($CNF['FIELD_STATUS_ADMIN'])
+            $sWhereClause .= " AND `" . $CNF['TABLE_ENTRIES'] . "`.`" . $CNF['FIELD_STATUS_ADMIN'] . "`='active'";
+        
+        parent::_getEntriesBySearchIds($aParams, $aMethod, $sSelectClause, $sJoinClause, $sWhereClause, $sOrderClause, $sLimitClause);        
+    }
+}
+
+/** @} */

+ 1189 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/text/classes/BxBaseModTextModule.php

@@ -0,0 +1,1189 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseText Base classes for text modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+define('BX_BASE_MOD_TEXT_STATUS_ACTIVE', 'active');
+define('BX_BASE_MOD_TEXT_STATUS_HIDDEN', 'hidden');
+define('BX_BASE_MOD_TEXT_STATUS_AWAITING', 'awaiting');
+/**
+ * Saved for backward compatibility.
+ * Can be removed in future releases of UNA 13.
+ */
+define('BX_BASE_MOD_TEXT_STATUS_PENDING', 'pending');
+
+/**
+ * Base module class for text based modules
+ */
+class BxBaseModTextModule extends BxBaseModGeneralModule implements iBxDolContentInfoService
+{
+    function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+    }
+
+
+    // ====== ACTIONS METHODS
+
+    public function actionEmbedPoll($iPollId = 0)
+    {
+        if(empty($iPollId) && bx_get('poll_id') !== false)
+            $iPollId = (int)bx_get('poll_id');
+
+        $aParams = bx_get_with_prefix('param');
+        array_walk($aParams, function(&$sValue) {
+            $sValue = bx_process_input($sValue);
+        });
+
+        $this->_oTemplate->embedPollItem($iPollId, $aParams);
+    }
+
+    public function actionEmbedPolls($iId = 0)
+    {
+        list($iContentId, $aContentInfo) = $this->_getContent($iId);
+        if($iContentId === false)
+            return;
+
+        $aParams = bx_get_with_prefix('param');
+        array_walk($aParams, function(&$sValue) {
+            $sValue = bx_process_input($sValue);
+        });
+
+        $this->_oTemplate->embedPollItems($aContentInfo, $aParams);
+    }
+
+    public function actionGetPoll()
+    {
+        $iPollId = (int)bx_get('poll_id');
+        $sView = bx_process_input(bx_get('view'));
+
+        $sMethod = 'serviceGetBlockPoll' . bx_gen_method_name($sView);
+        if(!method_exists($this, $sMethod))
+            return echoJson(array());
+
+        $aBlock = $this->$sMethod($iPollId, true);
+        if(empty($aBlock) || !is_array($aBlock))
+            return echoJson(array());
+
+        return echoJson(array(
+            'content' => $aBlock['content']
+        ));
+    }
+    public function actionDeletePoll()
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $iId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if(empty($iId))
+            return echoJson(array());
+
+        $aResult = array();
+        if($this->_oDb->deletePolls(array($CNF['FIELD_POLL_ID'] => $iId)))
+            $aResult = array('code' => 0);
+        else
+            $aResult = array('code' => 1, 'message' => _t($CNF['txt_err_cannot_perform_action']));
+
+        echoJson($aResult);
+    }
+
+    public function actionGetPollForm()
+    {
+        echo $this->_oTemplate->getPollForm();
+    }
+
+    public function actionSubmitPollForm()
+    {
+        echoJson($this->getPollForm());
+    }
+    
+    public function actionGetAttachLinkForm()
+    {
+        $iContentId = 0;
+        if(bx_get('content_id') !== false)
+            $iContentId = (int)bx_get('content_id');
+
+        echo $this->_oTemplate->getAttachLinkForm($iContentId);
+    }
+    
+    public function actionSubmitAttachLinkForm()
+    {
+        echoJson($this->getFormAttachLink());
+    }
+    
+    public function actionAddAttachLink()
+    {
+        $sUrl = bx_process_input(bx_get('url'));
+        if(empty($sUrl))
+            return echoJson(array());
+
+        $sUrl = htmlspecialchars_decode($sUrl);
+        $oStreamContext = stream_context_create([
+            'http' => [
+                'timeout' => getParam('sys_default_socket_timeout'), 
+            ]
+        ]);
+
+        $sHeader = 'Content-Type';
+        $aHeaders = @get_headers($sUrl, 1, $oStreamContext);
+        if(!empty($aHeaders) && is_array($aHeaders) && !empty($aHeaders[$sHeader])) {
+            $mixedContentType = $aHeaders[$sHeader];
+            if(!is_array($mixedContentType))
+                $mixedContentType = array($mixedContentType);
+
+            foreach($mixedContentType as $sContentType)
+                if(strpos($sContentType, 'image') !== false) 
+                    return echoJson(array());
+        }
+
+        $iContentId = 0;
+        if(bx_get('content_id') !== false)
+            $iContentId = (int)bx_get('content_id');
+
+        echoJson($this->addAttachLink(array(
+            'content_id' => $iContentId,
+            'url' => $sUrl
+        )));
+    }
+    
+    public function actionDeleteAttachLink()
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+    	$iUserId = $this->getUserId();
+        $iLinkId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if(empty($iLinkId))
+            return echoJson(array());
+
+        $aLink = $this->_oDb->getLinksBy(array('type' => 'id', 'id' => $iLinkId, 'profile_id' => $iUserId));
+    	if(empty($aLink) || !is_array($aLink))
+            return echoJson(array());
+
+        if(!empty($aLink['media_id']))
+            BxDolStorage::getObjectInstance($CNF['OBJECT_STORAGE_PHOTOS'])->deleteFile($aLink['media_id']);
+
+        $aResult = array();
+        if($this->_oDb->deleteLink($iLinkId))
+            $aResult = array('code' => 0);
+        else
+            $aResult = array('code' => 1, 'message' => _t(!empty($CNF['txt_link_form_err_delete']) ? $CNF['txt_link_form_err_delete'] : $this->getName() . '_form_post_input_link_err_delete'));
+
+        echoJson($aResult);
+    }
+
+    public function actionFileEmbedVideo($iFileId)
+    {
+        $this->_oTemplate->embedVideo($iFileId);
+    }
+
+    public function actionFileEmbedSound($iFileId)
+    {
+        $this->_oTemplate->embedSound($iFileId);
+    }
+
+
+    // ====== SERVICE METHODS
+
+    public function serviceGetSafeServices()
+    {
+        $a = parent::serviceGetSafeServices();
+        return array_merge($a, array (
+            'GetBlockPollAnswers' => '',
+            'GetBlockPollResults' => '',
+            'GetMenuAddonManageToolsProfileStats' => '',
+            'BrowsePublic' => '',
+            'BrowsePopular' => '',
+            'BrowseTop' => '',
+            'BrowseUpdated' => '',
+            'BrowseAuthor' => '',
+            'CategoriesMultiListContext' => ''
+        ));
+    }
+
+    public function serviceManageTools($sType = 'common')
+    {
+        $sResult = parent::serviceManageTools($sType);
+        if(!empty($sResult))
+            $this->_oTemplate->addJsSystem(['modules/base/text/js/|manage_tools.js']);
+
+        return $sResult;
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_text Base Text
+     * @subsection bx_base_general-page_blocks Page Blocks
+     * @subsubsection bx_base_general-categories_multi_list_context categories_multi_list_context
+     * 
+     * @code bx_srv('bx_posts', 'categories_multi_list_context', [...]); @endcode
+     * 
+     * Display multi-categorories block with number of posts in each category for current context
+     * @param $bDisplayEmptyCats display empty categories
+     * 
+     * @see BxBaseModGeneralModule::serviceCategoriesMultiListContext
+     */
+    /** 
+     * @ref bx_base_general-categories_multi_list_context "categories_multi_list_context"
+     */
+    public function serviceCategoriesMultiListContext($iProfileId = 0, $bDisplayEmptyCats = true)
+    {
+        if ($iProfileId == 0)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        
+		$oCategories = BxDolCategories::getInstance();
+        $aCats = $oCategories->getData([
+                'type' => 'by_module&context_with_num', 
+                'module' => $this->getName(), 
+                'context_id' => $iProfileId
+            ]
+        );
+        $aVars = array('bx_repeat:cats' => array());
+        foreach ($aCats as $oCat) {
+            $sValue = $oCat['value'];
+            $iNum = $oCat['num'];
+            
+            $aVars['bx_repeat:cats'][] = array(
+                'url' => $oCategories->getUrl($this->getName(), $sValue, '&context_id=' . $iProfileId),
+                'name' => _t($sValue),
+                'value' => $sValue,
+                'num' => $iNum,
+            );
+        }
+        
+        if (!$aVars['bx_repeat:cats'])
+            return '';
+
+        return $this->_oTemplate->parseHtmlByName('category_list_multi.html', $aVars);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_text Base Text
+     * @subsection bx_base_text-polls Polls Blocks
+     * @subsubsection bx_base_text-get_block_poll_answers get_block_poll_answers
+     * 
+     * @code bx_srv('bx_posts', 'get_block_poll_answers', [...]); @endcode
+     * 
+     * Get block with poll answers
+     * @param $iPollId poll ID
+     * 
+     * @see BxBaseModTextModule::serviceGetBlockPollAnswers
+     */
+    /** 
+     * @ref bx_base_text-get_block_poll_answers "get_block_poll_answers"
+     */
+    public function serviceGetBlockPollAnswers($iPollId, $bForceDisplay = false)
+    {
+        if(!$iPollId)
+            return false;
+
+        if(!$bForceDisplay && $this->isPollPerformed($iPollId))
+            return $this->serviceGetBlockPollResults($iPollId);
+
+        return $this->_serviceTemplateFunc('entryPollAnswers', $iPollId, 'getPollInfoById');
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_text Base Text
+     * @subsection bx_base_text-polls Polls Blocks
+     * @subsubsection bx_base_text-get_block_poll_results get_block_poll_results
+     * 
+     * @code bx_srv('bx_posts', 'get_block_poll_results', [...]); @endcode
+     * 
+     * Get block with poll results
+     * @param $iPollId poll ID
+     * 
+     * @see BxBaseModTextModule::serviceGetBlockPollResults
+     */
+    /** 
+     * @ref bx_base_text-get_block_poll_results "get_block_poll_results"
+     */
+    public function serviceGetBlockPollResults($iPollId)
+    {
+        return $this->_serviceTemplateFunc('entryPollResults', $iPollId, 'getPollInfoById');
+    }
+    
+    /**
+     * Display media EXIF information.
+     * @param $iMediaId media ID, if it's omitted then it's taken from 'id' GET variable.
+     * @return HTML string with EXIF info. On error empty string is returned.
+     */ 
+    public function serviceMediaExif ($iMediaId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        return $this->_serviceTemplateFunc ('mediaExif', $iMediaId, $CNF['FUNCTION_FOR_GET_ITEM_INFO']);
+    }
+
+    public function serviceGetThumb ($iContentId, $sTranscoder = '') 
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if(bx_is_api()){
+            $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+            if (isset($CNF['FIELD_THUMB']))
+                return bx_api_get_image($CNF['OBJECT_STORAGE'], $aContentInfo[$CNF['FIELD_THUMB']]);
+        }
+            
+        if(empty($sTranscoder) && !empty($CNF['OBJECT_IMAGES_TRANSCODER_GALLERY']))
+            $sTranscoder = $CNF['OBJECT_IMAGES_TRANSCODER_GALLERY'];
+
+        $mixedResult = $this->_getFieldValueThumb('FIELD_THUMB', $iContentId, $sTranscoder);
+        return $mixedResult !== false ? $mixedResult : '';
+    }
+
+    public function serviceGetMenuAddonManageTools()
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+        $iNumTotal = $this->_oDb->getEntriesNumByParams();
+
+        $iNum1 = $this->_oDb->getEntriesNumByParams([
+            [
+                'key' => $CNF['FIELD_STATUS'], 
+                'value' => 'hidden', 
+                'operator' => '='
+            ]
+        ]);
+        
+        $iNum2 = 0;
+        if (isset($CNF['OBJECT_REPORTS'])){
+            $iNum2 = $this->_oDb->getEntriesNumByParams([
+                [
+                    'key' => 'reports',
+                    'value' => '0', 
+                    'operator' => '>'
+                ]
+            ]);
+        }
+        return array('counter1_value' => $iNum1, 'counter2_value' => $iNum2, 'counter3_value' => $iNumTotal );
+	}
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_text Base Text
+     * @subsection bx_base_text-other Other
+     * @subsubsection bx_base_text-get_menu_addon_manage_tools_profile_stats get_menu_addon_manage_tools_profile_stats
+     * 
+     * @code bx_srv('bx_posts', 'get_menu_addon_manage_tools_profile_stats', []); @endcode
+     * 
+     * Get number of posts for currently logged in user
+     * 
+     * @see BxBaseModTextModule::serviceGetMenuAddonManageToolsProfileStats
+     */
+    /** 
+     * @ref bx_base_text-get_menu_addon_manage_tools_profile_stats "get_menu_addon_manage_tools_profile_stats"
+     */
+    public function serviceGetMenuAddonManageToolsProfileStats($iProfileId = 0)
+    {
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+
+        bx_import('SearchResult', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'SearchResult';
+        $o = new $sClass();
+        $o->fillFilters([
+            'author' => $iProfileId
+        ]);
+        $o->unsetPaginate();
+
+        return $o->getNum();
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_text Base Text
+     * @subsection bx_base_text-browse Browse
+     * @subsubsection bx_base_text-browse_public browse_public
+     * 
+     * @code bx_srv('bx_posts', 'browse_public', [...]); @endcode
+     * 
+     * Display public posts
+     * @param $sUnitView unit view, such as: full, extended, gallery, showcase
+     * @param $bEmptyMessage display or not "empty" message when there is no content
+     * @param $bAjaxPaginate use AJAX paginate or not
+     * 
+     * @see BxBaseModTextModule::serviceBrowsePublic
+     */
+    /** 
+     * @ref bx_base_text-browse_public "browse_public"
+     */
+    public function serviceBrowsePublic ($sUnitView = false, $bEmptyMessage = true, $bAjaxPaginate = true)
+    {   
+        return $this->_serviceBrowse ('public', $sUnitView ? array('unit_view' => $sUnitView) : false, BX_DB_PADDING_DEF, $bEmptyMessage, $bAjaxPaginate);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_text Base Text
+     * @subsection bx_base_text-browse Browse
+     * @subsubsection bx_base_text-browse_popular browse_popular
+     * 
+     * @code bx_srv('bx_posts', 'browse_popular', [...]); @endcode
+     * 
+     * Display popular posts
+     * @param $sUnitView unit view, such as: full, extended, gallery, showcase
+     * @param $bEmptyMessage display or not "empty" message when there is no content
+     * @param $bAjaxPaginate use AJAX paginate or not
+     * 
+     * @see BxBaseModTextModule::serviceBrowsePopular
+     */
+    /** 
+     * @ref bx_base_text-browse_popular "browse_popular"
+     */
+    public function serviceBrowsePopular ($sUnitView = false, $bEmptyMessage = true, $bAjaxPaginate = true)
+    {
+        return $this->_serviceBrowse ('popular', $sUnitView ? array('unit_view' => $sUnitView) : false, BX_DB_PADDING_DEF, $bEmptyMessage, $bAjaxPaginate);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_text Base Text
+     * @subsection bx_base_text-browse Browse
+     * @subsubsection bx_base_text-browse_top browse_top
+     * 
+     * @code bx_srv('bx_posts', 'browse_top', [...]); @endcode
+     * 
+     * Display top posts
+     * @param $sUnitView unit view, such as: full, extended, gallery, showcase
+     * @param $bEmptyMessage display or not "empty" message when there is no content
+     * @param $bAjaxPaginate use AJAX paginate or not
+     * 
+     * @see BxBaseModTextModule::serviceBrowseTop
+     */
+    /** 
+     * @ref bx_base_text-browse_top "browse_top"
+     */
+    public function serviceBrowseTop ($sUnitView = false, $bEmptyMessage = true, $bAjaxPaginate = true)
+    {
+        return $this->_serviceBrowse ('top', $sUnitView ? array('unit_view' => $sUnitView) : false, BX_DB_PADDING_DEF, $bEmptyMessage, $bAjaxPaginate);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_text Base Text
+     * @subsection bx_base_text-browse Browse
+     * @subsubsection bx_base_text-browse_updated browse_updated
+     * 
+     * @code bx_srv('bx_posts', 'browse_updated', [...]); @endcode
+     * 
+     * Display recently updated posts
+     * @param $sUnitView unit view, such as: full, extended, gallery, showcase
+     * @param $bEmptyMessage display or not "empty" message when there is no content
+     * @param $bAjaxPaginate use AJAX paginate or not
+     * 
+     * @see BxBaseModTextModule::serviceBrowseUpdated
+     */
+    /** 
+     * @ref bx_base_text-browse_updated "browse_updated"
+     */
+    public function serviceBrowseUpdated ($sUnitView = false, $bEmptyMessage = true, $bAjaxPaginate = true)
+    {
+        return $this->_serviceBrowse ('updated', $sUnitView ? array('unit_view' => $sUnitView) : false, BX_DB_PADDING_DEF, $bEmptyMessage, $bAjaxPaginate);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_text Base Text
+     * @subsection bx_base_text-browse Browse
+     * @subsubsection bx_base_text-browse_author browse_author
+     * 
+     * @code bx_srv('bx_posts', 'browse_author', [...]); @endcode
+     * 
+     * Display posts of specified author
+     * @param $iProfileId profile ID
+     * @param $aParams additional params, see BxBaseModGeneralModule::serviceBrowse
+     * 
+     * @see BxBaseModTextModule::serviceBrowseAuthor
+     */
+    /** 
+     * @ref bx_base_text-browse_author "browse_author"
+     */
+    public function serviceBrowseAuthor ($iProfileId = 0, $aParams = array())
+    {
+        return $this->_serviceBrowseWithParam ('author', 'profile_id', $iProfileId, $aParams);
+    }
+    
+    /**
+     * @page service Service Calls
+     * @section bx_base_text Base Text
+     * @subsection bx_base_text-page_blocks Page Blocks
+     * @subsubsection bx_base_text-entity_author entity_author
+     * 
+     * @code bx_srv('bx_posts', 'entity_author', [...]); @endcode
+     * 
+     * Display author block for the specified post
+     * @param $iContentId content ID
+     * 
+     * @see BxBaseModTextModule::serviceEntityAuthor
+     */
+    /** 
+     * @ref bx_base_text-entity_author "entity_author"
+     */
+    public function serviceEntityAuthor ($iContentId = 0)
+    {
+        return $this->_serviceTemplateFunc ('entryAuthor', $iContentId);
+    }
+    
+    /**
+     * @page service Service Calls
+     * @section bx_base_text Base Text
+     * @subsection bx_base_text-page_blocks Page Blocks
+     * @subsubsection bx_base_text-entity_polls entity_polls
+     * 
+     * @code bx_srv('bx_posts', 'entity_polls', [...]); @endcode
+     * 
+     * Display polls for the specified post
+     * @param $iContentId content ID
+     * 
+     * @see BxBaseModTextModule::serviceEntityPolls
+     */
+    /** 
+     * @ref bx_base_text-entity_polls "entity_polls"
+     */
+    public function serviceEntityPolls ($iContentId = 0)
+    {
+        return $this->_serviceTemplateFunc ('entryPolls', $iContentId);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_base_text Base Text
+     * @subsection bx_base_text-page_blocks Page Blocks
+     * @subsubsection bx_base_text-entity_breadcrumb entity_breadcrumb
+     * 
+     * @code bx_srv('bx_forum', 'entity_breadcrumb', [...]); @endcode
+     * 
+     * Display breadcrumb for the specified post
+     * @param $iContentId content ID
+     * 
+     * @see BxBaseModTextModule::serviceEntityBreadcrumb
+     */
+    /** 
+     * @ref bx_base_text-entity_breadcrumb "entity_breadcrumb"
+     */
+    public function serviceEntityBreadcrumb ($iContentId = 0)
+    {
+        return $this->_serviceTemplateFunc ('entryBreadcrumb', $iContentId);
+    }
+
+    /**
+     * Delete all content by profile 
+     * @param $iProfileId profile id 
+     * @return number of deleted items
+     */
+    public function serviceDeleteEntitiesByAuthor ($iProfileId)
+    {
+        $a = $this->_oDb->getEntriesByAuthor((int)$iProfileId);
+        if (!$a)
+            return 0;
+
+        $iCount = 0;
+        foreach ($a as $aContentInfo)
+            $iCount += ('' == $this->serviceDeleteEntity($aContentInfo[$this->_oConfig->CNF['FIELD_ID']]) ? 1 : 0);
+
+        return $iCount;
+    }
+
+
+    // ====== PERMISSION METHODS
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedSetThumb ($iContentId = 0)
+    {
+        // check ACL
+        $aCheck = checkActionModule($this->_iProfileId, 'set thumb', $this->getName(), false);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    public function isAllowedApprove($mixedContent, $isPerformAction = false)
+    {
+        return $this->checkAllowedApprove($mixedContent, $isPerformAction) === CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    public function checkAllowedApprove($mixedContent, $isPerformAction = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sTxtError = '_sys_txt_access_denied';
+        $iProfileId = bx_get_logged_profile_id();
+
+        if(!is_array($mixedContent))
+            $mixedContent = $this->_oDb->getContentInfoById((int)$mixedContent);
+
+        if(empty($mixedContent) || !is_array($mixedContent))
+            return _t($sTxtError);
+        
+        if(!isset($CNF['FIELD_STATUS_ADMIN']) || $mixedContent[$CNF['FIELD_STATUS_ADMIN']] != BX_BASE_MOD_GENERAL_STATUS_PENDING)
+            return _t($sTxtError);
+
+        if($this->_isModerator())
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        if(!empty($CNF['FIELD_ALLOW_VIEW_TO']) && (int)$mixedContent[$CNF['FIELD_ALLOW_VIEW_TO']] < 0) {
+            $iContextProfileId = abs((int)$mixedContent[$CNF['FIELD_ALLOW_VIEW_TO']]);
+            $oContextProfile = BxDolProfile::getInstance($iContextProfileId);
+
+            $aAdmins = bx_srv($oContextProfile->getModule(), 'get_admins_to_manage_content', [$iContextProfileId]);
+            if(!empty($aAdmins) && in_array($iProfileId, $aAdmins))
+                return CHECK_ACTION_RESULT_ALLOWED;
+        }
+        
+        return _t($sTxtError);
+    }    
+
+    public function isPollPerformed($iObjectId, $iAuthorId = 0, $iAuthorIp = 0)
+    {
+        if(empty($iAuthorId)) {
+            $iAuthorId = bx_get_logged_profile_id();
+            $iAuthorIp = bx_get_ip_hash(getVisitorIP());
+        }
+
+        return $this->_oDb->isPollPerformed($iObjectId, $iAuthorId, $iAuthorIp);
+    }
+
+
+    // ====== COMMON METHODS
+    public function addAttachLink($aValues, $sDisplay = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$sDisplay)
+            $sDisplay = $CNF['OBJECT_FORM_DISPLAY_ATTACH_LINK_ADD'];
+
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ATTACH_LINK'], $sDisplay, $this->_oTemplate);
+        if(!$oForm)
+            return array('message' => '_sys_txt_error_occured');
+
+        $oForm->aFormAttrs['method'] = BX_DOL_FORM_METHOD_SPECIFIC;
+        $oForm->aParams['csrf']['disable'] = true;
+        if(!empty($oForm->aParams['db']['submit_name'])) {
+            $sSubmitName = $oForm->aParams['db']['submit_name'];
+            if(!isset($oForm->aInputs[$sSubmitName])) {
+                if(isset($oForm->aInputs[$CNF['FIELD_ATTACH_LINK_CONTROLS']]))
+                    foreach($oForm->aInputs[$CNF['FIELD_ATTACH_LINK_CONTROLS']] as $mixedIndex => $aInput) {
+                        if(!is_numeric($mixedIndex) || empty($aInput['name']) || $aInput['name'] != $sSubmitName)
+                            continue;
+    
+                        $aValues[$sSubmitName] = $aInput['value'];
+                    }
+            }
+            else            
+                $aValues[$sSubmitName] = $oForm->aInputs[$sSubmitName]['value'];
+        }
+
+        $oForm->aInputs['url']['checker']['params']['preg'] = $this->_oConfig->getPregPattern('url');
+
+        $oForm->initChecker(array(), $aValues);
+        if(!$oForm->isSubmittedAndValid())
+            return array('message' => '_sys_txt_error_occured');
+
+        return $this->_addLink($oForm);
+    }
+    
+    public function deleteAttachLinks($iId)
+    {
+        if(!$this->_oConfig->isAttachLinks())
+            return;
+
+        $CNF = &$this->_oConfig->CNF;
+
+        $aLinks = $this->_oDb->getLinks($iId);
+        if(empty($aLinks) || !is_array($aLinks))
+            return;
+
+        $oStorage = BxDolStorage::getObjectInstance($CNF['OBJECT_STORAGE_PHOTOS']);
+        foreach($aLinks as $aLink)
+            if(!empty($aLink['media_id']))
+                $oStorage->deleteFile($aLink['media_id']);
+
+        $this->_oDb->deleteLinks($iId);
+    }
+
+    public function deleteAttachLinksUnused($iProfileId)
+    {
+        if(!$this->_oConfig->isAttachLinks())
+            return;
+
+        $CNF = &$this->_oConfig->CNF;
+
+        $aLinks = $this->_oDb->getUnusedLinks($iProfileId);
+        if(empty($aLinks) || !is_array($aLinks))
+            return;
+
+        $oStorage = BxDolStorage::getObjectInstance($CNF['OBJECT_STORAGE_PHOTOS']);
+        foreach($aLinks as $aLink)
+            if(!empty($aLink['media_id']))
+                $oStorage->deleteFile($aLink['media_id']);
+
+        $this->_oDb->deleteUnusedLinks($iProfileId);
+    }
+
+    public function getFormAttachLink($iContentId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ATTACH_LINK'], $CNF['OBJECT_FORM_DISPLAY_ATTACH_LINK_ADD'], $this->_oTemplate);
+        $oForm->aFormAttrs['action'] = BX_DOL_URL_ROOT . $this->_oConfig->getBaseUri() . 'submit_attach_link_form/';
+        $oForm->aInputs['content_id']['value'] = $iContentId;
+        $oForm->aInputs['url']['checker']['params']['preg'] = $this->_oConfig->getPregPattern('url');
+
+        $oForm->initChecker();
+        if($oForm->isSubmittedAndValid())
+            return $this->_addLink($oForm);
+
+        return array('form' => $oForm->getCode(), 'form_id' => $oForm->id);
+    }
+    
+    protected function _addLink(&$oForm)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+        $iUserId = $this->getUserId();
+
+        $iContentId = (int)$oForm->getCleanValue('content_id');
+        $sLink = rtrim($oForm->getCleanValue('url'), '/');
+        $sHost = parse_url($sLink, PHP_URL_HOST);
+        if (is_private_ip(gethostbyname($sHost)))
+            return array('message' => _t('_sys_txt_error_occured'));
+
+        $aMatches = array();
+        preg_match($this->_oConfig->getPregPattern('url'), $sLink, $aMatches);
+        $sLink = (empty($aMatches[2]) ? 'http://' : '') . $aMatches[0];
+
+        $aSiteInfo = bx_get_site_info($sLink, array(
+            'thumbnailUrl' => array('tag' => 'link', 'content_attr' => 'href'),
+            'OGImage' => array('name_attr' => 'property', 'name' => 'og:image'),
+        ));
+
+        $sTitle = !empty($aSiteInfo['title']) ? $aSiteInfo['title'] : $sHost;
+        $sDescription = !empty($aSiteInfo['description']) ? $aSiteInfo['description'] : '';
+
+        $sMediaUrl = '';
+        if(!empty($aSiteInfo['thumbnailUrl']))
+            $sMediaUrl = $aSiteInfo['thumbnailUrl'];
+        else if(!empty($aSiteInfo['OGImage']))
+            $sMediaUrl = $aSiteInfo['OGImage'];
+
+        $iMediaId = 0;
+        $oStorage = null;
+        if(!empty($sMediaUrl)) {
+            $oStorage = BxDolStorage::getObjectInstance($CNF['OBJECT_STORAGE_PHOTOS']);
+
+            $iMediaId = $oStorage->storeFileFromUrl($sMediaUrl, true, $iUserId);
+        }
+
+        $iId = (int)$oForm->insert(array('profile_id' => $iUserId, 'media_id' => $iMediaId, 'url' => $sLink, 'title' => $sTitle, 'text' => $sDescription, 'added' => time()));
+        if(!empty($iId)) {
+            if(!empty($oStorage) && !empty($iMediaId))
+                $oStorage->afterUploadCleanup($iMediaId, $iUserId);
+
+            return array(
+                'id' => $iId, 
+                'content_id' => $iContentId, 
+                'url' => $sLink,
+                'item' => $this->_oTemplate->getAttachLinkItem($iUserId, $iId)
+            );
+        }
+
+        return array('message' => _t('_bx_timeline_txt_err_cannot_perform_action'));
+    }
+
+    public function onApprove($mixedContent)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!is_array($mixedContent))
+            $mixedContent = $this->_oDb->getContentInfoById((int)$mixedContent);
+
+        $this->alertAfterApprove($mixedContent);
+
+        $this->onPublished($mixedContent[$CNF['FIELD_ID']]);
+    }
+
+    public function alertAfterAdd($aContentInfo)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $iId = (int)$aContentInfo[$CNF['FIELD_ID']];
+        $iAuthorId = (int)$aContentInfo[$CNF['FIELD_AUTHOR']];
+
+        $sAction = 'added';
+        if(isset($CNF['FIELD_STATUS']) && isset($aContentInfo[$CNF['FIELD_STATUS']]) && $aContentInfo[$CNF['FIELD_STATUS']] == BX_BASE_MOD_TEXT_STATUS_AWAITING)
+            $sAction = 'deferred';
+        else if(isset($CNF['FIELD_STATUS_ADMIN']) && isset($aContentInfo[$CNF['FIELD_STATUS_ADMIN']]) && $aContentInfo[$CNF['FIELD_STATUS_ADMIN']] == BX_BASE_MOD_GENERAL_STATUS_PENDING)
+            $sAction = 'deferred';
+
+        $aParams = $this->_alertParamsAdd($aContentInfo);
+        bx_alert('system', 'prepare_alert_params', 0, 0, array('unit'=> $this->getName(), 'action' => &$sAction, 'object_id' => &$iId, 'sender_id' => &$iAuthorId, 'extras' => &$aParams));
+        bx_alert($this->getName(), $sAction, $iId, $iAuthorId, $aParams);
+
+        $this->_processModerationNotifications($aContentInfo);
+    }
+
+    public function alertAfterEdit($aContentInfo)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $iId = (int)$aContentInfo[$CNF['FIELD_ID']];
+
+        $aParams = $this->_alertParams($aContentInfo);
+        bx_alert($this->getName(), 'edited', $iId, false, $aParams);
+    }
+
+    /**
+     * Get array of params to be passed in Add/Edit Alert.
+     */
+    protected function _alertParams($aContentInfo)
+    {
+        $aParams = parent::_alertParams($aContentInfo);
+
+        $CNF = &$this->_oConfig->CNF;
+        
+        if(!empty($CNF['FIELD_STATUS']) && isset($aContentInfo[$CNF['FIELD_STATUS']]))
+            $aParams['status'] = $aContentInfo[$CNF['FIELD_STATUS']];
+
+        if(!empty($CNF['FIELD_STATUS_ADMIN']) && isset($aContentInfo[$CNF['FIELD_STATUS_ADMIN']]))
+            $aParams['status_admin'] = $aContentInfo[$CNF['FIELD_STATUS_ADMIN']];
+
+        if(!empty($CNF['FIELD_ALLOW_VIEW_TO']) && isset($aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]))
+            $aParams['privacy_view'] = $aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']];
+
+        if(!empty($CNF['FIELD_CF']) && isset($aContentInfo[$CNF['FIELD_CF']]))
+            $aParams['cf'] = $aContentInfo[$CNF['FIELD_CF']];
+
+        return $aParams;
+    }
+
+    protected function _alertParamsAdd($aContentInfo)
+    {
+        $aParams = $this->_alertParams($aContentInfo);
+
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!empty($CNF['OBJECT_METATAGS']))
+            $aParams['timeline_group'] = array(
+                'by' => $this->getName() . '_' . abs((int)$aContentInfo[$CNF['FIELD_AUTHOR']]) . '_' . (int)$aContentInfo[$CNF['FIELD_ID']],
+                'field' => 'owner_id'
+            );
+
+        return $aParams;
+    }
+
+    public function getPollForm()
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($CNF['OBJECT_FORM_POLL']))
+            return array('code' => 1, 'message' => '_sys_txt_error_occured');
+
+        $iProfileId = bx_get_logged_profile_id();
+
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_POLL'], $CNF['OBJECT_FORM_POLL_DISPLAY_ADD'], $this->_oTemplate);
+        $oForm->aFormAttrs['action'] = BX_DOL_URL_ROOT . $this->_oConfig->getBaseUri() . 'submit_poll_form/';
+
+        $oForm->initChecker();
+        if($oForm->isSubmittedAndValid()) {
+            $iId = $oForm->insert();
+            if($iId)
+                return array('code' => 0, 'id' => $iId, 'item' => $this->_oTemplate->getPollItem($iId, $iProfileId, array(
+                    'manage' => true
+                )));
+            else
+                return array('code' => 2, 'message' => '_sys_txt_error_entry_creation');
+        }
+
+        return array('form' => $oForm->getCode(), 'form_id' => $oForm->id);
+    }
+    
+    public function getEntryImageData($aContentInfo, $sField = 'FIELD_THUMB', $aTranscoders = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+        $mResult = parent::getEntryImageData($aContentInfo, $sField, $aTranscoders);
+        if ($mResult === false &&  isset($CNF['PARAM_USE_GALERY_AS_COVER']) && getParam($CNF['PARAM_USE_GALERY_AS_COVER']) == 'on'){
+            if(!empty($CNF['OBJECT_STORAGE_PHOTOS']) && !empty($CNF['OBJECT_IMAGES_TRANSCODER_PREVIEW_PHOTOS'])){
+				$sStorage = $CNF['OBJECT_STORAGE_PHOTOS'];
+				$oStorage = BxDolStorage::getObjectInstance($sStorage); 
+				$aGhostFiles = $oStorage->getGhosts ($this->serviceGetContentOwnerProfileId($aContentInfo[$CNF['FIELD_ID']]), $aContentInfo[$CNF['FIELD_ID']]);
+				if ($aGhostFiles){
+					foreach ($aGhostFiles as $k => $a) {
+                        return array('id' => $a['id'], 'transcoder' => $CNF['OBJECT_IMAGES_TRANSCODER_PREVIEW_PHOTOS']);
+					}
+				}
+                
+			}
+        }
+        
+        return $mResult;
+    }
+    
+    public function decodeDataAPI($aData, $aParams = [])
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sModule = $this->getName();
+        $iId = (int)$aData[$CNF['FIELD_ID']];
+
+        $sSummary = '';
+        if(!empty($CNF['PARAM_CHARS_SUMMARY_PLAIN'])) {
+            $sSummary = $this->_oTemplate->getText($aData);
+            $sSummary = BxTemplFunctions::getInstance()->getStringWithLimitedLength(strip_tags($sSummary), (int)getParam($CNF['PARAM_CHARS_SUMMARY_PLAIN']));
+        }
+
+        $aResult = [
+            'id' => $iId,
+            'module' => $sModule,
+            'module_title' => _t($CNF['T']['txt_sample_single']),
+            'added' => $aData[$CNF['FIELD_ADDED']],
+            'author' => $aData[$CNF['FIELD_AUTHOR']],
+            'author_data' => !empty($aData[$CNF['FIELD_AUTHOR']]) ? BxDolProfile::getData($aData[$CNF['FIELD_AUTHOR']]) : '',
+            'title' => $aData[$CNF['FIELD_TITLE']],
+            'url' => bx_api_get_relative_url($this->serviceGetLink($iId)),
+            'image' => $this->serviceGetThumb($iId),
+            'summary_plain' => $sSummary
+        ];
+
+        if(isset($aParams['extended']) && $aParams['extended'] === true)
+            $aResult['text'] = $aData[$CNF['FIELD_TEXT']];
+
+        if(!empty($CNF['OBJECT_MENU_SNIPPET_META']) && ($oMetaMenu = BxDolMenu::getObjectInstance($CNF['OBJECT_MENU_SNIPPET_META'], $this->_oTemplate)) !== false) {
+            $oMetaMenu->setContentId($iId);
+
+            $aResult['meta'] = $oMetaMenu->getCodeAPI();
+        }
+
+        return $aResult;
+    }
+
+
+    // ====== PROTECTED METHODS
+
+    protected function _getImagesForTimelinePost($aEvent, $aContentInfo, $sUrl, $aBrowseParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $aResults = parent::_getImagesForTimelinePost($aEvent, $aContentInfo, $sUrl, $aBrowseParams);
+        
+        if (bx_is_api())
+            return $aResults;
+        
+        if (count($aResults) == 0 && isset($CNF['PARAM_USE_GALERY_AS_COVER']) && getParam($CNF['PARAM_USE_GALERY_AS_COVER']) == 'on'){
+            $aResults = $this->_getImagesForTimelinePostAttachInner($aEvent, $aContentInfo, $sUrl, $aBrowseParams);
+            if (count($aResults) > 1){
+                $aResults = array_slice($aResults, 0, 1);
+            }
+        }
+        return $aResults;
+    }
+
+    protected function _getImagesForTimelinePostAttach($aEvent, $aContentInfo, $sUrl, $aBrowseParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $aTmp = parent::_getImagesForTimelinePost($aEvent, $aContentInfo, $sUrl, $aBrowseParams);
+        $aResults = $this->_getImagesForTimelinePostAttachInner($aEvent, $aContentInfo, $sUrl, $aBrowseParams);
+        
+        if (bx_is_api())
+            return $aResults;
+        
+        if (count($aTmp) == 0 && count($aResults) > 0 && isset($CNF['PARAM_USE_GALERY_AS_COVER']) && getParam($CNF['PARAM_USE_GALERY_AS_COVER']) == 'on'){
+            $aResults = array_slice($aResults, 1);
+        }
+        return $aResults;
+    }
+    
+    protected function _getImagesForTimelinePostAttachInner($aEvent, $aContentInfo, $sUrl, $aBrowseParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aResults = parent::_getImagesForTimelinePostAttach($aEvent, $aContentInfo, $sUrl, $aBrowseParams);
+        
+        if(!$this->_oConfig->isAttachmentsInTimeline() || empty($CNF['OBJECT_STORAGE_PHOTOS']) || empty($CNF['OBJECT_IMAGES_TRANSCODER_GALLERY_PHOTOS']))
+            return $aResults;
+
+        $iContentId = (int)$aContentInfo[$CNF['FIELD_ID']];
+        $oStorage = BxDolStorage::getObjectInstance($CNF['OBJECT_STORAGE_PHOTOS']);
+
+        $aGhostFiles = $oStorage->getGhosts($this->serviceGetContentOwnerProfileId($iContentId), $iContentId);
+        if(!$aGhostFiles)
+            return [];
+
+        $oTranscoderSm = false;
+        if(!empty($CNF['OBJECT_IMAGES_TRANSCODER_MINIATURE_PHOTOS']))
+            $oTranscoderSm = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_MINIATURE_PHOTOS']);
+
+        $oTranscoderMd = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_GALLERY_PHOTOS']);
+
+        $oTranscoderXl = false;
+        if(!empty($CNF['OBJECT_IMAGES_TRANSCODER_VIEW_PHOTOS']))
+            $oTranscoderXl = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_VIEW_PHOTOS']);
+
+        foreach($aGhostFiles as $k => $a) {
+            $sPhotoSrcMd = $oTranscoderMd->getFileUrl($a['id']);
+            if(empty($sPhotoSrcMd))
+                continue;
+
+            $sPhotoSrcSm = $oTranscoderSm !== false ? $oTranscoderSm->getFileUrl($a['id']) : '';
+            if(!$sPhotoSrcSm)
+                $sPhotoSrcSm = $sPhotoSrcMd;
+
+            $sPhotoSrcXl = $oTranscoderXl !== false ? $oTranscoderXl->getFileUrl($a['id']) : $oStorage->getFileUrlById($a['id']);
+            if(!$sPhotoSrcXl)
+                $sPhotoSrcXl = $sPhotoSrcMd;
+
+            if (bx_is_api()){
+                $aResults[] = bx_api_get_image($CNF['OBJECT_STORAGE_PHOTOS'], (int)$a['id']);
+            }
+            else{
+                $aResults[] = [
+                    'id' => $a['id'],
+                    'src' => $sPhotoSrcMd,
+                    'src_small' => $sPhotoSrcSm,
+                    'src_medium' => $sPhotoSrcMd,
+                    'src_orig' => $sPhotoSrcXl,
+                ];
+            }
+        }
+        return $aResults;
+    }
+
+    protected function _getVideosForTimelinePostAttach($aEvent, $aContentInfo, $sUrl, $aBrowseParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aResults = parent::_getVideosForTimelinePostAttach($aEvent, $aContentInfo, $sUrl, $aBrowseParams); 
+        if(!$this->_oConfig->isAttachmentsInTimeline() || empty($CNF['OBJECT_STORAGE_VIDEOS']) || empty($CNF['OBJECT_VIDEOS_TRANSCODERS']))
+            return $aResults;
+
+        $iContentId = (int)$aContentInfo[$CNF['FIELD_ID']];
+        $oStorage = BxDolStorage::getObjectInstance($CNF['OBJECT_STORAGE_VIDEOS']);
+
+        $aGhostFiles = $oStorage->getGhosts($this->serviceGetContentOwnerProfileId($iContentId), $iContentId);
+        if(!$aGhostFiles)
+            return array();
+
+        $oTcPoster = BxDolTranscoderVideo::getObjectInstance($CNF['OBJECT_VIDEOS_TRANSCODERS']['poster']);
+        $oTcMp4 = BxDolTranscoderVideo::getObjectInstance($CNF['OBJECT_VIDEOS_TRANSCODERS']['mp4']);
+        $oTcMp4Hd = BxDolTranscoderVideo::getObjectInstance($CNF['OBJECT_VIDEOS_TRANSCODERS']['mp4_hd']);
+        if(!$oTcPoster || !$oTcMp4 || !$oTcMp4Hd)
+            return array();
+
+        $aResults = array();
+        foreach ($aGhostFiles as $k => $a) {
+            $sVideoUrl = $oStorage->getFileUrlById($a['id']);
+            $aVideoFile = $oStorage->getFile($a['id']);
+
+            $sVideoUrlHd = '';
+            if (!empty($aVideoFile['dimensions']) && $oTcMp4Hd->isProcessHD($aVideoFile['dimensions']))
+                $sVideoUrlHd = $oTcMp4Hd->getFileUrl($a['id']);
+
+            $aResults[$a['id']] = array(
+                'id' => $a['id'],
+                'src_poster' => $oTcPoster->getFileUrl($a['id']),
+                'src_mp4' => $oTcMp4->getFileUrl($a['id']),
+                'src_mp4_hd' => $sVideoUrlHd,
+            );
+        }
+
+        return $aResults;
+    }
+
+    protected function _getFilesForTimelinePostAttach($aEvent, $aContentInfo, $sUrl, $aBrowseParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aResults = parent::_getFilesForTimelinePostAttach($aEvent, $aContentInfo, $sUrl, $aBrowseParams);
+        if(!$this->_oConfig->isAttachmentsInTimeline() || empty($CNF['OBJECT_STORAGE_FILES']))
+            return $aResults;
+
+        $iContentId = (int)$aContentInfo[$CNF['FIELD_ID']];
+        $oStorage = BxDolStorage::getObjectInstance($CNF['OBJECT_STORAGE_FILES']);
+
+        $aGhostFiles = $oStorage->getGhosts($this->serviceGetContentOwnerProfileId($iContentId), $iContentId);
+        if(!$aGhostFiles)
+            return array();
+
+        $oTranscoder = null;
+        if(!empty($CNF['OBJECT_IMAGES_TRANSCODER_GALLERY_FILES']))
+            $oTranscoder = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_GALLERY_FILES']);
+
+        foreach ($aGhostFiles as $k => $a) {
+            $bImage = in_array($a['ext'], array('jpg', 'jpeg', 'png', 'gif'));
+
+            $sPhotoSrc = '';
+            if($bImage && $oTranscoder)
+                $sPhotoSrc = $oTranscoder->getFileUrl($a['id']);
+
+            if(empty($sPhotoSrc) && $oStorage)
+                $sPhotoSrc = $this->_oTemplate->getIconUrl($oStorage->getIconNameByFileName($a['file_name']));
+
+            $sUrl = $oStorage->getFileUrlById($a['id']);
+
+            $aResults[] = array(
+                'id' => $a['id'],
+                'src' => $sPhotoSrc,
+                'src_medium' => $sPhotoSrc,
+                'src_orig' => $bImage ? $sUrl : '',
+                'url' => !$bImage ? $sUrl : ''
+            );
+        }
+
+        return $aResults;
+    }
+
+    protected function _getContentForTimelinePost($aEvent, $aContentInfo, $aBrowseParams = array())
+    {
+        $aResult = parent::_getContentForTimelinePost($aEvent, $aContentInfo, $aBrowseParams);
+        if(!$this->_oConfig->isAttachmentsInTimeline())
+            return $aResult;
+
+        $CNF = &$this->_oConfig->CNF;
+
+        $bDynamic = isset($aBrowseParams['dynamic_mode']) && (bool)$aBrowseParams['dynamic_mode'] === true;
+		if(!empty($CNF['PARAM_POLL_ENABLED']) && $CNF['PARAM_POLL_ENABLED'] == true)
+        	$aPolls = $this->_oDb->getPolls(array('type' => 'content_id', 'content_id' => (int)$aContentInfo[$CNF['FIELD_ID']]));
+        if(!empty($aPolls) && is_array($aPolls)) {
+            $sInclude = $this->_oTemplate->addCss(array('polls.css'), $bDynamic);
+
+            $aResult['raw'] = ($bDynamic ? $sInclude : '') . $this->_oTemplate->parseHtmlByName('poll_items_embed.html', array(
+                'embed_url' => BX_DOL_URL_ROOT . bx_append_url_params($this->_oConfig->getBaseUri() . 'embed_polls/', array(
+                    'id' => (int)$aContentInfo[$CNF['FIELD_ID']],
+                    'param_switch_menu' => 0,
+                    'param_showcase' => 1
+                ))
+            ));
+/*
+            if((!empty($aResult['videos']) && is_array($aResult['videos'])) || (!empty($aResult['videos_attach']) && is_array($aResult['videos_attach'])))
+                $aResult['_cache'] = false;
+ */
+        }
+        return $aResult;
+    }
+
+    protected function _buildRssParams($sMode, $aArgs)
+    {
+        $aParams = array ();
+        $sMode = bx_process_input($sMode);
+        switch ($sMode) {
+            case 'author':
+                $aParams = array('author' => isset($aArgs[0]) ? (int)$aArgs[0] : '');
+                break;
+        }
+
+        return $aParams;
+    }
+
+    public function serviceIsAllowedPostInContext()
+    {
+        return true;
+    }
+}
+
+/** @} */

+ 168 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/text/classes/BxBaseModTextPageEntry.php

@@ -0,0 +1,168 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseText Base classes for text modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Entry create/edit pages
+ */
+class BxBaseModTextPageEntry extends BxBaseModGeneralPageEntry
+{
+    protected $_bLoggedOwner;
+    protected $_bLoggedModerator;
+    protected $_bLoggedContextModerator;
+
+    public function __construct($aObject, $oTemplate = false)
+    {
+        parent::__construct($aObject, $oTemplate);
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(($iContentId = bx_get('id')) !== false)
+            $this->setContentId(bx_process_input($iContentId, BX_DATA_INT));
+
+        $sTitle = $sUrl = $sIcon = "";
+        if ($this->_aContentInfo && CHECK_ACTION_RESULT_ALLOWED === $this->_oModule->checkAllowedView($this->_aContentInfo)) {
+            $sTitle = $this->_oModule->_oTemplate->getTitleAuto($this->_aContentInfo);
+            $sUrl = bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $this->_aContentInfo[$CNF['FIELD_ID']]));
+            $sIcon = $CNF['ICON'];
+
+            $this->addMarkers($this->_aContentInfo); // every field can be used as marker
+        }
+
+        $this->addMarkers(array(
+            'title' => $sTitle,
+            'entry_link' => $sUrl,
+        ));
+
+        // select view entry submenu
+        $this->_setSubmenu(array(
+            'title' => $sTitle,
+            'link' => $sUrl,
+            'icon' => $sIcon
+        ));
+
+        $aInformers = array ();
+        $oInformer = BxDolInformer::getInstance($this->_oTemplate);
+        if($oInformer && ($this->_bLoggedOwner || $this->_bLoggedModerator || $this->_bLoggedContextModerator)) {
+            $iNow = time();
+            $bFieldPublished = isset($CNF['FIELD_PUBLISHED']);
+            $sStatus = isset($CNF['FIELD_STATUS']) && isset($this->_aContentInfo[$CNF['FIELD_STATUS']]) ? $this->_aContentInfo[$CNF['FIELD_STATUS']] : '';
+            $sStatusAdmin = isset($CNF['FIELD_STATUS_ADMIN']) && isset($this->_aContentInfo[$CNF['FIELD_STATUS_ADMIN']]) ? $this->_aContentInfo[$CNF['FIELD_STATUS_ADMIN']] : '';
+
+            //--- Display 'approving' informer.
+            if(!empty($sStatusAdmin) && $sStatusAdmin != BX_BASE_MOD_GENERAL_STATUS_ACTIVE) {
+                if(!empty($CNF['INFORMERS']['approving']) && isset($CNF['INFORMERS']['approving']['map'][$sStatusAdmin])) {
+                    $aInformer = $CNF['INFORMERS']['approving'];
+                    $aInformers[] = array ('name' => $aInformer['name'], 'msg' => _t($aInformer['map'][$sStatusAdmin]['msg']), 'type' => $aInformer['map'][$sStatusAdmin]['type']);
+                }
+            }
+
+            //--- Display 'processing' informer if an item was already published but awaiting/failed to be processed.
+            if(!$bFieldPublished || (isset($this->_aContentInfo[$CNF['FIELD_PUBLISHED']]) && $this->_aContentInfo[$CNF['FIELD_PUBLISHED']] <= $iNow)) {
+                if(!empty($CNF['INFORMERS']['processing']) && isset($CNF['INFORMERS']['processing']['map'][$sStatus])) {
+                    $aInformer = $CNF['INFORMERS']['processing'];
+                    $aInformers[] = array ('name' => $aInformer['name'], 'msg' => _t($aInformer['map'][$sStatus]['msg']), 'type' => $aInformer['map'][$sStatus]['type']);
+                }
+            }
+            //--- Display 'scheduled' informer if an item wasn't published yet.
+            else if($bFieldPublished) {
+                if(!empty($CNF['INFORMERS']['scheduled']) && isset($CNF['INFORMERS']['scheduled']['map'][$sStatus])) {
+                    $this->addMarkers(array(
+                        'date_publish_uf' => bx_time_js((int)$this->_aContentInfo[$CNF['FIELD_PUBLISHED']], BX_FORMAT_DATE, true)
+                    ));
+
+                    $aInformer = $CNF['INFORMERS']['scheduled'];
+                    $aInformers[] = array ('name' => $aInformer['name'], 'msg' => _t($aInformer['map'][$sStatus]['msg']), 'type' => $aInformer['map'][$sStatus]['type']);
+                }
+            }
+
+            // add informers
+            if($aInformers)
+                foreach($aInformers as $aInformer)
+                    $oInformer->add($aInformer['name'], $this->_replaceMarkers($aInformer['msg']), $aInformer['type']);
+        }
+    }
+
+    public function setContentId($iId)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        $iProfileId = bx_get_logged_profile_id();
+
+        $this->_aContentInfo = $this->_oModule->_oDb->getContentInfoById($iId);
+
+        $this->_bLoggedOwner = isset($this->_aContentInfo[$CNF['FIELD_AUTHOR']]) && $this->_aContentInfo[$CNF['FIELD_AUTHOR']] == $iProfileId;
+        $this->_bLoggedModerator = $this->_oModule->checkAllowedEditAnyEntry() === CHECK_ACTION_RESULT_ALLOWED;
+        $this->_bLoggedContextModerator = false;
+        if($this->_aContentInfo && !empty($CNF['FIELD_ALLOW_VIEW_TO']) && (int)$this->_aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']] < 0) {
+            $iContextProfileId = abs((int)$this->_aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]);
+            if(($oContextProfile = BxDolProfile::getInstance($iContextProfileId)) !== false) {
+                $aAdmins = bx_srv($oContextProfile->getModule(), 'get_admins_to_manage_content', [$iContextProfileId]);
+                if(!empty($aAdmins) && in_array($iProfileId, $aAdmins))
+                    $this->_bLoggedContextModerator = true;
+            }
+        }
+    }
+
+    public function getCode ()
+    {
+        $mixedResult = parent::getCode();
+
+        /*
+         * Hide actions menu from View Page cover because 
+         * cover area should be decorative in Text Based modules.
+         */
+        $oMenuSubmenu = BxDolMenu::getObjectInstance('sys_site_submenu');
+        if($oMenuSubmenu)
+            $oMenuSubmenu->setObjectActionsMenu(false);
+
+        return $mixedResult;
+    }
+
+    protected function _isAvailablePage($a)
+    {
+        if(!$this->_aContentInfo || !$this->_oModule->isEntryActive($this->_aContentInfo))
+            return false;
+        
+        return parent::_isAvailablePage($a);
+    }
+
+    protected function _setSubmenu($aParams)
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+        $oMenuSubmenu = BxDolMenu::getObjectInstance('sys_site_submenu');
+        if(!$oMenuSubmenu)
+            return false;
+
+        $oMenuSubmenu->setObjectSubmenu(isset($this->_aObject['submenu']) && $this->_aObject['submenu'] ? $this->_aObject['submenu'] : $CNF['OBJECT_MENU_SUBMENU_VIEW_ENTRY'], array_merge(array(
+            'title' => '',
+            'link' => '',
+            'icon' => '',
+        ), $aParams));
+
+        return true;
+    }
+
+    protected function _getBlockService ($aBlock)
+    {
+        $a = parent::_getBlockService ($aBlock);
+        $sTest = '_view_entry_comments';
+        if (false !== strpos($aBlock['content'], 'entity_comments') && substr_compare($this->_sObject, $sTest, strlen($this->_sObject) - strlen($sTest), strlen($sTest)) === 0)
+            unset($a['title']);            
+        return $a;
+    }
+
+    protected function _getImageForPageCover ()
+    {
+        return false;
+    }
+}
+
+/** @} */

+ 1124 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/modules/base/text/classes/BxBaseModTextTemplate.php

@@ -0,0 +1,1124 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseText Base classes for text modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Module representation.
+ */
+class BxBaseModTextTemplate extends BxBaseModGeneralTemplate
+{
+    protected $_sUnitClassShowCase;
+    
+    function __construct(&$oConfig, &$oDb)
+    {
+        parent::__construct($oConfig, $oDb);
+        
+        $this->_sUnitClassShowCase = 'bx-base-unit-showcase bx-base-text-unit-showcase bx-def-margin-sec-bottom';
+    }
+
+    public function addLocationBase()
+    {
+        parent::addLocationBase();
+
+        $this->addLocation('mod_text', BX_DIRECTORY_PATH_MODULES . 'base' . DIRECTORY_SEPARATOR . 'text' . DIRECTORY_SEPARATOR, BX_DOL_URL_MODULES . 'base/text/');
+    }
+
+    public function getJsCode($sType, $aParams = array(), $bWrap = true)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        $aParams = array_merge(array(
+            'aHtmlIds' => $this->_oConfig->getHtmlIds(),
+            'sEditorId' => isset($CNF['FIELD_TEXT_ID']) ? $CNF['FIELD_TEXT_ID'] : ''
+        ), $aParams);
+
+        return parent::getJsCode($sType, $aParams, $bWrap);
+    }
+
+    function unit ($aData, $isCheckPrivateContent = true, $sTemplateName = 'unit.html', $aParams = array())
+    {
+        if(!empty($aParams['template_name']))
+            $sTemplateName = $aParams['template_name'];
+        else 
+            $aParams['template_name'] = $sTemplateName;
+
+    	$sResult = $this->checkPrivacy ($aData, $isCheckPrivateContent, $this->getModule(), $sTemplateName);
+    	if($sResult)
+            return $sResult;            
+
+        $aTemplateVars = $this->getUnit($aData, $aParams);
+        bx_alert($this->getModule()->getName(), 'unit', 0, 0, array(
+            'data' => $aData,
+            'check_private_content' => $isCheckPrivateContent,
+            'template' => $sTemplateName,
+            'params' => $aParams,
+            'tmpl_name' => &$sTemplateName,
+            'tmpl_vars' => &$aTemplateVars
+        ));
+
+        return $this->parseHtmlByName($sTemplateName, $aTemplateVars);
+    }
+
+    function entryAttachments ($aData, $aParams = array())
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        $aStoragesKeys = array('OBJECT_STORAGE_PHOTOS', 'OBJECT_STORAGE_VIDEOS', 'OBJECT_STORAGE_SOUNDS', 'OBJECT_STORAGE_FILES');
+
+        $aStorages = array();
+        foreach($aStoragesKeys as $sKey)
+            if(!empty($CNF[$sKey]))
+                $aStorages[] = $CNF[$sKey];
+
+        if(!empty($aStorages))
+            return $this->entryAttachmentsByStorage($aStorages, $aData, array_merge($aParams, array('filter_field' => '')));
+
+        return parent::entryAttachments($aData, $aParams);
+    }
+
+    function entryAuthor ($aData, $iProfileId = false, $sFuncAuthorDesc = 'getAuthorDesc', $sTemplateName = 'author.html', $sFuncAuthorAddon = 'getAuthorAddon')
+    {
+        $oModule = $this->getModule();
+        $CNF = &$oModule->_oConfig->CNF;
+
+        if (!$iProfileId)
+            $iProfileId = $aData[$CNF['FIELD_AUTHOR']];
+
+        if($this->_bIsApi) {
+            $sFldAdd = 'FIELD_ADDED';
+            $sFldAvt = 'FIELD_ALLOW_VIEW_TO';
+
+            return [
+                bx_api_get_block('entity_author', [
+                    'author_data' => BxDolProfile::getData($iProfileId),
+                    'entry_date' => !empty($CNF[$sFldAdd]) && !empty($aData[$CNF[$sFldAdd]]) ? $aData[$CNF[$sFldAdd]] : '',
+                    'entry_context' => !empty($CNF[$sFldAvt]) && (int)$aData[$CNF[$sFldAvt]] < 0 ? BxDolProfile::getData(abs((int)$aData[$CNF[$sFldAvt]])) : '',
+                    'menu_manage' => $oModule->getEntryAllActions()
+                ])
+            ];
+        }
+
+        $oProfile = BxDolProfile::getInstanceMagic($iProfileId);
+        $sName = $oProfile->getDisplayName();
+        $sAddon = $sFuncAuthorAddon && is_a($oProfile, 'BxDolProfile') ? $this->$sFuncAuthorAddon($aData, $oProfile) : '';        
+
+        return $this->parseHtmlByName($sTemplateName, [
+            'author_url' => $oProfile->getUrl(),
+            'author_thumb_url' => $oProfile->getThumb(),
+            'author_unit' => $oProfile->getUnit(0, ['template' => 'unit_wo_info']),
+            'author_title' => $sName,
+            'author_title_attr' => bx_html_attribute($sName),
+            'author_desc' => $sFuncAuthorDesc ? $this->$sFuncAuthorDesc($aData, $oProfile) : '',
+            'author_profile_desc' => $this->getAuthorProfileDesc($aData, $oProfile),
+            'bx_if:addon' => [
+                'condition' => (bool)$sAddon,
+                'content' => [
+                    'content' => $sAddon,
+                ],
+            ],
+        ]);
+    }
+
+    public function entryBreadcrumb($aContentInfo, $aTmplVarsItems = array())
+    {
+        if(!empty($aTmplVarsItems) && is_array($aTmplVarsItems))
+            return parent::entryBreadcrumb($aContentInfo, $aTmplVarsItems);
+
+    	$CNF = &$this->getModule()->_oConfig->CNF;
+
+        $aTmplVarsItems = array();
+        if(!empty($CNF['OBJECT_CATEGORY']) && !empty($CNF['FIELD_CATEGORY'])) {
+            $oCategory = BxDolCategory::getObjectInstance($CNF['OBJECT_CATEGORY']);
+
+            $aParams = [];
+            if(isset($CNF['URL_CATEGORY']))
+                $aParams['page'] = bx_append_url_params(BxDolPermalinks::getInstance()->permalink($CNF['URL_CATEGORY']), [
+                    'category' => '{keyword}'
+                ], true, ['{keyword}']);
+
+            $aTmplVarsItems[] = array(
+                'url' => $oCategory->getCategoryUrl($aContentInfo[$CNF['FIELD_CATEGORY']], $aParams),
+                'title' => $oCategory->getCategoryTitle($aContentInfo[$CNF['FIELD_CATEGORY']])
+            );
+        }
+
+    	$aTmplVarsItems[] = array(
+            'url' => BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]),
+            'title' => bx_process_output($aContentInfo[$CNF['FIELD_TITLE']])
+        );
+
+    	return parent::entryBreadcrumb($aContentInfo, $aTmplVarsItems);
+    }
+
+    public function entryPolls($aData)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+        
+        $aPolls = $this->_oDb->getPolls(array(
+            'type' => 'content_id', 
+            'content_id' => $aData[$CNF['FIELD_ID']], 
+        ));
+        if(empty($aPolls) || !is_array($aPolls))
+            return;
+
+        $iProfileId = bx_get_logged_profile_id();
+
+        $sPolls = '';
+        foreach($aPolls as $aPoll)
+            $sPolls .= $this->getPollItem($aPoll, $iProfileId);
+
+        if(empty($sPolls))
+            return '';
+
+        $this->_addCssJs();
+        return $this->getJsCode('poll') . $this->parseHtmlByName('poll_items.html', array(
+            'polls' => $sPolls
+        ));
+    }
+
+    public function entryPollAnswers($aPoll, $bDynamic = false)
+    {
+        $sContent = $this->_getPollAnswers($aPoll, $bDynamic);
+        if(empty($sContent))
+            return '';
+
+    	return array(
+            'content' => $sContent,
+            'menu' => $this->_getPollBlockMenu($aPoll, 'answers')
+        ); 
+    }
+
+    public function entryPollResults($aPoll, $bDynamic = false)
+    {
+        $sContent = $this->_getPollResults($aPoll, $bDynamic);
+        if(empty($sContent))
+            return '';
+
+        return array(
+            'content' => $sContent,
+            'menu' => $this->_getPollBlockMenu($aPoll, 'results')
+        );  
+    }
+
+    public function getPollForm()
+    {
+        $aForm = $this->getModule()->getPollForm();
+
+        return $this->parseHtmlByName('poll_form.html', array(
+            'js_object' => $this->_oConfig->getJsObject('poll'),
+            'form_id' => $aForm['form_id'],
+            'form' => $aForm['form'],
+        ));
+    }
+    
+    public function getPollField($iContentId = 0, $iProfileId = 0)
+    {
+        if(empty($iProfileId))
+            $iProfileId = bx_get_logged_profile_id();
+
+        $aPolls = array();
+        if(!empty($iContentId))
+            $aPolls = array_merge($aPolls, $this->_oDb->getPolls(array(
+                'type' => 'content_id', 
+                'content_id' => $iContentId, 
+            )));
+
+        $aPolls = array_merge($aPolls, $this->_oDb->getPolls(array(
+            'type' => 'author_id', 
+            'author_id' => $iProfileId, 
+            'unused' => true
+        )));
+
+        $sPolls = '';
+        foreach($aPolls as $aPoll)
+            $sPolls .= $this->getPollItem($aPoll, $iProfileId, array(
+                'manage' => true
+            ));
+
+        return $this->parseHtmlByName('poll_form_field.html', array(
+            'html_id' => $this->_oConfig->getHtmlIds('add_poll_form_field'),
+            'polls' => $sPolls
+        ));
+    }
+
+    public function getPollItem($mixedPoll, $iProfileId = 0, $aParams = array())
+    {
+        $oModule = $this->getModule();
+        $CNF = &$oModule->_oConfig->CNF;
+
+        $aPoll = is_array($mixedPoll) ? $mixedPoll : $this->_oDb->getPolls(array('type' => 'id', 'id' => (int)$mixedPoll));
+        if(empty($aPoll) || !is_array($aPoll))
+            return '';
+
+        $sJsObject = $this->_oConfig->getJsObject('poll');
+
+        $bDynamic = isset($aParams['dynamic']) && $aParams['dynamic'] === true;
+        $bManage = isset($aParams['manage']) && $aParams['manage'] === true;
+        $bSwitchMenu = isset($aParams['switch_menu']) ? (bool)$aParams['switch_menu'] : true;
+        $bForceDisplayAnswers = isset($aParams['force_display_answers']) && (bool)$aParams['force_display_answers'] === true;
+
+        $iPollId = (int)$aPoll[$CNF['FIELD_POLL_ID']];
+        $sPollView = !$bForceDisplayAnswers && $oModule->isPollPerformed($iPollId, $iProfileId) ? 'results' : 'answers';
+        
+        $sMethod = '_getPoll' . ucfirst($sPollView);
+        if(!method_exists($this, $sMethod))
+            return '';
+
+        $mixedMenu = '';
+        if($bSwitchMenu)
+            $mixedMenu = $this->_getPollBlockMenu($aPoll, $sPollView, array('template' => 'menu_interactive.html'));
+
+        return $this->parseHtmlByName('poll_item.html', array(
+            'html_id' => $this->_oConfig->getHtmlIds('poll') . $iPollId,
+            'bx_if:show_input_hidden' => array(
+                'condition' => $bManage,
+                'content' => array(
+                    'name' => $CNF['FIELD_POLL'],
+                    'id' => $iPollId
+                )
+            ),
+            'action_menu' => !empty($mixedMenu) ? $mixedMenu->getCode() : '',
+            'bx_if:show_action_embed' => array(
+                'condition' => $bManage,
+                'content' => array(
+                    'js_object' => $sJsObject,
+                    'id' => $iPollId
+                )
+            ),
+            'bx_if:show_action_delete' => array(
+                'condition' => $bManage,
+                'content' => array(
+                    'js_object' => $sJsObject,
+                    'id' => $iPollId,
+                    'editor_id' => isset($CNF['FIELD_TEXT_ID']) ? $CNF['FIELD_TEXT_ID'] : ''
+                )
+            ),
+            'text' => bx_process_output($aPoll[$CNF['FIELD_POLL_TEXT']], BX_DATA_TEXT),
+            'content' => $this->$sMethod($aPoll, $bDynamic)
+        ));
+    }
+
+    public function embedPollItem($mixedPoll, $aParams = array())
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        $sHeader = '';
+        $sContent = $this->getPollItem($mixedPoll, 0, $aParams);
+        if(!empty($sContent)) {
+            $aPoll = is_array($mixedPoll) ? $mixedPoll : $this->_oDb->getPolls(array('type' => 'id', 'id' => (int)$mixedPoll));
+
+            $sHeader = strmaxtextlen($aPoll[$CNF['FIELD_POLL_TEXT']], 32, '...');
+            $sContent = $this->getJsCode('poll') . $sContent;
+        }
+
+        $this->_addCssJs();
+
+        $oTemplate = BxDolTemplate::getInstance();
+        $oTemplate->addCssStyle($CNF['STYLES_POLLS_EMBED_CLASS'], $CNF['STYLES_POLLS_EMBED_CONTENT']);
+        $oTemplate->setPageNameIndex(BX_PAGE_EMBED);
+        $oTemplate->setPageHeader($sHeader);
+        $oTemplate->setPageContent('page_main_code', $sContent);
+        $oTemplate->getPageCode();
+        exit;
+    }
+
+    public function embedPollItems($mixedContentInfo, $aParams = array())
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        $aContentInfo = is_array($mixedContentInfo) ? $mixedContentInfo : $this->_oDb->getContentInfoById((int)$mixedContentInfo);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return;
+
+        $iContentId = (int)$aContentInfo[$CNF['FIELD_ID']];
+
+        $aPolls = $this->_oDb->getPolls(array('type' => 'content_id', 'content_id' => $iContentId));
+        if(empty($aPolls) || !is_array($aPolls))
+            return;
+
+        $iPolls = 0;
+        $sContent = '';
+        foreach($aPolls as $aPoll) {
+            $sPoll = $this->getPollItem($aPoll, 0, $aParams);
+            if(empty($sPoll))
+                continue;
+
+            $sContent .= $sPoll;
+            $iPolls += 1;
+        }
+
+        if(!empty($sContent) && isset($aParams['showcase']) && (bool)$aParams['showcase'] === true) {
+            $this->addJs(array('flickity/flickity.pkgd.min.js'));
+            $this->addCss(BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'flickity/|flickity.css');
+
+            $sContent = $this->parseHtmlByName('poll_items_showcase.html', array(
+                'js_object' => $this->_oConfig->getJsObject('poll'),
+                'html_id' => $this->_oConfig->getHtmlIds('polls_showcase') . $iContentId,
+                'type' => $iPolls == 1 ? 'single' : 'multiple',
+                'polls' => $sContent
+            ));
+        }
+
+        $sHeader = '';
+        if(!empty($sContent)) {
+            $sHeader = $iPolls == 1 ? array_shift($aPolls)[$CNF['FIELD_POLL_TEXT']] : _t('_polls_from', $aContentInfo[$CNF['FIELD_TITLE']]);
+            $sHeader = strmaxtextlen($sHeader, 32, '...');
+
+            $sContent = $this->getJsCode('poll') . $sContent;
+        }
+
+        $this->_addCssJs();
+
+        $oTemplate = BxDolTemplate::getInstance();
+        $oTemplate->addCssStyle($CNF['STYLES_POLLS_EMBED_CLASS'], $CNF['STYLES_POLLS_EMBED_CONTENT']);
+        $oTemplate->setPageNameIndex(BX_PAGE_EMBED);
+        $oTemplate->setPageHeader($sHeader);
+        $oTemplate->setPageContent('page_main_code', $sContent);
+        $oTemplate->getPageCode();
+        exit;
+    }
+
+    protected function _addCssJs()
+    {
+        $this->addJs(array(
+            'modules/base/text/js/|polls.js',
+            'polls.js'
+        ));
+        $this->addCss(array('polls.css'));
+    }
+
+    protected function _getPollAnswers($aPoll, $bDynamic = false)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+        
+        $aAnswers = $this->_oDb->getPollAnswers(array('type' => 'poll_id', 'poll_id' => $aPoll[$CNF['FIELD_POLL_ID']]));
+        if(empty($aAnswers) || !is_array($aAnswers))
+            return '';
+
+        $aTmplVarsAnswers = array();
+        foreach($aAnswers as $aAnswer) {
+            $oVotes = BxDolVote::getObjectInstance($CNF['OBJECT_VOTES_POLL_ANSWERS'], $aAnswer['id']);
+
+            $aTmplVarsAnswers[] = array(
+                'answer' => $oVotes->getElementBlock(array(
+                    'dynamic_mode' => $bDynamic
+                ))
+            );
+        }
+
+    	return $this->parseHtmlByName('poll_item_answers.html', array(
+            'html_id' => $this->_oConfig->getHtmlIds('poll_content') . $aPoll[$CNF['FIELD_POLL_ID']],
+            'bx_repeat:answers' => $aTmplVarsAnswers
+        ));
+    }
+    
+    protected function _getPollResults($aPoll, $bDynamic = false)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        $aAnswers = $this->_oDb->getPollAnswers(array('type' => 'poll_id', 'poll_id' => $aPoll[$CNF['FIELD_POLL_ID']]));
+        if(empty($aAnswers) || !is_array($aAnswers))
+            return '';
+
+        $iTotal = 0;
+        foreach($aAnswers as $aAnswer)
+            $iTotal += $aAnswer['votes'];
+
+        $aTmplVarsAnswers = array();
+        foreach($aAnswers as $aAnswer) {
+            $oVotes = BxDolVote::getObjectInstance($CNF['OBJECT_VOTES_POLL_ANSWERS'], $aAnswer['id']);
+            $aVotesParams = array('show_counter_empty' => true, 'show_counter_in_brackets' => false, 'dynamic_mode' => $bDynamic);
+
+            $fPercent = $iTotal > 0 ? 100 * (float)$aAnswer['votes']/$iTotal : 0;
+            $aTmplVarsAnswers[] = array(
+                'title' => bx_process_output($aAnswer['title']),
+                'width' => (int)round($fPercent) . '%',
+                'votes' => $oVotes->getCounter($aVotesParams),
+                'percent' => _t($CNF['T']['txt_poll_answer_vote_percent'], $iTotal > 0 ? round($fPercent, 2) : 0)
+            );
+        }
+
+        return $this->parseHtmlByName('poll_item_results.html', array(
+            'html_id' => $this->_oConfig->getHtmlIds('poll_content') . $aPoll[$CNF['FIELD_POLL_ID']],
+            'bx_repeat:answers' => $aTmplVarsAnswers,
+        ));
+    }
+
+    protected function _getPollBlockMenu($aPoll, $sSelected = '', $aParams = array())
+    {
+        $oModule = $this->getModule();
+        $CNF = &$oModule->_oConfig->CNF;
+
+        $sPostfix = '-' . time() . rand(0, PHP_INT_MAX);
+        $sJsObject = $this->_oConfig->getJsObject('poll');
+        $iPollId = $aPoll[$CNF['FIELD_POLL_ID']];
+
+        $aViews = array(
+            'answers' => true, 
+            'results' => $CNF['PARAM_POLL_HIDDEN_RESULTS'] === false || $oModule->isPollPerformed($iPollId)
+        );
+
+        $aMenu = array();
+        foreach($aViews as $sView => $bActive) {
+            if(!$bActive) 
+                continue;
+
+            $sId = $this->_oConfig->getHtmlIds('poll_view_link_' . $sView) . $iPollId;
+            if(!empty($sSelected) && $sSelected == $sView)
+                $sSelected = $sId;
+
+            $aMenu[] = array(
+                'id' => $sId, 
+                'name' => $sId, 
+                'class' => '', 
+                'link' => 'javascript:void(0)', 
+                'onclick' => 'javascript:' . $sJsObject . '.changePollView(this, \'' . $sView . '\', ' . $iPollId . ')', 
+                'target' => '_self', 
+                'title_attr' => _t($CNF['T']['txt_poll_menu_view_' . $sView]), 
+                'title' => $this->parseIcon($CNF['ICON_POLLS_' . strtoupper($sView)])
+            );
+        }
+
+        if(count($aMenu) <= 1)
+            return '';
+
+        $oMenu = new BxTemplMenuInteractive(array(
+            'template' => !empty($aParams['template']) ? $aParams['template'] : 'menu_interactive_vertical.html', 
+            'menu_id' => $this->_oConfig->getHtmlIds('poll_view_menu') . $sPostfix, 
+            'menu_items' => $aMenu
+        ));
+
+        if(!empty($sSelected))
+            $oMenu->setSelected('', $sSelected);
+
+        return $oMenu;
+    }
+    
+    public function getAttachLinkForm($iContentId = 0)
+    {
+        $sStylePrefix = $this->_oConfig->getPrefix('style');
+        $sJsObject = $this->_oConfig->getJsObject('links');
+
+        $aForm = $this->getModule()->getFormAttachLink($iContentId);
+
+        return $this->parseHtmlByName('attach_link_form.html', array(
+            'style_prefix' => $sStylePrefix,
+            'js_object' => $sJsObject,
+            'form_id' => $aForm['form_id'],
+            'form' => $aForm['form'],
+        ));
+    }
+    
+    public function getAttachLinkField($iUserId, $iContentId = 0)
+    {
+        $sStylePrefix = $this->_oConfig->getPrefix('style');
+
+        if(!$iContentId)
+            $aLinks = $this->_oDb->getUnusedLinks($iUserId);
+        else
+            $aLinks = $this->_oDb->getLinks($iContentId);
+
+        $sLinks = '';
+        foreach($aLinks as $aLink)
+            $sLinks .= $this->getAttachLinkItem($iUserId, $aLink);
+
+        return $this->parseHtmlByName('attach_link_form_field.html', array(
+            'html_id' => $this->_oConfig->getHtmlIds('attach_link_form_field') . $iContentId,
+            'style_prefix' => $sStylePrefix,
+            'links' => $sLinks
+        ));
+    }
+
+    public function getAttachLinkItem($iUserId, $mixedLink)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+        
+        $aLink = is_array($mixedLink) ? $mixedLink : $this->_oDb->getLinksBy(array('type' => 'id', 'id' => (int)$mixedLink, 'profile_id' => $iUserId));
+        if(empty($aLink) || !is_array($aLink))
+            return '';
+
+        $sLinkIdPrefix = $this->_oConfig->getHtmlIds('attach_link_item');
+        $sStylePrefix = $this->_oConfig->getPrefix('style');
+        $sClass = $sStylePrefix . '-al-item';
+        $sJsObject = $this->_oConfig->getJsObject('links');
+
+        $oEmbed = BxDolEmbed::getObjectInstance();
+        $bEmbed = $oEmbed !== false;
+
+        $sThumbnail = '';
+        $aLinkAttrs = array();
+        if(!$bEmbed) {
+            $aLinkAttrs = array(
+            	'title' => bx_html_attribute($aLink['title'])
+            );
+            if(!$this->_oConfig->isEqualUrls(BX_DOL_URL_ROOT, $aLink['url'])) {
+                $aLinkAttrs['target'] = '_blank';
+    
+                if($this->_oDb->getParam('sys_add_nofollow') == 'on')
+            	    $aLinkAttrs['rel'] = 'nofollow';
+            }
+
+            if((int)$aLink['media_id'] != 0)
+                $sThumbnail = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_PREVIEW_PHOTOS'])->getFileUrl($aLink['media_id']);
+        }
+        else
+            $sClass .= ' embed';
+
+        return $this->parseHtmlByName('attach_link_item.html', array(
+            'html_id' => $sLinkIdPrefix . $aLink['id'],
+            'style_prefix' => $sStylePrefix,
+            'class' => $sClass,
+            'js_object' => $sJsObject,
+            'id' => $aLink['id'],
+            'bx_if:show_embed_outer' => array(
+                'condition' => $bEmbed,
+                'content' => array(
+                    'style_prefix' => $sStylePrefix,
+                    'embed' => $bEmbed ? $oEmbed->getLinkHTML($aLink['url'], $aLink['title'], 300) : '',
+                )
+            ),
+            'bx_if:show_embed_inner' => array(
+                'condition' => !$bEmbed,
+                'content' => array(
+                    'style_prefix' => $sStylePrefix,
+                    'bx_if:show_thumbnail' => array(
+                        'condition' => !empty($sThumbnail),
+                        'content' => array(
+                            'style_prefix' => $sStylePrefix,
+                            'thumbnail' => $sThumbnail
+                        )
+                    ),
+                    'url' => $aLink['url'],
+                    'link' => $this->parseLink($aLink['url'], $aLink['title'], $aLinkAttrs)
+                )
+            ),
+        ));
+    }
+
+    public function getEventLinks($iContentId)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+        
+        $aLinks = $this->_oDb->getLinks($iContentId);
+        if(empty($aLinks) || !is_array($aLinks))
+            return array();
+
+        $oTranscoder = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_PREVIEW_PHOTOS']);
+
+        $aResult = array();
+        foreach($aLinks as $aLink)
+            $aResult[] = array(
+                'url' => $aLink['url'],
+                'title' => $aLink['title'],
+                'text' => $aLink['text'],
+                'thumbnail' => (int)$aLink['media_id'] != 0 ? $oTranscoder->getFileUrl($aLink['media_id']) : ''
+            );
+
+        return $aResult;
+    }
+    
+    public function getTmplVarsText($aData)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+        $sStylePrefix = $this->_oConfig->getPrefix('style');
+
+        $aVars = parent::getTmplVarsText($aData);
+
+        $sImage = '';
+        $mixedImage = $this->_getHeaderImage($aData);
+        if($mixedImage !== false) {
+            if(!empty($mixedImage['object']))
+                $o = BxDolStorage::getObjectInstance($mixedImage['object']);
+            else if(!empty($mixedImage['transcoder']))
+                $o = BxDolTranscoder::getObjectInstance($mixedImage['transcoder']);
+
+            if($o)
+                $sImage = $o->getFileUrlById($mixedImage['id']);
+        }
+        
+        if(bx_is_api() ){
+            if($sImage){
+                $aVars['image'] = bx_api_get_image($CNF['OBJECT_STORAGE'], $mixedImage['id']);
+            }
+            return $aVars;
+        }
+        
+        $sAddClassPicture = "";
+        $sAddCode = "";
+        $oModule = $this->getModule();
+        $bIsAllowEditPicture =  CHECK_ACTION_RESULT_ALLOWED === $oModule->checkAllowedEdit($aData);
+
+        if(isset($CNF['FIELD_THUMB']) && isset($CNF['OBJECT_UPLOADERS']) && isset($CNF['OBJECT_STORAGE']) && isset($CNF['OBJECT_IMAGES_TRANSCODER_PREVIEW'])){
+            bx_alert('system', 'image_editor', 0, 0, array(
+                'module' => $oModule->getName(),
+                'content_id' => $aData[$CNF['FIELD_ID']],
+                'image_type' => 'header_image',
+                'image_url' => $sImage,
+                'is_allow_edit' => $bIsAllowEditPicture,
+                'uploader' => !empty($CNF['OBJECT_UPLOADERS']) && is_array($CNF['OBJECT_UPLOADERS']) ? $CNF['OBJECT_UPLOADERS'][0] : '',
+                'storage' => isset($CNF['OBJECT_STORAGE']) ? $CNF['OBJECT_STORAGE'] : '',
+                'transcoder' => isset($CNF['OBJECT_IMAGES_TRANSCODER_COVER']) ? $CNF['OBJECT_IMAGES_TRANSCODER_COVER'] : '',
+                'field' => isset($CNF['FIELD_THUMB']) ? $CNF['FIELD_THUMB'] : '',
+                'is_background' => false,
+                'add_class' => &$sAddClassPicture,
+                'add_code' => &$sAddCode
+            )); 
+        }
+
+        $sImageTweak = '';
+        $sUniqIdImage = genRndPwd (8, false);
+        if ($bIsAllowEditPicture && empty($sAddCode) && isset($CNF['FIELD_THUMB_POSITION'])){
+            $sImageTweak = $this->_prepareImage($aData, $sUniqIdImage, $CNF['OBJECT_UPLOADERS'], $CNF['OBJECT_STORAGE'], $CNF['FIELD_THUMB'], true);
+        }
+
+        
+        $aVars['content_description_before'] = '';
+        $aVars['content_description_after'] = '';
+        $aVars['bx_if:show_image'] = array(
+            'condition' => !empty($sImage),
+            'content' => array(
+                'entry_image' => $sImage,
+                'image_settings' => isset($CNF['FIELD_THUMB_POSITION']) ? $this->_getImageSettings($aData[$CNF['FIELD_THUMB_POSITION']]) : '',
+                'add_class' => $sAddClassPicture,
+                'img_class' => $sAddClassPicture != '' ? 'bx-media-editable-src' : '',
+                'additional_code' => $sAddCode,
+                'image_tweak' => $sImageTweak,
+                'unique_id' => $sUniqIdImage,
+            )
+        );
+        
+        $aTmplVarsLinks = [];
+        
+        if(isset($CNF['FIELD_LINK'])){
+            $aLinks = $this->getEventLinks($aData[$CNF['FIELD_ID']]);
+
+            $sClass = $sStylePrefix . '-al-item';
+            
+            $bAddNofollow = $this->_oDb->getParam('sys_add_nofollow') == 'on';
+            
+            foreach($aLinks as $aLink) {
+                $sLink = '';
+
+                $oEmbed = BxDolEmbed::getObjectInstance();
+                if ($oEmbed) {
+                    $sLink = $this->parseHtmlByName('link_embed_provider.html', array(
+                        'style_prefix' => $sStylePrefix,
+                        'embed' => $oEmbed->getLinkHTML($aLink['url'], $aLink['title']),
+                    ));
+                }
+                else {
+                    $aLinkAttrs = array(
+                    	'title' => $aLink['title']
+                    );
+                    if(!$this->_oConfig->isEqualUrls(BX_DOL_URL_ROOT, $aLink['url'])) {
+                        $aLinkAttrs['target'] = '_blank';
+    
+                        if($bAddNofollow)
+                    	    $aLinkAttrs['rel'] = 'nofollow';
+                    }
+
+                    $sLinkAttrs = '';
+                    foreach($aLinkAttrs as $sKey => $sValue)
+                        $sLinkAttrs .= ' ' . $sKey . '="' . bx_html_attribute($sValue) . '"';
+
+                    $sLink = $this->parseHtmlByName('link_embed_common.html', array(
+                        'bx_if:show_thumbnail' => [
+                            'condition' => !empty($aLink['thumbnail']),
+                            'content' => [
+                                'style_prefix' => $sStylePrefix,
+                                'thumbnail' => $aLink['thumbnail'],
+                                'link' => !empty($aLink['url']) ? $aLink['url'] : 'javascript:void(0)',
+                                'attrs' => $sLinkAttrs
+                            ]
+                        ],
+                        'link' => !empty($aLink['url']) ? $aLink['url'] : 'javascript:void(0)',
+                        'attrs' => $sLinkAttrs,
+                        'content' => $aLink['title'],
+                        'bx_if:show_text' => [
+                            'condition' => !empty($aLink['text']),
+                            'content' => [
+                                'style_prefix' => $sStylePrefix,
+                                'text' => $aLink['text']
+                            ]
+                        ]
+                    ));
+                }
+                
+                $aTmplVarsLinks[] = [
+                    'style_prefix' => $sStylePrefix,
+                    'link' => $sLink
+                ];
+            }
+        }
+        
+        $aVars['bx_if:show_links'] = [
+            'condition' => count($aTmplVarsLinks) > 0,
+            'content' => [
+                'style_prefix' => $sStylePrefix,
+                'bx_repeat:links' => $aTmplVarsLinks
+            ]
+        ];
+
+        if(!empty($CNF['OBJECT_REACTIONS']) && ($oReactions = BxDolVote::getObjectInstance($CNF['OBJECT_REACTIONS'], $aData[$CNF['FIELD_ID']])) !== false)
+            $aVars['content_description_after'] .= $oReactions->getCounter([
+                'show_counter' => true,
+                'show_counter_empty' => false
+            ]);
+        
+        $this->addCss(array('cover.css'));
+
+        return $aVars;
+    }
+
+    function getAuthorDesc($aData, $oProfile)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        $aItem = array(
+            'bx_if:text' => array(
+                'condition' => false,
+                'content' => array(
+                    'content' => ''
+                )
+            ),
+            'bx_if:link' => array(
+                'condition' => false,
+                'content' => array(
+                    'link' => '',
+                    'content' => ''
+                )
+            )
+        );
+
+        $aTmplVarsItems = array();
+        if(!empty($CNF['FIELD_ADDED']) && !empty($aData[$CNF['FIELD_ADDED']]))
+            $aTmplVarsItems[] = array_merge($aItem, array('bx_if:text' => array(
+                'condition' => true,
+                'content' => array(
+                    'content' => bx_time_js($aData[$CNF['FIELD_ADDED']], BX_FORMAT_DATE)
+                )
+            )));
+
+        if(!empty($CNF['URI_AUTHOR_ENTRIES']))
+            $aTmplVarsItems[] = array_merge($aItem, array('bx_if:link' => array(
+                'condition' => true,
+                'content' => array(
+                    'link' => BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_AUTHOR_ENTRIES'] . '&profile_id=' . $oProfile->id()),
+                    'content' => _t($CNF['T']['txt_all_entries_by'], $this->getModule()->serviceGetMenuAddonManageToolsProfileStats($oProfile->id()))
+                )
+            )));
+
+        return $this->parseHtmlByName('author_desc.html', array(
+            'bx_repeat:items' => $aTmplVarsItems
+        ));
+    }
+
+    function getAuthorProfileDesc ($aData, $oProfile)
+    {
+        $aSnippetMeta = $this->getProfileSnippetMenu($aData);
+        if(empty($aSnippetMeta) || !is_array($aSnippetMeta) || !isset($aSnippetMeta['meta']))
+            return '';
+
+        return $aSnippetMeta['meta'];
+    }
+
+    function getProfileSnippetMenu ($aData)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+        if (!($oProfile = BxDolProfile::getInstance($aData[$CNF['FIELD_AUTHOR']])))
+            return array();
+
+        return bx_srv($oProfile->getModule(), 'get_snippet_menu_vars', array($oProfile->id()));
+    }
+
+    function getAuthorAddon ($aData, $oProfile)
+    {
+        return '';
+    }
+
+    protected function checkPrivacy ($aData, $isCheckPrivateContent, $oModule, $sTemplateName = '')
+    {
+        if ($isCheckPrivateContent && CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $oModule->checkAllowedView($aData))) {
+            $aVars = array (
+                'summary' => $sMsg,
+            );
+            return $this->parseHtmlByName($sTemplateName ? str_replace('.html', '_private.html', $sTemplateName) : 'unit_private.html', $aVars);
+        }
+
+        return '';
+    }
+
+    protected function getUnitThumbAndGallery ($aData)
+    {
+        $CNF = &BxDolModule::getInstance($this->MODULE)->_oConfig->CNF;
+
+        $sPhotoThumb = '';
+        $sPhotoGallery = '';
+        if(!empty($CNF['FIELD_THUMB']) && !empty($aData[$CNF['FIELD_THUMB']])) {
+
+            $oImagesTranscoder = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_PREVIEW']);
+            if ($oImagesTranscoder)
+                $sPhotoThumb = $oImagesTranscoder->getFileUrl($aData[$CNF['FIELD_THUMB']]);
+
+            $oImagesTranscoder = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_GALLERY']);
+            if ($oImagesTranscoder)
+                $sPhotoGallery = $oImagesTranscoder->getFileUrl($aData[$CNF['FIELD_THUMB']]);
+            else
+                $sPhotoGallery = $sPhotoThumb;
+        }
+
+        return array($sPhotoThumb, $sPhotoGallery);
+    }
+
+    protected function getUnit ($aData, $aParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $oModule = $this->getModule();
+
+        // get thumb url
+        list($sPhotoThumb, $sPhotoGallery) = $this->getUnitThumbAndGallery($aData);
+
+        if ($sPhotoGallery == '' && isset($CNF['PARAM_USE_GALERY_AS_COVER']) && getParam($CNF['PARAM_USE_GALERY_AS_COVER']) == 'on'){
+            if(!empty($CNF['OBJECT_STORAGE_PHOTOS'])){
+                $sStorage = $CNF['OBJECT_STORAGE_PHOTOS'];
+                $oStorage = BxDolStorage::getObjectInstance($sStorage); 
+                list($oTranscoder, $oTranscoderView) = $this->getAttachmentsImagesTranscoders($sStorage);
+                $aGhostFiles = $oStorage->getGhosts($oModule->serviceGetContentOwnerProfileId($aData[$CNF['FIELD_ID']]), $aData[$CNF['FIELD_ID']]);
+                if(!empty($aGhostFiles) && is_array($aGhostFiles)) {
+                    $aGhostFile = array_shift($aGhostFiles);
+
+                    $sPhotoThumb = $oTranscoderView->getFileUrl($aGhostFile['id']);
+                    $sPhotoGallery = $oTranscoder->getFileUrl($aGhostFile['id']);
+                }
+            }
+        }
+
+        // get entry url
+        $sUrl = $this->_oConfig->getViewEntryUrl($aData[$CNF['FIELD_ID']]);
+
+        $oProfile = BxDolProfile::getInstanceMagic($aData[$CNF['FIELD_AUTHOR']]);
+        
+        $sTitle = $this->getTitle($aData);
+        $sText = $this->getText($aData);
+        $sSummary = $this->getSummary($aData, $sTitle, $sText, $sUrl);
+        $sSummaryPlain = isset($CNF['PARAM_CHARS_SUMMARY_PLAIN']) && $CNF['PARAM_CHARS_SUMMARY_PLAIN'] ? BxTemplFunctions::getInstance()->getStringWithLimitedLength(strip_tags($sSummary), (int)getParam($CNF['PARAM_CHARS_SUMMARY_PLAIN'])) : '';
+
+        if(!empty($CNF['OBJECT_METATAGS'])) {
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+            $sText = $oMetatags->metaParse($aData[$CNF['FIELD_ID']], $sText);
+        }
+
+        $aTmplVarsMeta = [];
+        if(!empty($CNF['OBJECT_MENU_SNIPPET_META'])) {
+            $oMenuMeta = BxDolMenu::getObjectInstance($CNF['OBJECT_MENU_SNIPPET_META'], $this);
+            if($oMenuMeta) {
+                $oMenuMeta->setContentId($aData[$CNF['FIELD_ID']]);
+                if(isset($aParams['context']))
+                    $oMenuMeta->setContext($aParams['context']);
+
+                $aTmplVarsMeta = [
+                    'meta' => $oMenuMeta->getCode()
+                ];
+            }
+        }
+
+        $bBadgesSingle = isset($aParams['badges_single']) ? $aParams['badges_single'] : false;
+        $bBadgesCompact = isset($aParams['badges_compact']) ? $aParams['badges_compact'] : false;
+
+        $sCoverData = isset($aData['thumb_data']) ? $aData['thumb_data'] : '';
+
+        $aTmplVars = array (
+            'class' => $this->_getUnitClass($aData,(isset($aParams['template_name']) ? $aParams['template_name'] : '')),
+            'html_id' => $this->_getUnitHtmlId($aData,(isset($aParams['template_name']) ? $aParams['template_name'] : '')),
+            'id' => $aData[$CNF['FIELD_ID']],
+            'content_url' => $sUrl,
+            'bx_if:show_onclick' => [
+                'condition' => false,
+                'content' => [
+                    'content_onclick' => ''
+                ]
+            ],
+            'title' => $sTitle,
+            'badges' => $oModule->serviceGetBadges($aData[$CNF['FIELD_ID']], $bBadgesSingle, $bBadgesCompact),
+            'title_attr' => bx_html_attribute($sTitle),
+            'summary' => $sSummary,
+            'text' => $sText,
+            'author' => $oProfile->getDisplayName(),
+            'author_url' => $oProfile->getUrl(),
+            'author_icon' => $oProfile->getIcon(),
+            'author_thumb' => $oProfile->getThumb(),
+            'author_avatar' => $oProfile->getAvatar(),
+            'entry_posting_date' => bx_time_js($aData[$CNF['FIELD_ADDED']], BX_FORMAT_DATE),
+            'module_name' => _t($CNF['T']['txt_sample_single']),
+            'ts' => $aData[$CNF['FIELD_ADDED']],
+            'bx_if:meta' => array(
+                'condition' => !empty($aTmplVarsMeta),
+                'content' => $aTmplVarsMeta
+            ),
+            'bx_if:thumb' => array (
+                'condition' => $sPhotoThumb,
+                'content' => array (
+                    'title' => $sTitle,
+                    'summary_attr' => bx_html_attribute($sSummaryPlain),
+                    'content_url' => $sUrl,
+                    'bx_if:show_thumb_onclick' => [
+                        'condition' => false,
+                        'content' => [
+                            'content_onclick' => ''
+                        ]
+                    ],
+                    'thumb_url' => $sPhotoThumb ? $sPhotoThumb : '',
+                    'gallery_url' => $sPhotoGallery ? $sPhotoGallery : '',
+                    'image_settings' => $this->_getImageSettings($sCoverData),
+                    'strecher' => str_repeat('&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ', 40),
+                ),
+            ),
+            'bx_if:no_thumb' => array (
+                'condition' => !$sPhotoThumb,
+                'content' => array (
+                    'module_icon' => $CNF['ICON'],
+                    'content_url' => $sUrl,
+                    'bx_if:show_no_thumb_onclick' => [
+                        'condition' => false,
+                        'content' => [
+                            'content_onclick' => ''
+                        ]
+                    ],
+                    'summary_plain' => $sSummaryPlain,
+                    'strecher' => mb_strlen($sSummaryPlain) > 240 ? '' : str_repeat('&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ', round((240 - mb_strlen($sSummaryPlain)) / 6)),
+                ),
+            ),
+            'unit_content_before' => '',
+            'unit_content_after' => ''
+        );
+
+        if(isset($aParams['template_vars']) && is_array($aParams['template_vars']))
+            $aTmplVars = array_merge($aTmplVars, $aParams['template_vars']);
+
+        // generate html
+        return $aTmplVars;
+    }
+
+    protected function getAttachmentsImagesTranscoders ($sStorage = '')
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        $oTranscoder = null;
+        $oTranscoderView = null;
+
+        if(isset($CNF['OBJECT_STORAGE_PHOTOS']) && $CNF['OBJECT_STORAGE_PHOTOS'] == $sStorage) {
+            if(!empty($CNF['OBJECT_IMAGES_TRANSCODER_PREVIEW_PHOTOS']))
+                $oTranscoder = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_PREVIEW_PHOTOS']);
+            if(!empty($CNF['OBJECT_IMAGES_TRANSCODER_VIEW_PHOTOS']))
+                $oTranscoderView = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_VIEW_PHOTOS']);
+            else if(!empty($CNF['OBJECT_IMAGES_TRANSCODER_GALLERY_PHOTOS']))
+                $oTranscoderView = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_GALLERY_PHOTOS']);
+        }
+        else if(isset($CNF['OBJECT_STORAGE_FILES']) && $CNF['OBJECT_STORAGE_FILES'] == $sStorage) {
+            if(!empty($CNF['OBJECT_IMAGES_TRANSCODER_PREVIEW_FILES']))
+                $oTranscoder = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_PREVIEW_FILES']);
+            if(!empty($CNF['OBJECT_IMAGES_TRANSCODER_GALLERY_FILES']))
+                $oTranscoderView = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_GALLERY_FILES']);
+        }
+        else
+            list($oTranscoder, $oTranscoderView) = parent::getAttachmentsImagesTranscoders($sStorage);
+
+        return array($oTranscoder, $oTranscoderView);
+    }
+
+    protected function _getUnitName($aData, $sTemplateName = 'unit.html')
+    {
+        return trim(str_replace('.html', '', $sTemplateName));
+    }
+
+    protected function _getUnitClass($aData, $sTemplateName = 'unit.html')
+    {
+        $sResult = '';
+
+        switch($sTemplateName) {
+            case 'unit_showcase.html':
+                $sResult = $this->_sUnitClassShowCase;
+                break;
+        }
+
+        return $sResult;
+    }
+
+    protected function _getUnitHtmlId($aData, $sTemplateName = 'unit.html')
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        return str_replace('_', '-', $this->MODULE . '_unit_' . $aData[$CNF['FIELD_ID']]);
+    }
+
+    protected function _getHeaderImage($aData)
+    {
+        return $this->getModule()->getEntryImageData($aData);
+    }
+    
+    function mediaExif ($aMediaInfo, $iProfileId = false, $sFuncAuthorDesc = '', $sTemplateName = 'media-exif.html') 
+    {
+        if (empty($aMediaInfo['exif']))
+            return '';
+
+        $a = unserialize($aMediaInfo['exif']);
+
+        $s = '';
+        if (!empty($a['Make'])) {
+            $oModule = BxDolModule::getInstance($this->MODULE);
+            $CNF = &$oModule->_oConfig->CNF;
+          
+            $sCamera = BxDolMetatags::keywordsCameraModel($a);
+            if (!empty($CNF['OBJECT_METATAGS_MEDIA_CAMERA'])) {
+                $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS_MEDIA_CAMERA']);
+                if ($oMetatags->keywordsIsEnabled()) {
+                    $sCamera = $oMetatags->keywordsParseOne($aMediaInfo['id'], $sCamera);
+                }
+            }
+
+            if (!empty($sCamera))
+                $s .= $this->parseHtmlByName('media-exif-value.html', array(
+                    'key' => _t($CNF['T']['txt_media_exif_camera']), 
+                    'val' => $sCamera,
+                ));
+        }
+        
+        if (!empty($a['FocalLength']))
+            $s .= $this->parseHtmlByName('media-exif-value.html', array(
+                'key' => _t($CNF['T']['txt_media_exif_focal_length']),
+                'val' => _t($CNF['T']['txt_media_exif_focal_length_value'], $a['FocalLength']),
+            ));
+
+        if (!empty($a['COMPUTED']['ApertureFNumber']))
+            $s .= $this->parseHtmlByName('media-exif-value.html', array(
+                'key' => _t($CNF['T']['txt_media_exif_aperture']),
+                'val' => $a['COMPUTED']['ApertureFNumber'],
+            ));
+
+        if (!empty($a['ExposureTime']))
+            $s .= $this->parseHtmlByName('media-exif-value.html', array(
+                'key' => _t($CNF['T']['txt_media_exif_shutter_speed']),
+                'val' => _t($CNF['T']['txt_media_exif_shutter_speed_value'], $a['ExposureTime']),
+            ));
+
+        if (!empty($a['ISOSpeedRatings']))
+            $s .= $this->parseHtmlByName('media-exif-value.html', array(
+                'key' => _t($CNF['T']['txt_media_exif_iso']),
+                'val' => $a['ISOSpeedRatings'],
+            ));
+
+        if (empty($s))
+            return '';
+        
+        return $this->parseHtmlByName($sTemplateName, array('content' => $s));
+    }
+}
+
+/** @} */

+ 7 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/plugins/autoload.php

@@ -0,0 +1,7 @@
+<?php
+
+// autoload.php @generated by Composer
+
+require_once __DIR__ . '/composer/autoload_real.php';
+
+return ComposerAutoloaderInitfa6d4080f02e144d0af2cc4b59aa222b::getLoader();

+ 75 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/plugins/composer/autoload_real.php

@@ -0,0 +1,75 @@
+<?php
+
+// autoload_real.php @generated by Composer
+
+class ComposerAutoloaderInitfa6d4080f02e144d0af2cc4b59aa222b
+{
+    private static $loader;
+
+    public static function loadClassLoader($class)
+    {
+        if ('Composer\Autoload\ClassLoader' === $class) {
+            require __DIR__ . '/ClassLoader.php';
+        }
+    }
+
+    /**
+     * @return \Composer\Autoload\ClassLoader
+     */
+    public static function getLoader()
+    {
+        if (null !== self::$loader) {
+            return self::$loader;
+        }
+
+        require __DIR__ . '/platform_check.php';
+
+        spl_autoload_register(array('ComposerAutoloaderInitfa6d4080f02e144d0af2cc4b59aa222b', 'loadClassLoader'), true, true);
+        self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
+        spl_autoload_unregister(array('ComposerAutoloaderInitfa6d4080f02e144d0af2cc4b59aa222b', 'loadClassLoader'));
+
+        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
+        if ($useStaticLoader) {
+            require __DIR__ . '/autoload_static.php';
+
+            call_user_func(\Composer\Autoload\ComposerStaticInitfa6d4080f02e144d0af2cc4b59aa222b::getInitializer($loader));
+        } else {
+            $map = require __DIR__ . '/autoload_namespaces.php';
+            foreach ($map as $namespace => $path) {
+                $loader->set($namespace, $path);
+            }
+
+            $map = require __DIR__ . '/autoload_psr4.php';
+            foreach ($map as $namespace => $path) {
+                $loader->setPsr4($namespace, $path);
+            }
+
+            $classMap = require __DIR__ . '/autoload_classmap.php';
+            if ($classMap) {
+                $loader->addClassMap($classMap);
+            }
+        }
+
+        $loader->register(true);
+
+        if ($useStaticLoader) {
+            $includeFiles = Composer\Autoload\ComposerStaticInitfa6d4080f02e144d0af2cc4b59aa222b::$files;
+        } else {
+            $includeFiles = require __DIR__ . '/autoload_files.php';
+        }
+        foreach ($includeFiles as $fileIdentifier => $file) {
+            composerRequirefa6d4080f02e144d0af2cc4b59aa222b($fileIdentifier, $file);
+        }
+
+        return $loader;
+    }
+}
+
+function composerRequirefa6d4080f02e144d0af2cc4b59aa222b($fileIdentifier, $file)
+{
+    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
+        require $file;
+
+        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
+    }
+}

+ 226 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/plugins/composer/autoload_static.php

@@ -0,0 +1,226 @@
+<?php
+
+// autoload_static.php @generated by Composer
+
+namespace Composer\Autoload;
+
+class ComposerStaticInitfa6d4080f02e144d0af2cc4b59aa222b
+{
+    public static $files = array (
+        '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
+        '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
+        '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
+        '3109cb1a231dcd04bee1f9f620d46975' => __DIR__ . '/..' . '/paragonie/sodium_compat/autoload.php',
+        '714ccd4b330431237faf946f71c4c9a4' => __DIR__ . '/..' . '/akeeba/s3/src/aliasing.php',
+        '2cffec82183ee1cea088009cef9a6fc3' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php',
+    );
+
+    public static $prefixLengthsPsr4 = array (
+        'T' => 
+        array (
+            'Twilio\\' => 7,
+        ),
+        'S' => 
+        array (
+            'Stripe\\' => 7,
+            'Spatie\\CalendarLinks\\' => 21,
+            'Snipe\\BanBuilder\\' => 17,
+            'Sample\\' => 7,
+        ),
+        'P' => 
+        array (
+            'Pusher\\' => 7,
+            'Psr\\Log\\' => 8,
+            'Psr\\Http\\Message\\' => 17,
+            'Psr\\Http\\Client\\' => 16,
+            'PayPalHttp\\' => 11,
+            'PayPalCheckoutSdk\\' => 18,
+            'PHPMailer\\PHPMailer\\' => 20,
+        ),
+        'M' => 
+        array (
+            'MatthiasMullie\\PathConverter\\' => 29,
+            'MatthiasMullie\\Minify\\' => 22,
+        ),
+        'I' => 
+        array (
+            'Intervention\\Image\\' => 19,
+        ),
+        'G' => 
+        array (
+            'GuzzleHttp\\Psr7\\' => 16,
+            'GuzzleHttp\\Promise\\' => 19,
+            'GuzzleHttp\\' => 11,
+        ),
+        'E' => 
+        array (
+            'Embera\\' => 7,
+        ),
+        'C' => 
+        array (
+            'ChargeBee\\' => 10,
+        ),
+        'A' => 
+        array (
+            'AvtoDev\\Composer\\Cleanup\\' => 25,
+            'Akeeba\\S3\\' => 10,
+        ),
+    );
+
+    public static $prefixDirsPsr4 = array (
+        'Twilio\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/twilio/sdk/src/Twilio',
+        ),
+        'Stripe\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/stripe/stripe-php/lib',
+        ),
+        'Spatie\\CalendarLinks\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/spatie/calendar-links/src',
+        ),
+        'Snipe\\BanBuilder\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/snipe/banbuilder/src',
+        ),
+        'Sample\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/paypal/paypal-checkout-sdk/samples',
+        ),
+        'Pusher\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/pusher/pusher-php-server/src',
+        ),
+        'Psr\\Log\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/psr/log/Psr/Log',
+        ),
+        'Psr\\Http\\Message\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/psr/http-message/src',
+            1 => __DIR__ . '/..' . '/psr/http-factory/src',
+        ),
+        'Psr\\Http\\Client\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/psr/http-client/src',
+        ),
+        'PayPalHttp\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/paypal/paypalhttp/lib/PayPalHttp',
+        ),
+        'PayPalCheckoutSdk\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/paypal/paypal-checkout-sdk/lib/PayPalCheckoutSdk',
+        ),
+        'PHPMailer\\PHPMailer\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/phpmailer/phpmailer/src',
+        ),
+        'MatthiasMullie\\PathConverter\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/matthiasmullie/path-converter/src',
+        ),
+        'MatthiasMullie\\Minify\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/matthiasmullie/minify/src',
+        ),
+        'Intervention\\Image\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/intervention/image/src/Intervention/Image',
+        ),
+        'GuzzleHttp\\Psr7\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src',
+        ),
+        'GuzzleHttp\\Promise\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/guzzlehttp/promises/src',
+        ),
+        'GuzzleHttp\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src',
+        ),
+        'Embera\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/mpratt/embera/src/Embera',
+        ),
+        'ChargeBee\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/chargebee/chargebee-php/lib',
+        ),
+        'AvtoDev\\Composer\\Cleanup\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/avto-dev/composer-cleanup-plugin/src',
+        ),
+        'Akeeba\\S3\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/akeeba/s3/src',
+        ),
+    );
+
+    public static $prefixesPsr0 = array (
+        'P' => 
+        array (
+            'Parsedown' => 
+            array (
+                0 => __DIR__ . '/..' . '/erusev/parsedown',
+            ),
+        ),
+        'O' => 
+        array (
+            'OAuth\\Unit' => 
+            array (
+                0 => __DIR__ . '/..' . '/lusitanian/oauth/tests',
+            ),
+            'OAuth2' => 
+            array (
+                0 => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src',
+            ),
+            'OAuth' => 
+            array (
+                0 => __DIR__ . '/..' . '/lusitanian/oauth/src',
+            ),
+        ),
+        'L' => 
+        array (
+            'Less' => 
+            array (
+                0 => __DIR__ . '/..' . '/wikimedia/less.php/lib',
+            ),
+        ),
+        'H' => 
+        array (
+            'HTMLPurifier' => 
+            array (
+                0 => __DIR__ . '/..' . '/ezyang/htmlpurifier/library',
+            ),
+        ),
+        'C' => 
+        array (
+            'ChargeBee' => 
+            array (
+                0 => __DIR__ . '/..' . '/chargebee/chargebee-php/lib',
+            ),
+        ),
+    );
+
+    public static $classMap = array (
+        'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+        'S3v4\\S3' => __DIR__ . '/..' . '/tpyo/amazon-s3-php-class/S3.php',
+        'S3v4\\S3Exception' => __DIR__ . '/..' . '/tpyo/amazon-s3-php-class/S3.php',
+        'S3v4\\S3Request' => __DIR__ . '/..' . '/tpyo/amazon-s3-php-class/S3.php',
+        'lessc' => __DIR__ . '/..' . '/wikimedia/less.php/lessc.inc.php',
+    );
+
+    public static function getInitializer(ClassLoader $loader)
+    {
+        return \Closure::bind(function () use ($loader) {
+            $loader->prefixLengthsPsr4 = ComposerStaticInitfa6d4080f02e144d0af2cc4b59aa222b::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInitfa6d4080f02e144d0af2cc4b59aa222b::$prefixDirsPsr4;
+            $loader->prefixesPsr0 = ComposerStaticInitfa6d4080f02e144d0af2cc4b59aa222b::$prefixesPsr0;
+            $loader->classMap = ComposerStaticInitfa6d4080f02e144d0af2cc4b59aa222b::$classMap;
+
+        }, null, ClassLoader::class);
+    }
+}

+ 375 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/plugins/composer/installed.php

@@ -0,0 +1,375 @@
+<?php return array(
+    'root' => array(
+        'pretty_version' => 'dev-master',
+        'version' => 'dev-master',
+        'type' => 'library',
+        'install_path' => __DIR__ . '/../../',
+        'aliases' => array(),
+        'reference' => 'ebc3767da19b4b8bdc3e1acb921c66705899f903',
+        'name' => 'unaio/una',
+        'dev' => true,
+    ),
+    'versions' => array(
+        'achingbrain/php5-akismet' => array(
+            'pretty_version' => '0.5',
+            'version' => '0.5.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../achingbrain/php5-akismet',
+            'aliases' => array(),
+            'reference' => 'master',
+            'dev_requirement' => false,
+        ),
+        'akeeba/s3' => array(
+            'pretty_version' => 'dev-development',
+            'version' => 'dev-development',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../akeeba/s3',
+            'aliases' => array(
+                0 => '9999999-dev',
+            ),
+            'reference' => '982d78d9f35c15bbfeff42a5f2a6e6ac4fd60f48',
+            'dev_requirement' => false,
+        ),
+        'avto-dev/composer-cleanup-plugin' => array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'type' => 'composer-plugin',
+            'install_path' => __DIR__ . '/../avto-dev/composer-cleanup-plugin',
+            'aliases' => array(
+                0 => '9999999-dev',
+            ),
+            'reference' => '75187ca4854265174021765e3b67cb973dc37723',
+            'dev_requirement' => false,
+        ),
+        'bshaffer/oauth2-server-php' => array(
+            'pretty_version' => 'v1.14.1',
+            'version' => '1.14.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../bshaffer/oauth2-server-php',
+            'aliases' => array(),
+            'reference' => '096db2c86a7d67a2ba45e72be7d208c342694542',
+            'dev_requirement' => false,
+        ),
+        'chargebee/chargebee-php' => array(
+            'pretty_version' => 'v3.23.0',
+            'version' => '3.23.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../chargebee/chargebee-php',
+            'aliases' => array(),
+            'reference' => 'e821b81adf4b6334c0705f1e13471abbe41e94db',
+            'dev_requirement' => false,
+        ),
+        'curl/cacert' => array(
+            'pretty_version' => '0.0.1',
+            'version' => '0.0.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../curl/cacert',
+            'aliases' => array(),
+            'reference' => NULL,
+            'dev_requirement' => false,
+        ),
+        'erusev/parsedown' => array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../erusev/parsedown',
+            'aliases' => array(
+                0 => '9999999-dev',
+            ),
+            'reference' => '77947eda2fdaf06b181c63a7db13e38968306aee',
+            'dev_requirement' => false,
+        ),
+        'ezyang/htmlpurifier' => array(
+            'pretty_version' => 'v4.17.0',
+            'version' => '4.17.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../ezyang/htmlpurifier',
+            'aliases' => array(),
+            'reference' => 'bbc513d79acf6691fa9cf10f192c90dd2957f18c',
+            'dev_requirement' => false,
+        ),
+        'ffmpeg/ffmpeg' => array(
+            'pretty_version' => '4.4.0',
+            'version' => '4.4.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../ffmpeg/ffmpeg',
+            'aliases' => array(),
+            'reference' => NULL,
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/guzzle' => array(
+            'pretty_version' => '7.8.1',
+            'version' => '7.8.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/guzzle',
+            'aliases' => array(),
+            'reference' => '41042bc7ab002487b876a0683fc8dce04ddce104',
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/promises' => array(
+            'pretty_version' => '2.0.2',
+            'version' => '2.0.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/promises',
+            'aliases' => array(),
+            'reference' => 'bbff78d96034045e58e13dedd6ad91b5d1253223',
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/psr7' => array(
+            'pretty_version' => '2.6.2',
+            'version' => '2.6.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/psr7',
+            'aliases' => array(),
+            'reference' => '45b30f99ac27b5ca93cb4831afe16285f57b8221',
+            'dev_requirement' => false,
+        ),
+        'intervention/image' => array(
+            'pretty_version' => '2.7.2',
+            'version' => '2.7.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../intervention/image',
+            'aliases' => array(),
+            'reference' => '04be355f8d6734c826045d02a1079ad658322dad',
+            'dev_requirement' => false,
+        ),
+        'lusitanian/oauth' => array(
+            'pretty_version' => 'v0.8.11',
+            'version' => '0.8.11.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../lusitanian/oauth',
+            'aliases' => array(),
+            'reference' => 'fc11a53db4b66da555a6a11fce294f574a8374f9',
+            'dev_requirement' => false,
+        ),
+        'matthiasmullie/minify' => array(
+            'pretty_version' => '1.3.71',
+            'version' => '1.3.71.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../matthiasmullie/minify',
+            'aliases' => array(),
+            'reference' => 'ae42a47d7fecc1fbb7277b2f2d84c37a33edc3b1',
+            'dev_requirement' => false,
+        ),
+        'matthiasmullie/path-converter' => array(
+            'pretty_version' => '1.1.3',
+            'version' => '1.1.3.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../matthiasmullie/path-converter',
+            'aliases' => array(),
+            'reference' => 'e7d13b2c7e2f2268e1424aaed02085518afa02d9',
+            'dev_requirement' => false,
+        ),
+        'mpratt/embera' => array(
+            'pretty_version' => '2.0.36',
+            'version' => '2.0.36.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../mpratt/embera',
+            'aliases' => array(),
+            'reference' => '661452ae5933ce458161faea578a59280428ca8c',
+            'dev_requirement' => false,
+        ),
+        'paragonie/random_compat' => array(
+            'pretty_version' => 'v9.99.100',
+            'version' => '9.99.100.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../paragonie/random_compat',
+            'aliases' => array(),
+            'reference' => '996434e5492cb4c3edcb9168db6fbb1359ef965a',
+            'dev_requirement' => false,
+        ),
+        'paragonie/sodium_compat' => array(
+            'pretty_version' => 'v1.20.0',
+            'version' => '1.20.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../paragonie/sodium_compat',
+            'aliases' => array(),
+            'reference' => 'e592a3e06d1fa0d43988c7c7d9948ca836f644b6',
+            'dev_requirement' => false,
+        ),
+        'paypal/paypal-checkout-sdk' => array(
+            'pretty_version' => '1.0.2',
+            'version' => '1.0.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../paypal/paypal-checkout-sdk',
+            'aliases' => array(),
+            'reference' => '19992ce7051ff9e47e643f28abb8cc1b3e5f1812',
+            'dev_requirement' => false,
+        ),
+        'paypal/paypalhttp' => array(
+            'pretty_version' => '1.0.1',
+            'version' => '1.0.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../paypal/paypalhttp',
+            'aliases' => array(),
+            'reference' => '7b09c89c80828e842c79230e7f156b61fbb68d25',
+            'dev_requirement' => false,
+        ),
+        'phpmailer/phpmailer' => array(
+            'pretty_version' => 'v6.9.1',
+            'version' => '6.9.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../phpmailer/phpmailer',
+            'aliases' => array(),
+            'reference' => '039de174cd9c17a8389754d3b877a2ed22743e18',
+            'dev_requirement' => false,
+        ),
+        'psr/http-client' => array(
+            'pretty_version' => '1.0.3',
+            'version' => '1.0.3.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-client',
+            'aliases' => array(),
+            'reference' => 'bb5906edc1c324c9a05aa0873d40117941e5fa90',
+            'dev_requirement' => false,
+        ),
+        'psr/http-client-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        'psr/http-factory' => array(
+            'pretty_version' => '1.0.2',
+            'version' => '1.0.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-factory',
+            'aliases' => array(),
+            'reference' => 'e616d01114759c4c489f93b099585439f795fe35',
+            'dev_requirement' => false,
+        ),
+        'psr/http-factory-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        'psr/http-message' => array(
+            'pretty_version' => '2.0',
+            'version' => '2.0.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-message',
+            'aliases' => array(),
+            'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71',
+            'dev_requirement' => false,
+        ),
+        'psr/http-message-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        'psr/log' => array(
+            'pretty_version' => '1.1.4',
+            'version' => '1.1.4.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/log',
+            'aliases' => array(),
+            'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11',
+            'dev_requirement' => false,
+        ),
+        'pusher/pusher-php-server' => array(
+            'pretty_version' => '7.2.4',
+            'version' => '7.2.4.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../pusher/pusher-php-server',
+            'aliases' => array(),
+            'reference' => 'de2f72296808f9cafa6a4462b15a768ff130cddb',
+            'dev_requirement' => false,
+        ),
+        'ralouphie/getallheaders' => array(
+            'pretty_version' => '3.0.3',
+            'version' => '3.0.3.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../ralouphie/getallheaders',
+            'aliases' => array(),
+            'reference' => '120b605dfeb996808c31b6477290a714d356e822',
+            'dev_requirement' => false,
+        ),
+        'snipe/banbuilder' => array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../snipe/banbuilder',
+            'aliases' => array(
+                0 => '9999999-dev',
+            ),
+            'reference' => '5e76a97b3ff80e2beaae75ac140c38573ea027a0',
+            'dev_requirement' => false,
+        ),
+        'spatie/calendar-links' => array(
+            'pretty_version' => '1.8.0',
+            'version' => '1.8.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../spatie/calendar-links',
+            'aliases' => array(),
+            'reference' => '654fc0174e6279539b9bf0abdc3339e34c5286d7',
+            'dev_requirement' => false,
+        ),
+        'stripe/stripe-php' => array(
+            'pretty_version' => 'v7.128.0',
+            'version' => '7.128.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../stripe/stripe-php',
+            'aliases' => array(),
+            'reference' => 'c704949c49b72985c76cc61063aa26fefbd2724e',
+            'dev_requirement' => false,
+        ),
+        'symfony/deprecation-contracts' => array(
+            'pretty_version' => 'v2.5.2',
+            'version' => '2.5.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
+            'aliases' => array(),
+            'reference' => 'e8b495ea28c1d97b5e0c121748d6f9b53d075c66',
+            'dev_requirement' => false,
+        ),
+        'tpyo/amazon-s3-php-class' => array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../tpyo/amazon-s3-php-class',
+            'aliases' => array(
+                0 => '9999999-dev',
+            ),
+            'reference' => '7c62b0e9b7d956a04e377ca236979a78dbac4e66',
+            'dev_requirement' => false,
+        ),
+        'twilio/sdk' => array(
+            'pretty_version' => '5.42.2',
+            'version' => '5.42.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../twilio/sdk',
+            'aliases' => array(),
+            'reference' => '0cfcb871b18a9c427dd9e8f0ed7458d43009b48a',
+            'dev_requirement' => false,
+        ),
+        'unaio/amazon-s3-php-class-hmac-v2' => array(
+            'pretty_version' => '0.6',
+            'version' => '0.6.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../unaio/amazon-s3-php-class-hmac-v2',
+            'aliases' => array(),
+            'reference' => '3687dec0af7be2cb11f21828aa4b12d20c4c5935',
+            'dev_requirement' => false,
+        ),
+        'unaio/una' => array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../../',
+            'aliases' => array(),
+            'reference' => 'ebc3767da19b4b8bdc3e1acb921c66705899f903',
+            'dev_requirement' => false,
+        ),
+        'wikimedia/less.php' => array(
+            'pretty_version' => 'v4.1.1',
+            'version' => '4.1.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../wikimedia/less.php',
+            'aliases' => array(),
+            'reference' => '7c814ca156716e7b46ad84ac038bf298ff91cbb4',
+            'dev_requirement' => false,
+        ),
+    ),
+);

+ 1326 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/plugins_public/tailwind/css/tailwind.css

@@ -0,0 +1,1326 @@
+/* tailwind source */
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer utilities {
+    .animation-delay-2000 {
+        animation-delay: 2s;
+    }
+    .animation-delay-4000 {
+        animation-delay: 4s;
+    }
+}
+
+@layer components {
+    /* Global: HTML tags */
+    ol, ul {
+        @apply pl-10 list-outside;
+    }
+
+    ul {
+        @apply list-disc;
+    }
+
+    ol {
+        @apply list-decimal;
+    }
+
+
+    /* Defaults */
+    .bx-def-vanilla-html {
+        @apply prose dark:prose-invert;
+        @apply prose-a:text-blue-500 prose-a:font-medium prose-a:no-underline hover:prose-a:underline visited:prose-a:text-purple-500;
+    }
+    
+    .bx-def-vanilla-html.bx-def-vh-sm {
+        @apply prose-sm;
+    }
+    .bx-def-vanilla-html img::after,
+    .bx-def-vanilla-html p::after{
+        content: '';
+        display: block;
+        clear: both;
+    }
+    
+    .bx-def-vanilla-html div > :where(:first-child):not(:where([class~="not-prose"] *)),
+    .bx-def-vanilla-html > :where(:first-child):not(:where([class~="not-prose"] *)){
+        margin-top: 0 !important;
+    }
+    
+    .bx-def-vanilla-html div > :where(:last-child):not(:where([class~="not-prose"] *)),
+    .bx-def-vanilla-html > :where(:last-child):not(:where([class~="not-prose"] *)) {
+        margin-bottom: 0 !important;
+    }
+
+    .bx-artificer .bx-menthion-link {
+        @apply font-normal text-blue-600 hover:text-blue-700 dark:text-blue-500 dark:hover:text-blue-600;
+    }
+
+    .bx-artificer .bx-def-box-sizing {
+        @apply box-border;
+    }
+
+    .bx-artificer .bx-def-align-center {
+        @apply flex items-center justify-center !important;
+    }
+
+    .bx-artificer .bx-def-valign-center {
+        @apply flex items-center !important;
+    }
+
+
+    /* Color Palette */
+    .bx-artificer .bx-def-color-bg-hl,
+    .bx-artificer .bx-def-color-bg-hl-even:nth-child(even),
+    .bx-artificer .bx-def-color-bg-hl-hover:hover {
+        @apply bg-gray-100/50 dark:bg-gray-700/50;
+    }
+
+    .bx-artificer .col-red1 {
+        @apply text-red-500;
+    }
+    .bx-artificer .bg-col-red1 { 
+        @apply bg-red-400;
+    }
+    .bx-artificer .col-red1-dark { 
+        @apply text-red-800; 
+    }
+    .bx-artificer .bg-col-red1-dark { 
+        @apply bg-red-700;
+    }
+
+    .bx-artificer .col-red2 { 
+        @apply text-rose-500; 
+    }
+    .bx-artificer .bg-col-red2 { 
+        @apply bg-rose-400;
+    }
+    .bx-artificer .col-red2-dark { 
+        @apply text-rose-800;
+    }
+    .bx-artificer .bg-col-red2-dark {
+        @apply bg-rose-700;
+    }
+
+    .bx-artificer .col-red3 {
+        @apply text-orange-500;
+    }
+    .bx-artificer .bg-col-red3 {
+        @apply bg-orange-400;
+    }
+    .bx-artificer .col-red3-dark {
+        @apply text-orange-800;
+    }
+    .bx-artificer .bg-col-red3-dark {
+        @apply bg-orange-700;
+    }
+
+    .bx-artificer .col-green1 {
+        @apply text-green-500;
+    }
+    .bx-artificer .bg-col-green1 {
+        @apply bg-green-400;
+    }
+    .bx-artificer .col-green1-dark {
+        @apply text-green-800;
+    }
+    .bx-artificer .bg-col-green1-dark {
+        @apply bg-green-700;
+    }
+
+    .bx-artificer .col-green2 {
+        @apply text-teal-500;
+    }
+    .bx-artificer .bg-col-green2 {
+        @apply bg-teal-400;
+    }
+    .bx-artificer .col-green2-dark {
+        @apply text-teal-800;
+    }
+    .bx-artificer .bg-col-green2-dark {
+        @apply bg-teal-700;
+    }
+
+    .bx-artificer .col-green3 {
+        @apply text-emerald-500;
+    }
+    .bx-artificer .bg-col-green3 {
+        @apply bg-emerald-400;
+    }
+    .bx-artificer .col-green3-dark {
+        @apply text-emerald-800;
+    }
+    .bx-artificer .bg-col-green3-dark {
+        @apply bg-emerald-700;
+    }
+
+    .bx-artificer .col-blue1 {
+        @apply text-blue-500;
+    }
+    .bx-artificer .bg-col-blue1 {
+        @apply bg-blue-400;
+    }
+    .bx-artificer .col-blue1-dark {
+        @apply text-blue-800;
+    }
+    .bx-artificer .bg-col-blue1-dark {
+        @apply bg-blue-700;
+    }
+
+    .bx-artificer .col-blue2 {
+        @apply text-purple-500;
+    }
+    .bx-artificer .bg-col-blue2 {
+        @apply bg-purple-400;
+    }
+    .bx-artificer .col-blue2-dark {
+        @apply text-purple-800;
+    }
+    .bx-artificer .bg-col-blue2-dark {
+        @apply bg-purple-700;
+    }
+
+    .bx-artificer .col-blue3 {
+        @apply text-sky-500;
+    }
+    .bx-artificer .bg-col-blue3 {
+        @apply bg-sky-400;
+    }
+    .bx-artificer .col-blue3-dark {
+        @apply text-sky-800;
+    }
+    .bx-artificer .bg-col-blue3-dark {
+        @apply bg-sky-700;
+    }
+
+    .bx-artificer .col-gray {
+        @apply text-gray-500;
+    }
+    .bx-artificer .bg-col-gray {
+        @apply bg-gray-400;
+    }
+    .bx-artificer .col-gray-dark {
+        @apply text-gray-800;
+    }
+    .bx-artificer .bg-col-gray-dark {
+        @apply bg-gray-700;
+    }
+
+    /* Margins and Paddings */
+    .bx-artificer .bx-def-centered {
+        @apply mx-auto;
+    }
+    .bx-artificer .bx-def-margin {
+        @apply m-4;
+    }
+    .bx-artificer .bx-def-margin-neg {
+        @apply -m-4;
+    }
+    .bx-artificer .bx-def-margin-left {
+        @apply ml-4;
+    }
+    .bx-artificer .bx-def-margin-left-auto {
+        @apply ml-4 first:ml-0;
+    }
+    .bx-artificer .bx-def-margin-right {
+        @apply mr-4;
+    }
+    .bx-artificer .bx-def-margin-top {
+        @apply mt-4;
+    }
+    .bx-artificer .bx-def-margin-top-auto {
+        @apply mt-4 first:mt-0;
+    }
+    .bx-artificer .bx-def-margin-bottom {
+        @apply mb-4;
+    }
+    .bx-artificer .bx-def-margin-leftright {
+        @apply mx-4;
+    }
+    .bx-artificer .bx-def-margin-leftright-neg {
+        @apply -mx-4;
+    }
+    .bx-artificer .bx-def-margin-topbottom {
+        @apply my-4;
+    }
+    .bx-artificer .bx-def-margin-topbottom-neg {
+        @apply -my-4;
+    }
+    .bx-artificer .bx-def-margin-lefttopright {
+        @apply mx-4 mt-4;
+    }
+    .bx-artificer .bx-def-margin-rightbottomleft {
+        @apply mx-4 mb-4;
+    }
+    
+    .bx-artificer .bx-def-margin-sec {
+        @apply m-2;
+    }
+    .bx-artificer .bx-def-margin-sec-neg {
+        @apply -m-2;
+    }
+    .bx-artificer .bx-def-margin-sec-left {
+        @apply ml-2;
+    }
+    .bx-artificer .bx-def-margin-sec-left-auto {
+        @apply ml-2 first:ml-0;
+    }
+    .bx-artificer .bx-def-margin-sec-right {
+        @apply mr-2;
+    }
+    .bx-artificer .bx-def-margin-sec-top {
+        @apply mt-2;
+    }
+    .bx-artificer .bx-def-margin-sec-top-auto {
+        @apply mt-2 first:mt-0;
+    }
+    .bx-artificer .bx-def-margin-sec-bottom {
+        @apply mb-2;
+    }
+    .bx-artificer .bx-def-margin-sec-bottom-neg {
+        @apply -mb-2;
+    }
+    .bx-artificer .bx-def-margin-sec-leftright {
+        @apply mx-2;
+    }
+    .bx-artificer .bx-def-margin-sec-leftright-neg {
+        @apply -mx-2;
+    }
+    .bx-artificer .bx-def-margin-sec-lefttopright-neg {
+        @apply -mx-2 -mt-2;
+    }
+    .bx-artificer .bx-def-margin-sec-topbottom {
+        @apply my-2;
+    }
+    .bx-artificer .bx-def-margin-sec-topbottom-neg {
+        @apply -my-2;
+    }
+    .bx-artificer .bx-def-margin-sec-lefttopright {
+        @apply mx-2 mt-2;
+    }
+    .bx-artificer .bx-def-margin-sec-rightbottomleft {
+        @apply mx-2 mb-2;
+    }
+
+    .bx-artificer .bx-def-margin-thd {
+        @apply m-1;
+    }
+    .bx-artificer .bx-def-margin-thd-neg {
+        @apply -m-1;
+    }
+    .bx-artificer .bx-def-margin-thd-left {
+        @apply ml-1;
+    }
+    .bx-artificer .bx-def-margin-thd-left-auto {
+        @apply ml-1 first:ml-0;
+    }
+    .bx-artificer .bx-def-margin-thd-right {
+        @apply mr-1;
+    }
+    .bx-artificer .bx-def-margin-thd-top {
+        @apply mt-1;
+    }
+    .bx-artificer .bx-def-margin-thd-top-auto {
+        @apply mt-1 first:mt-0;
+    }
+    .bx-artificer .bx-def-margin-thd-bottom {
+        @apply mb-1;
+    }
+    .bx-artificer .bx-def-margin-thd-leftright {
+        @apply mx-1;
+    }
+    .bx-artificer .bx-def-margin-thd-leftright-neg {
+        @apply -mx-1;
+    }
+    .bx-artificer .bx-def-margin-thd-topbottom {
+        @apply my-1;
+    }
+    .bx-artificer .bx-def-margin-thd-topbottom-neg {
+        @apply -my-1;
+    }
+    .bx-artificer .bx-def-margin-thd-lefttopright {
+        @apply mx-1 mt-1;
+    }
+    .bx-artificer .bx-def-margin-thd-rightbottomleft {
+        @apply mx-1 mb-1;
+    }
+
+    .bx-artificer .bx-def-padding {
+        @apply p-4;
+    }
+    .bx-artificer .bx-def-padding-left {
+        @apply pl-4;
+    }
+    .bx-artificer .bx-def-padding-left-auto {
+        @apply pl-4 first:pl-0;
+    }
+    .bx-artificer .bx-def-padding-right {
+        @apply pr-4;
+    }
+    .bx-artificer .bx-def-padding-top {
+        @apply pt-4;
+    }
+    .bx-artificer .bx-def-padding-top-auto {
+        @apply pt-4 first:pt-0;
+    }
+    .bx-artificer .bx-def-padding-bottom {
+        @apply pb-4;
+    }
+    .bx-artificer .bx-def-padding-leftright {
+        @apply px-4;
+    }
+    .bx-artificer .bx-def-padding-topbottom {
+        @apply py-4;
+    }
+    .bx-artificer .bx-def-padding-lefttopright {
+        @apply px-4 pt-4;
+    }
+    .bx-artificer .bx-def-padding-rightbottomleft {
+        @apply px-4 pb-4;
+    }
+    
+    .bx-artificer .bx-def-padding-sec {
+        @apply p-2;
+    }
+    .bx-artificer .bx-def-padding-sec-left {
+        @apply pl-2;
+    }
+    .bx-artificer .bx-def-padding-sec-left-auto {
+        @apply pl-2 first:pl-0;
+    }
+    .bx-artificer .bx-def-padding-sec-right {
+        @apply pr-2;
+    }
+    .bx-artificer .bx-def-padding-sec-top {
+        @apply pt-2;
+    }
+    .bx-artificer .bx-def-padding-sec-top-auto {
+        @apply pt-2 first:pt-0;
+    }
+    .bx-artificer .bx-def-padding-sec-bottom {
+        @apply pb-2;
+    }
+    .bx-artificer .bx-def-padding-sec-leftright {
+        @apply px-2;
+    }
+    .bx-artificer .bx-def-padding-sec-topbottom {
+        @apply py-2;
+    }
+    .bx-artificer .bx-def-padding-sec-lefttopright {
+        @apply px-2 pt-2;
+    }
+    .bx-artificer .bx-def-padding-sec-rightbottomleft {
+        @apply px-2 pb-2;
+    }
+
+    .bx-artificer .bx-def-padding-thd {
+        @apply p-1;
+    }
+    .bx-artificer .bx-def-padding-thd-left {
+        @apply pl-1;
+    }
+    .bx-artificer .bx-def-padding-thd-left-auto {
+        @apply pl-1 first:pl-0;
+    }
+    .bx-artificer .bx-def-padding-thd-right {
+        @apply pr-1;
+    }
+    .bx-artificer .bx-def-padding-thd-top {
+        @apply pt-1;
+    }
+    .bx-artificer .bx-def-padding-thd-top-auto {
+        @apply pt-1 first:pt-0;
+    }
+    .bx-artificer .bx-def-padding-thd-bottom {
+        @apply pb-1;
+    }
+    .bx-artificer .bx-def-padding-thd-leftright {
+        @apply px-1;
+    }
+    .bx-artificer .bx-def-padding-thd-topbottom {
+        @apply py-1;
+    }
+    .bx-artificer .bx-def-padding-thd-lefttopright {
+        @apply px-1 pt-1;
+    }
+    .bx-artificer .bx-def-padding-thd-rightbottomleft {
+        @apply px-1 pb-1;
+    }
+
+    /* Borders */
+    .bx-artificer .bx-def-border {
+        @apply border border-gray-200 dark:border-gray-700;
+    }
+    .bx-artificer .bx-def-round-corners {
+        @apply rounded-md;
+    }
+
+    /* Fonts */
+    .bx-def-font,
+    .bx-artificer .bx-def-font-inputs:not(.bx-btn) {
+        @apply font-inter text-gray-500 dark:text-gray-400;
+    }
+
+    .bx-def-font-grayed {
+        @apply text-gray-700 dark:text-gray-500 text-opacity-60;
+    }
+
+    .bx-def-font-author,
+    .bx-def-font-author.bx-def-font-meta {
+        @apply text-sm text-gray-800 hover:text-black dark:text-gray-300 dark:hover:text-white font-semibold;
+    }
+
+    .bx-def-font-meta {
+        @apply text-sm;
+    }
+    .bx-def-font-meta:not(a) {
+        @apply text-gray-700 dark:text-gray-400 text-opacity-60;
+    }
+    a.bx-def-font-meta {
+        @apply hover:underline;
+    }
+
+    .bx-artificer .bx-def-a-colored {
+        @apply text-blue-600 hover:text-blue-700 dark:text-blue-500 dark:hover:text-blue-600;
+    }
+
+    .bx-artificer a.bx-def-font-meta,
+    .bx-artificer a.bx-def-font-meta.bx-def-a-colored {
+        @apply text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-50;
+    }
+    .bx-artificer .bx-def-font-meta:not(a) {
+        @apply text-opacity-100;
+    }
+
+    .bx-artificer .bx-def-font-small {
+        @apply text-xs;
+    }
+
+    .bx-artificer .bx-def-font-middle {
+        @apply text-sm;
+    }
+
+    .bx-artificer .bx-def-font-large {
+        @apply text-lg;
+    }
+
+    /* Typography in combination with 'bx-def-vanilla-html' class */
+    .bx-def-vanilla-html h1,
+    .bx-def-vanilla-html .bx-def-font-h1 {
+        @apply scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl;
+    }
+
+    .bx-def-vanilla-html h2,
+    .bx-def-vanilla-html .bx-def-font-h2 {
+        @apply scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0;
+    }
+
+    .bx-def-vanilla-html h3,
+    .bx-def-vanilla-html .bx-def-font-h3 {
+        @apply scroll-m-20 text-2xl font-semibold tracking-tight;
+    }
+
+    .bx-def-vanilla-html h4 {
+        @apply scroll-m-20 text-xl font-semibold tracking-tight;
+    }
+
+    .bx-def-vanilla-html p {
+        @apply leading-7 [&:not(:first-child)]:mt-6;
+    }
+
+    .bx-def-vanilla-html blockquote {
+        @apply mt-6 border-l-2 pl-6 italic;
+    }
+
+    .bx-def-vanilla-html table {
+        @apply w-full;
+    }
+
+    .bx-def-vanilla-html tr {
+        @apply m-0 border-t p-0;
+    }
+
+    .bx-def-vanilla-html th {
+        @apply border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right;
+    }
+
+    .bx-def-vanilla-html td {
+        @apply border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right;
+    }
+
+    .bx-def-vanilla-html ul {
+        @apply my-6 ml-6 list-disc [&>li]:mt-2;
+    }
+
+    .bx-def-vanilla-html code {
+        @apply relative rounded px-[0.3rem] py-[0.2rem] text-sm font-semibold;
+    }
+
+    /* Common elements: Icons. */
+    .bx-icon svg {
+        @apply w-5 h-5;
+    }
+    .bx-icon.bx-icon-small svg {
+        @apply w-4 h-4;
+    }
+    .bx-icon.bx-icon-close svg {
+        @apply w-7 h-7;
+    }
+
+    /* Common elements: Pages. */
+    .bx-layout-wrapper{
+        @apply gap-4 lg:gap-6 flex flex-col;
+    }
+    .bx-layout-row{
+        @apply w-full flex relative flex-wrap md:flex-nowrap gap-y-4 lg:gap-y-6;
+    }
+    .bx-layout-col{
+        @apply max-w-full w-full;
+        flex-shrink: 2 !important;
+    }
+
+    .bx-layout-cell{
+        @apply flex flex-col gap-4 lg:gap-6;
+    }
+    .bx-layout-gap{
+        @apply gap-y-4 lg:gap-y-6;
+    }
+    .bx-def-color-bg-page{
+	    @apply bg-gray-100 dark:bg-gray-900;
+    }
+
+    /* Common elements: Blocks, Boxes, etc. */
+    .bx-def-box {
+        @apply flex flex-col;
+    }
+    .bx-def-box.bx-def-box-hor {
+        @apply flex-row;
+    }
+    .bx-def-color-bg-block,
+    .bx-def-color-bg-box,
+    .bx-popup-color-bg {
+        @apply bg-white dark:bg-gray-800 shadow-md;
+    }
+    .bx-def-color-bg-box-overflow {
+        @apply bg-gradient-to-b from-transparent to-white dark:to-gray-800;
+    }
+    .bx-def-border-block,
+    .bx-def-box,
+    .bx-popup-border {
+        @apply ring-1 ring-gray-300/50 dark:ring-gray-600/50;
+    }
+    .bx-def-border-radius-block,
+    .bx-def-box-round-corners,
+    .bx-popup-border {
+        @apply rounded-md;
+    }
+    .bx-content-padding-block {
+        @apply p-3 md:p-4;
+    }
+    .bx-def-color-bg-box-hover {
+        @apply hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-800 dark:hover:text-gray-200;
+    }
+    .bx-def-color-bg-box-active {
+        @apply bg-gray-100 dark:bg-gray-700;
+    }
+
+    .bx-db-title {
+        @apply text-lg xl:text-xl font-bold text-gray-800 dark:text-gray-100 align-middle;
+    }
+
+    /* Common Elements: HR */
+    .bx-artificer .bx-def-hr {
+        @apply mx-0 p-0 border-t border-gray-200 dark:border-gray-700;
+    }
+
+    /* Common Elements: Labels */
+    .bx-artificer .bx-def-label {
+        @apply inline-block min-w-6 px-4 py-1 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 text-gray-800 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-200 text-sm font-normal rounded-full box-border;
+    }
+    
+    /* Common Elements: Alerts */
+    .bx-def-unit-alert,
+    .bx-def-unit-alert-small,
+    .bx-def-unit-alert-middle {
+        @apply block absolute top-0 -right-0.5 text-center text-white;
+    }
+    .bx-def-unit-alert {
+        @apply h-4 p-1 leading-4 rounded-full text-xs;
+        min-width: 1rem;
+    }
+
+    /* Common elements: Buttons */
+    .bx-btn-shadow {
+        @apply shadow-sm hover:shadow active:shadow-none;
+    }
+    .bx-btn-border {
+        @apply border border-gray-300 dark:border-gray-600 rounded-md;
+    }
+    .bx-btn-bg {
+        @apply bg-gray-100 dark:bg-gray-700;
+    }
+    .bx-btn-text {
+        @apply text-sm text-gray-700 dark:text-gray-100 font-medium ;
+    }
+    .bx-btn-hover {
+        @apply hover:bg-white dark:hover:bg-gray-600;
+        @apply hover:border-gray-300 dark:hover:border-gray-500;
+        @apply hover:text-gray-800 dark:hover:text-white;
+        @apply hover:no-underline;
+        @apply hover:-translate-y-px;
+    }
+    .bx-btn-active {
+        @apply active:bg-gray-300 dark:active:bg-gray-800;
+        @apply active:border-gray-400 dark:active:border-gray-700;
+        @apply active:text-gray-900 dark:active:text-gray-200;
+        @apply active:translate-y-px;
+    }
+    .bx-btn {
+        @apply relative inline-flex items-center px-4 py-2;
+        @apply bx-btn-shadow;
+        @apply bx-btn-bg;
+        @apply bx-btn-border;
+        @apply bx-btn-text;
+        @apply transform-gpu transition-all duration-150;
+        @apply cursor-pointer;
+        @apply bx-btn-hover bx-btn-active;
+    }
+    .bx-btn i, 
+    .bx-btn u {
+        @apply inline-block align-middle;
+    }
+
+    .bx-btn i, 
+    .bx-btn img,
+    .bx-btn svg {
+        @apply w-5 h-5 leading-5;
+    }
+    .bx-btn i:not(.sys-colored),
+    .bx-btn img {
+        @apply text-gray-600 dark:text-gray-300;
+    }
+
+    .bx-btn:hover i:not(.sys-colored), 
+    .bx-btn:hover img {
+        @apply text-gray-800 dark:text-gray-100;
+    }
+
+    .bx-btn i {
+        @apply text-center;
+    }
+
+    .bx-btn u {
+        @apply ml-2 first:ml-0 no-underline;
+    }
+
+    .bx-btn.bx-btn-primary {
+        @apply bg-blue-600 hover:bg-blue-500 active:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600 dark:active:bg-blue-800;
+        @apply border border-blue-600 hover:border-blue-500 active:border-blue-800 dark:border-blue-600 dark:hover:border-blue-500 dark:active:border-blue-700;
+        @apply text-white hover:text-white active:text-white dark:text-white dark:hover:text-white dark:active:text-white;
+    }
+
+    .bx-btn.bx-btn-primary i, 
+    .bx-btn.bx-btn-primary img {
+        @apply text-gray-300;
+    }
+    
+    .bx-btn.bx-btn-primary:hover i, 
+    .bx-btn.bx-btn-primary:hover img {
+        @apply text-gray-100;
+    }
+    
+    .bx-btn.bx-btn-disabled {
+        @apply bg-gray-50 dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-400 hover:text-gray-400 dark:hover:text-gray-400 focus:ring-0 focus:ring-offset-0 shadow-none cursor-default !important;
+    }
+
+    .bx-btn.bx-btn-small {
+        @apply py-1 text-xs;
+    }
+
+    .bx-btn.bx-btn-small i, 
+    .bx-btn.bx-btn-small img,
+    .bx-btn.bx-btn-small svg {
+        @apply w-4 h-4 leading-4;
+    }
+
+    /* Common elements: Forms */
+    .bx-artificer form {
+        @apply text-sm font-normal text-gray-700 dark:text-gray-300;
+    }
+
+    .bx-artificer form a:not(.bx-btn),
+    .bx-artificer .sys-jf-agreement a:not(.bx-btn), 
+    .bx-artificer .sys-lf-agreement a:not(.bx-btn) {
+        @apply font-normal text-blue-600 hover:text-blue-700 dark:text-blue-500 dark:hover:text-blue-600;
+    }
+
+    .bx-artificer .bx-form-caption,
+    .bx-artificer .bx-form-row-view-caption {
+        @apply text-gray-700 dark:text-gray-300 text-sm font-medium;
+    }
+
+    .bx-artificer .bx-form-value {
+        @apply mt-1 first:mt-0;
+    }
+
+    .bx-artificer .bx-form-required,
+    .bx-artificer .bx-form-warn {
+        @apply text-red-400;
+    }
+
+    /* Common elements: Forms: Text Input */
+    .bx-artificer .bx-form-input-slider,
+    .bx-artificer .bx-form-input-doublerange,
+    .bx-artificer .bx-form-input-select_multiple,
+    .bx-artificer .bx-form-input-select,
+    .bx-artificer .bx-form-input-number,
+    .bx-artificer .bx-form-input-time,
+    .bx-artificer .bx-form-input-datepicker,
+    .bx-artificer .bx-form-input-datetime,
+    .bx-artificer .bx-form-input-textarea:not(.bx-form-input-html),
+    .bx-artificer .bx-form-input-text,
+    .bx-artificer .bx-form-input-price,
+    .bx-artificer .bx-form-input-wrapper-price span {
+        @apply block w-full px-3 py-2;
+        @apply bg-gray-50 dark:bg-gray-900/50 focus:bg-white dark:focus:bg-gray-900;
+        @apply border border-gray-300 dark:border-gray-700 focus:border-blue-500 dark:focus:border-blue-500 focus:border-opacity-70 focus:ring-2 focus:ring-blue-500 focus:ring-opacity-20 focus:outline-none;
+        @apply leading-6 text-base font-medium text-gray-700 dark:text-gray-300 focus:text-gray-900 dark:focus:text-gray-100;
+        @apply placeholder:text-gray-500 dark:focus:placeholder:text-gray-700 focus:placeholder:text-gray-300;
+        @apply rounded-md;
+    }
+	
+    /* Common elements: Forms: Password Input */
+    .bx-artificer .bx-form-input-wrapper-password {
+        @apply relative block w-full pl-3 pr-8;
+        @apply bg-gray-50 dark:bg-gray-900/50 focus:bg-white dark:focus:bg-gray-900;
+        @apply border border-gray-300 dark:border-gray-700 focus:border-blue-500 dark:focus:border-blue-500 focus:border-opacity-70 focus:ring-2 focus:ring-blue-500 focus:ring-opacity-20 focus:outline-none;
+        @apply leading-6 text-base font-medium text-gray-700 dark:text-gray-300 focus:text-gray-900 dark:focus:text-gray-100;
+        @apply placeholder:text-gray-500 dark:focus:placeholder:text-gray-700 focus:placeholder:text-gray-300;
+        @apply rounded-md;
+    }
+
+    /* Common elements: Forms: Checkbox/Radio Set Input */
+    .bx-artificer .bx-form-input-wrapper-checkbox_set,
+    .bx-artificer .bx-form-input-wrapper-radio_set {
+        @apply relative block w-full py-2 px-3;
+        @apply bg-gray-50 dark:bg-gray-900/50 focus:bg-white dark:focus:bg-gray-900;
+        @apply border border-gray-300 dark:border-gray-700 focus:border-blue-500 dark:focus:border-blue-500 focus:border-opacity-70 focus:ring-2 focus:ring-blue-500 focus:ring-opacity-20 focus:outline-none;
+        @apply leading-6 text-base font-medium text-gray-700 dark:text-gray-300 focus:text-gray-900 dark:focus:text-gray-100;
+        @apply placeholder:text-gray-500 dark:focus:placeholder:text-gray-700 focus:placeholder:text-gray-300;
+        @apply rounded-md;
+    }
+
+    /* Common elements: Forms: Checkbox Input */
+    .bx-artificer .bx-form-input-checkbox,
+    .bx-artificer .bx-form-input-radio {
+        @apply block w-4 h-4;
+        @apply bg-gray-50 dark:bg-gray-900/50 bg-origin-border;
+        @apply border border-gray-300 dark:border-gray-700 focus:border-blue-500 dark:focus:border-blue-500 focus:border-opacity-70;
+        @apply placeholder:text-gray-500 dark:focus:placeholder:text-gray-700 focus:placeholder:text-gray-300;
+        @apply rounded;
+        @apply appearance-none select-none;
+    }
+    .bx-artificer .bx-def-font-inputs.bx-form-input-checkbox,
+    .bx-artificer .bx-def-font-inputs.bx-form-input-radio {
+        @apply leading-6 text-base font-medium text-blue-600 dark:text-blue-600;
+    }
+    .bx-artificer .bx-form-input-checkbox:checked,
+    .bx-artificer .bx-form-input-radio:checked {
+        background-color: currentColor;
+        border-color: transparent;
+    }
+    .bx-artificer .bx-form-input-checkbox:checked {
+        background-image:url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2aWV3Qm94PScwIDAgMTYgMTYnIGZpbGw9JyNmZmYnIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Zyc+PHBhdGggZD0nTTEyLjIwNyA0Ljc5M2ExIDEgMCAwIDEgMCAxLjQxNGwtNSA1YTEgMSAwIDAgMS0xLjQxNCAwbC0yLTJhMSAxIDAgMCAxIDEuNDE0LTEuNDE0TDYuNSA5LjA4Nmw0LjI5My00LjI5M2ExIDEgMCAwIDEgMS40MTQgMHonLz48L3N2Zz4=)
+    }
+    .bx-artificer .bx-form-input-radio:checked {
+        background-image:url(data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2aWV3Qm94PScwIDAgMTYgMTYnIGZpbGw9JyNmZmYnIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Zyc+PGNpcmNsZSBjeD0nOCcgY3k9JzgnIHI9JzMnLz48L3N2Zz4=)
+    }
+
+
+    /* Common elements: Forms: Switcher Input */
+    .bx-switcher-cont.on button {
+        @apply bg-white dark:bg-gray-800;
+    }
+    .bx-switcher-cont.on button span {
+        @apply bg-green-500 dark:bg-green-500 translate-x-5;
+    }
+    .bx-switcher-cont.off button {
+        @apply bg-gray-200 dark:bg-gray-900;
+    }
+    .bx-switcher-cont.off button span {
+        @apply bg-white dark:bg-gray-500 translate-x-0;
+    }
+
+    /* Common elements: Forms: Autosuggestion Input */
+    .bx-artificer .bx-form-input-text.bx-form-input-autotoken {
+        @apply flex flex-wrap items-stretch w-full py-2 px-3 ;
+        @apply bg-gray-50 dark:bg-gray-900/50 focus:bg-white dark:focus:bg-gray-900;
+        @apply border border-gray-300 dark:border-gray-700 focus:border-blue-500 dark:focus:border-blue-500 focus:border-opacity-70 focus:ring-2 focus:ring-blue-500 focus:ring-opacity-20 focus:outline-none;
+        @apply leading-6 text-base font-medium text-gray-700 dark:text-gray-300 focus:text-gray-900 dark:focus:text-gray-100;
+        @apply placeholder:text-gray-500 dark:focus:placeholder:text-gray-700 focus:placeholder:text-gray-300;
+        @apply rounded-md;
+    }
+    .bx-artificer .bx-form-input-autotoken b.val {
+        @apply bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600;
+    }
+
+
+    /* Common elements: Popups */
+    .bx-artificer .bx-popup-wrapper {
+        @apply overflow-visible;
+    }
+
+    .bx-artificer .bx-popup-applied {
+        @apply rounded-lg;
+    }
+
+    .bx-artificer .bx-popup-responsive:not(.bx-popup-full-screen):not(.bx-popup-menu) {
+        @apply w-full md:w-auto;
+    }
+
+    .bx-artificer .bx-popup-full-screen {
+        @apply w-full;
+    }
+
+    .bx-artificer .bx-popup {
+        @apply relative;
+    }
+
+    .bx-artificer .bx-popup-color-bg {
+        @apply bg-white dark:bg-gray-800 shadow;
+    }
+
+    .bx-artificer .bx-popup-border {
+        @apply ring-1 ring-gray-300 dark:ring-gray-700 rounded-md;
+    }
+
+    .bx-artificer .bx-popup-width {
+        @apply w-full md:w-128;
+    }
+
+    .bx-artificer .bx-popup-width-small,
+    .bx-artificer .bx-popup-menu .bx-popup-content {
+        @apply w-56;
+    }
+
+    .bx-artificer .bx-popup-full-screen .bx-popup-width {
+        @apply w-full md:w-full;
+    }
+
+    .bx-artificer .bx-popup-fog {
+        @apply bg-gray-600 opacity-75;
+    }
+
+
+    /* Common elements: Menus */
+    .bx-stl-mil-gaps {
+        @apply px-3 py-2;
+    }
+
+    .bx-stl-mil-gaps-sm {
+        @apply px-2 py-1.5;
+    }
+
+    .bx-stl-mil-hover-text {
+        @apply hover:text-gray-800 dark:hover:text-gray-200;
+    }
+    .bx-stl-mil-hover-bg {
+        @apply hover:border-gray-200/50 dark:hover:border-gray-700/50;
+        @apply hover:bg-gray-200/50 dark:hover:bg-gray-700/50;
+    }
+    .bx-stl-mil-hover {
+        @apply bx-stl-mil-hover-text;
+        @apply bx-stl-mil-hover-bg;
+    }
+
+    .bx-stl-mil-active-bg {
+        @apply active:border-gray-300 dark:active:border-gray-700;
+        @apply active:bg-gray-300 dark:active:bg-black/50;
+    }
+    .bx-stl-mil-active {
+        @apply bx-stl-mil-active-bg;
+    }
+    
+    .bx-stl-mil,
+    .bx-stl-mil-sm,
+    .bx-stl-mil-wo-bg,
+    .bx-stl-mil-wo-bg-gaps,
+    .bx-stl-mil-wo-gaps,
+    .bx-stl-mil-wo-gaps-sm {
+        @apply flex items-center;
+        @apply text-base text-gray-600 dark:text-gray-400 font-medium;
+        @apply bx-stl-mil-hover-text;
+    }
+
+    .bx-stl-mil,
+    .bx-stl-mil-sm,
+    .bx-stl-mil-wo-bg {
+        @apply bx-stl-mil-gaps;
+    }
+
+    .bx-stl-mil,
+    .bx-stl-mil-sm,
+    .bx-stl-mil-wo-gaps,
+    .bx-stl-mil-wo-gaps-sm {
+        @apply border border-transparent;
+        @apply rounded-md;
+        @apply bx-stl-mil-hover-bg bx-stl-mil-active-bg;
+    }
+    .bx-stl-mil.bx-stl-mib,
+    .bx-stl-mil-sm.bx-stl-mib,
+    .bx-stl-mil-wo-gaps.bx-stl-mib,
+    .bx-stl-mil-wo-gaps-sm.bx-stl-mib {
+        @apply border border-gray-300 dark:border-gray-700;
+    }
+    .bx-stl-mil-sm,
+    .bx-stl-mil-wo-gaps-sm {
+        @apply text-sm;
+    }
+
+    .bx-stl-mil-sm {
+        @apply px-2 py-1.5;
+    }
+    .bx-popup .bx-stl-mil-sm,
+    .bx-popup .bx-stl-mil-gaps-sm {
+        @apply px-4;
+    }
+
+    .bx-popup ul:not(.bx-menu-hor) .bx-stl-mil,
+    .bx-popup ul:not(.bx-menu-hor) .bx-stl-mil-sm,
+    .bx-popup ul:not(.bx-menu-hor) .bx-stl-mil-wo-gaps,
+    .bx-popup ul:not(.bx-menu-hor) .bx-stl-mil-wo-gaps-sm {
+        @apply rounded-none;
+    }
+    .bx-menu-tab-active.bx-stl-mil,
+    .bx-menu-tab-active.bx-stl-mil-sm,
+    .bx-menu-tab-active.bx-stl-mil-wo-gaps,
+    .bx-menu-tab-active.bx-stl-mil-wo-gaps-sm,
+    .bx-menu-tab-active .bx-stl-mil,
+    .bx-menu-tab-active .bx-stl-mil-sm,
+    .bx-menu-tab-active .bx-stl-mil-wo-gaps,
+    .bx-menu-tab-active .bx-stl-mil-wo-gaps-sm,
+    .bx-menu-item-active .bx-stl-mil,
+    .bx-menu-item-active .bx-stl-mil-sm, 
+    .bx-menu-item-active .bx-stl-mil-wo-gaps,
+    .bx-menu-item-active .bx-stl-mil-wo-gaps-sm {
+        @apply text-gray-900 dark:text-white;
+        @apply bg-gray-200 dark:bg-gray-900;
+        @apply ring-1 ring-white/50 dark:ring-gray-800/50;
+    }
+    .bx-popup .bx-menu-tab-active.bx-stl-mil,
+    .bx-popup .bx-menu-tab-active.bx-stl-mil-sm,
+    .bx-popup .bx-menu-tab-active.bx-stl-mil-wo-gaps,
+    .bx-popup .bx-menu-tab-active.bx-stl-mil-wo-gaps-sm,
+    .bx-popup .bx-menu-tab-active .bx-stl-mil,
+    .bx-popup .bx-menu-tab-active .bx-stl-mil-sm,
+    .bx-popup .bx-menu-tab-active .bx-stl-mil-wo-gaps,
+    .bx-popup .bx-menu-tab-active .bx-stl-mil-wo-gaps-sm {
+        @apply ring-0;
+    }
+    .bx-stl-mii,
+    .bx-stl-mii-sm,
+    .bx-stl-mil .sys-action-do-icon,
+    .bx-stl-mil-sm .sys-action-do-icon,
+    .bx-stl-mil-wo-gaps .sys-action-do-icon,
+    .bx-stl-mil-wo-gaps-sm .sys-action-do-icon {
+        @apply flex items-center justify-center text-gray-500 text-lg;
+    }
+    
+    .bx-stl-mii > *,
+    .bx-stl-mii-sm > *,
+    .bx-stl-mil .sys-action-do-icon > *,
+    .bx-stl-mil-sm .sys-action-do-icon > *,
+    .bx-stl-mil-wo-gaps .sys-action-do-icon > *,
+    .bx-stl-mil-wo-gaps-sm .sys-action-do-icon > * {
+        @apply flex items-center justify-center h-6 w-6 mr-3;
+    }
+    .bx-stl-mii:last-child > *,
+    .bx-stl-mii-sm:last-child > *,
+    .bx-stl-mil .sys-action-do-icon:last-child > *,
+    .bx-stl-mil-sm .sys-action-do-icon:last-child > *,
+    .bx-stl-mil-wo-gaps .sys-action-do-icon:last-child > *,
+    .bx-stl-mil-wo-gaps-sm .sys-action-do-icon:last-child > * {
+        @apply mr-0;
+    }
+    .bx-stl-mil .sys-action-do-icon ~ .sys-action-do-text,
+    .bx-stl-mil-sm .sys-action-do-icon ~ .sys-action-do-text,
+    .bx-stl-mil-wo-gaps .sys-action-do-icon ~ .sys-action-do-text,
+    .bx-stl-mil-wo-gaps-sm .sys-action-do-icon ~ .sys-action-do-text {
+        @apply ml-0; 
+    }
+    .bx-stl-mii-sm > *,
+    .bx-stl-mil-sm .sys-action-do-icon > *,
+    .bx-stl-mil-wo-gaps-sm .sys-action-do-icon > * {
+        @apply h-5 w-5 mr-1.5 text-sm;
+    }
+    .bx-stl-mit {
+        @apply truncate;
+    }
+    .bx-stl-mia {
+        @apply inline-block py-0.5 px-2 bg-gray-200/50 dark:bg-gray-800/50 text-gray-600 dark:text-gray-300 text-xs rounded-full;
+    }
+
+    /* Common elements: Horizontal menu */
+    .bx-artificer .bx-menu-hor li.bx-menu-tab-active a,
+    .bx-artificer .bx-menu-custom-hor li.bx-menu-tab-active a {
+        @apply text-gray-700 dark:text-gray-200 font-medium;
+    }
+
+    /* Common elements: Vertical menu */
+    .bx-artificer .bx-menu-ver li.bx-menu-tab-active .bx-mi-highlight,
+    .bx-artificer .bx-menu-verlit li.bx-menu-tab-active .bx-mi-highlight,
+    .bx-artificer .bx-menu-custom-ver li.bx-menu-tab-active .bx-mi-highlight {
+        @apply bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white font-medium;
+    }
+
+    /* Common elements: Vertical menu in Popup */
+    .bx-artificer .bx-popup .bx-menu-ver > li:hover {
+        @apply hover:text-gray-900 dark:hover:text-gray-100;
+    }
+
+    .bx-artificer .bx-popup .bx-menu-ver > li.bx-def-color-bg-active {
+        @apply bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white;
+    }
+
+    /* Common elements: Toolbar menu */
+    .bx-artificer .bx-menu-toolbar .bx-menu-toolbar-item .bx-menu-toolbar-item-unit {
+        @apply border-4 border-transparent;
+    }
+    .bx-artificer .bx-menu-toolbar .bx-menu-toolbar-item .bx-menu-toolbar-item-unit .bx-def-icon-size {
+        @apply h-6 w-6 md:h-8 md:w-8;
+    }
+
+    /* Common elements: Page Submenu */
+    .bx-artificer .bx-menu-main-submenu .bx-menu-item.bx-menu-tab-active a {
+        @apply text-gray-900 dark:text-gray-100;
+    }
+    .bx-artificer .bx-menu-main-submenu .bx-menu-item.bx-menu-tab-active .bx-mi-underline {
+        @apply bg-blue-500 group-hover:bg-blue-500;
+    }
+
+    /* Common elements: Profile Stats menu */
+    .bx-artificer .bx-menu-profile-stats.bx-menu-ver li.bx-menu-tab-active {
+        @apply bg-slate-900/[.08] dark:bg-slate-900 ring-1 ring-gray-200 dark:ring-gray-800 text-gray-900 dark:text-white;
+    }
+    .bx-artificer .bx-menu-profile-stats.bx-menu-ver li.bx-psmi-show-more,
+    .bx-artificer .bx-menu-profile-stats.bx-menu-ver li.bx-psmi-show-less {
+        @apply text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300
+    }
+
+
+    /* Common elements: Profile Cover */
+    .bx-artificer .bx-base-pofile-cover-thumb + .bx-base-pofile-cover-user {
+        @apply lg:pl-52;
+    }
+
+    .bx-artificer .bx-base-pofile-cover-actions div.bx-base-general-entity-actions .bx-menu-custom-hor.bx-menu-hor-inline li.bx-mi-primary {
+        @apply border-transparent text-gray-200 hover:text-white bg-blue-600 hover:bg-blue-700 focus:ring-blue-500;
+    }
+
+
+    /* Common elements: Informers */
+    .bx-informer-msg a {
+        @apply text-blue-600 hover:text-blue-700 dark:text-blue-500 dark:hover:text-blue-600;
+    }
+    .bx-informer-msg-info {
+        @apply bg-blue-50 text-blue-700;
+    }
+    .bx-informer-msg-alert {
+        @apply bg-yellow-50 text-yellow-700;
+    }
+    .bx-informer-msg-error {
+        @apply bg-red-50 text-red-700;
+    }
+
+
+    /* Common elements: All Actions */
+    .sys-action-counter.sys-ac-link,
+    .sys-action-counter .sys-ac-link {
+        @apply flex items-center;
+        @apply border-none;
+        @apply text-gray-500 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300;
+        @apply rounded-full;
+    }
+    .sys-action-counter.sys-ac-only.sys-ac-link,
+    .sys-action-counter.sys-ac-only .sys-ac-link {
+        @apply hover:bg-gray-100 dark:hover:bg-gray-600;
+    }
+
+    /* Common elements: Comments */
+    .bx-artificer .cmt .bx-base-general-unit-meta-username {
+        @apply text-gray-700 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white;
+    }
+    
+    /* Common Elements: Votes */
+    .bx-vote-bls-submenu-cnt {
+        @apply border-b-gray-300 dark:border-b-gray-700;
+    }
+    .bx-vote-bls-submenu-cnt ul li.bx-menu-inter-act {
+        @apply border-b-gray-500 dark:border-b-gray-500;
+    }
+
+    /* Common elements: Units */
+    .bx-artificer .bx-def-icon-size,
+    .bx-artificer .bx-profile-avatar .bx-def-thumb-size,
+    .bx-artificer .bx-profile-stats .bx-ps-user .bx-def-thumb-size {
+        @apply h-8 w-8;
+    }
+    .bx-artificer .bx-def-thumb-size {
+        @apply h-10 w-10;
+    }
+    .bx-artificer .bx-def-ava-size {
+        @apply h-24 w-24;
+    }
+    .bx-artificer .bx-def-ava-big-size {
+        @apply w-48 h-48;
+    }
+
+    .bx-base-text-unit-title a,
+    .bx-tl-title a {
+        @apply text-gray-800 hover:text-blue-500 dark:text-gray-100 dark:hover:text-blue-400 hover:no-underline;
+    }
+
+    div.bx-tl-item-image{
+        @apply aspect-video lg:aspect-auto rounded-lg;
+    }
+    img.bx-tl-item-image {
+        @apply w-full h-full object-center object-cover lg:w-full lg:h-full rounded-lg;
+    }
+
+    .bx-base-text-unit-title a {
+        @apply text-lg lg:text-xl font-semibold;
+    }
+
+    .bx-artificer .bx-base-pofile-unit-online {
+        @apply absolute w-2.5 h-2.5 right-0.5 bottom-0.5 ring-1 ring-inset ring-white dark:ring-gray-800 bg-green-500 rounded-full;
+    }
+
+    .bx-artificer .bx-base-general-unit-meta .bx-base-profile-unit-meta-item > a:not(.bx-btn),
+    .bx-artificer .bx-base-general-unit-meta ul.bx-menu-custom.bx-menu-custom-hor .bx-base-pofile-unit {
+        @apply p-1 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700;
+    }
+
+
+    /* Common elements: Auth */
+    .sys-auth-block {
+        @apply grid-cols-2;
+    }
+    .sys-auth-block.sys-auth-compact-container {
+        @apply grid-cols-4;
+    }
+
+
+    /* Common elements: Votes */
+    .bx-vote-likes .bx-vote-element-holder .bx-vote-do-vote.bx-vote-disabled,
+    .bx-vote-likes .bx-vote-element-holder .bx-vote-do-vote.bx-vote-disabled .sys-icon {
+        @apply text-gray-300 dark:text-gray-700;
+    }
+
+
+    /* Modules: Timeline */
+    .bx-artificer .group\/item:hover .bx-tl-item-text .bx-tl-content.bx-tl-overflow .bx-tl-content-overflow {
+        @apply to-gray-50;
+    }
+    .dark .bx-artificer .group\/item:hover .bx-tl-item-text .bx-tl-content.bx-tl-overflow .bx-tl-content-overflow {
+        @apply to-gray-900;
+    }
+    .bx-artificer .bx-tl-item-activity .bx-stl-mil-wo-gaps-sm .sys-action-element-holder > a,
+    .bx-artificer .bx-menu-object-bx_timeline_menu_item_manage .bx-stl-mil-wo-gaps-sm .sys-action-element-holder > a,
+    .bx-artificer .bx-menu-object-sys_cmts_item_manage .bx-stl-mil-wo-gaps-sm .sys-action-element-holder > a{
+        @apply relative flex;
+        @apply bx-stl-mil-gaps-sm;
+    }
+    .bx-artificer .bx-popup .bx-menu-object-bx_timeline_menu_item_manage .bx-stl-mil-wo-gaps-sm .sys-action-element-holder > a, 
+    .bx-artificer .bx-popup .bx-menu-object-sys_cmts_item_manage .bx-stl-mil-wo-gaps-sm .sys-action-element-holder > a {
+        @apply px-4;
+    }
+
+
+    /* 3d Party: Flickity */
+    .bx-artificer .flickity-button {
+        @apply text-gray-700 dark:text-gray-300 hover:text-gray-800 dark:hover:text-white bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 opacity-75 hover:opacity-100;
+    }
+
+
+    /* 3d Party: Quill */
+    .bx-artificer div.ql-editor {
+        @apply bg-gray-50 dark:bg-gray-900/50 focus:bg-white dark:focus:bg-gray-900;
+    }
+
+    .bx-artificer div.ql-editor:focus{
+        @apply border border-gray-300 dark:border-gray-700 focus:border-blue-500 dark:focus:border-blue-500 focus:border-opacity-70 focus:ring-2 focus:ring-blue-500 focus:ring-opacity-20 focus:outline-none;
+    }
+
+    .bx-artificer .ql-mention-list li {
+        @apply flex items-center px-4 py-2 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:text-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 text-sm font-normal;
+    }
+    
+    /* Modules: Editor */
+    .editor_toolbar button.selected{
+        @apply bg-gray-300 dark:bg-black/50 dark:text-gray-200 text-gray-800;
+    }
+
+    /* Modules: Forum. Units - new view, for UNA14 need to migrate in general */
+    .bx-artificer .bx-base-unit-showcase-wrapper.bx-forum-wrapper:not(.flickity-enabled)
+    {
+        grid-auto-rows: 0; 
+        grid-template-rows: repeat(1, 1fr);
+        overflow-y: hidden;
+    }
+    
+    .bx-artificer .bx-base-text-unit-gallery-wrapper.bx-forum-wrapper,
+    .bx-artificer .bx-base-unit-showcase-wrapper.bx-forum-wrapper:not(.flickity-enabled)
+    {
+        @apply max-w-full mx-auto grid sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4 md:max-w-none;
+    }
+    
+    .bx-artificer .bx-layout-column-wide .bx-base-text-unit-gallery-wrapper.bx-forum-wrapper,
+    .bx-artificer .bx-layout-column-wide .bx-base-unit-showcase-wrapper.bx-forum-wrapper:not(.flickity-enabled)
+    {
+        @apply max-w-full mx-auto grid sm:grid-cols-2 md:grid-cols-3 gap-4 md:max-w-none;
+    }
+    
+    .bx-artificer .bx-layout-column-half .bx-base-text-unit-gallery-wrapper.bx-forum-wrapper,
+    .bx-artificer .bx-layout-column-half .bx-base-unit-showcase-wrapper.bx-forum-wrapper:not(.flickity-enabled)
+    {
+        @apply max-w-full mx-auto grid sm:grid-cols-1 md:grid-cols-2 gap-4 md:max-w-none;
+    }
+    .bx-artificer .bx-layout-column-thin .bx-base-text-unit-gallery-wrapper.bx-forum-wrapper,
+    .bx-artificer .bx-layout-column-thin .bx-base-unit-showcase-wrapper.bx-forum-wrapper:not(.flickity-enabled)
+    {
+        @apply max-w-full mx-auto grid sm:grid-cols-1 gap-4 md:max-w-none;
+    }
+    .bx-artificer .bx-base-unit-showcase-wrapper.bx-forum-wrapper.flickity-enabled
+    { 
+       @apply block;
+    }
+    .bx-artificer .bx-base-unit-showcase-wrapper.bx-forum-wrapper .flickity-slider{ 
+       @apply space-x-4 ;
+    }
+    .bx-artificer .bx-search-result-block.bx-clearfix:before{
+        @apply hidden;
+    }
+
+    /* badges */
+    .bx-badges-list{
+        @apply inline-flex grow-0 space-x-1 w-min
+    }
+    .bx-badge{
+        @apply inline-flex items-center w-max space-x-1 px-1.5 py-0.5 rounded-full;
+        @apply text-white text-xs font-medium whitespace-nowrap;
+        @apply opacity-90 hover:opacity-100 active:translate-y-0.5
+    }
+    .bx-badge img{
+        @apply max-h-4;
+    }
+    .sys-icon-image.bx-badge-icon,
+    .sys-icon-svg.bx-badge-icon{
+        @apply w-4 h-4
+    }
+    
+    /* filepond */
+    .filepond--drop-label label{
+        @apply leading-6 text-base font-medium text-gray-700 dark:text-gray-300;
+    }
+}

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/plugins_public/tailwind/css/tailwind.min.css


+ 96 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/template/css/cmts.css

@@ -0,0 +1,96 @@
+ul.cmts {
+    margin: 0px;
+    padding: 0px;
+    list-style: none;
+}
+ul.cmts-margin,
+div.cmt-reply-margin {
+    margin-top: 1.0rem;
+    padding-left: 3.5rem;
+}
+
+/*--- Minified Post Form ---*/
+div.cmt-reply.cmt-reply-min div.cmt-body {
+    display: none;
+}
+
+/*--- Add Comment/Reply Form Styles ---*/
+form.cmt-post-reply div#bx-form-element-cmt_image,
+form.cmt-post-reply .bx-form-input-files-uploaders {
+    margin-top: 0px;
+    position: relative;
+}
+
+form.cmt-post-reply .bx-form-input-files-uploaders a.cmts-uploader-button {
+    position: absolute;
+    z-index: 1;
+    right: 0px;
+    -webkit-transform: translateY(-100%); 
+    transform: translateY(-100%);  
+    margin-top: 0px;
+}
+
+/*--- Comment ---*/
+span.cmt-reply-to a {
+    display: inline-block !important;
+}
+span.cmt-reply-to a .bx-base-pofile-unit-wo-info .bx-base-pofile-unit-online {
+    display: none;
+}
+
+/*--- Comment Body ---*/
+div.cmt-cont .cmt-body {
+    position: relative;
+}
+div.cmt-cont .cmt-body .cmt-body-overflow {
+    display: none;
+}
+div.cmt-cont .cmt-body.bx-overflow .cmt-body-overflow {
+    display: block;
+}
+
+div.cmt-cont .cmt-post-reply .bx-form-element-wrapper {
+    margin-top: 0.5rem;
+}
+div.cmt-cont .cmt-post-reply .bx-form-element-wrapper:first-child {
+    margin-top: 0;
+}
+
+/*--- Menu action ---*/
+.bx-menu-object-sys_cmts_item_manage .menu-custom-item-ver a {
+    display: block;
+    width: 100%;
+    box-sizing: border-box;
+}
+.bx-menu-object-sys_cmts_item_manage .bx-report .sys-action-element-holder{
+    padding: 0px !important;
+}
+.bx-menu-object-sys_cmts_item_manage span.bx-def-margin-thd-topbottom,
+.bx-menu-object-sys_cmts_item_manage .bx-report-element-holder,
+.bx-menu-object-sys_cmts_item_manage .sys-action.bx-report{
+    margin: 0px !important;
+    width:100%;
+    display: block;
+}
+
+/*--- Menu meta ---*/
+span.cmt-reply-to a .bx-def-icon-size {
+    width: 1.0rem;
+    height: 1.0rem;
+    line-height: 0.875rem;
+    font-size: 0.6rem;
+}
+
+/*--- Comment Element Styles ---*/
+.cmt-counter .bx-base-pofile-unit-online {
+    display: none;
+}
+.cmt-counter .cmt-counter-text {
+    margin-left: 0.5rem;
+    margin-right: 0.5rem;
+}
+
+/*--- PhotoSwipe Styles ---*/
+.cmts.pswp {
+    z-index: 15000;
+}

+ 687 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/template/css/forms.css

@@ -0,0 +1,687 @@
+
+.bx-form-advanced-wrapper .bx-form-section-wrapper:first-child {
+    margin-top:0;
+}
+
+.bx-form-advanced-wrapper .bx-form-section-divider:first-child {
+    border-top:none; 
+    padding-top:0; 
+}
+
+.bx-form-advanced-wrapper .bx-form-section-divider:first-child .bx-form-section-content {
+    padding-top:0; 
+}
+
+.bx-form-advanced-wrapper .bx-form-section-wrapper:last-child .bx-form-section-content {
+    padding-bottom:0;
+}
+
+.bx-form-advanced-wrapper .bx-form-section-wrapper:first-child .bx-form-row-view-wrapper:first-child {
+    padding-top:0;
+}
+
+.bx-form-section-wrapper {
+    position:relative;
+}
+
+.bx-form-section {
+    padding:0px;
+}
+
+.bx-form-section {
+    margin:0;
+    border: none;
+}
+
+.bx-form-section-header .bx-form-section-title {
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+    -webkit-flex-wrap: wrap;
+    flex-wrap: wrap;
+
+    line-height: 1.25rem;
+    text-transform: uppercase;
+}
+
+.bx-form-section-header .bx-form-section-title u {
+    cursor: pointer;
+}
+
+.bx-form-section-header .bx-form-section-title span {
+    width: 100%;
+    padding-left: 1.25rem;
+
+    white-space: nowrap;
+    text-transform: none;
+    font-size: 0.75rem;
+}
+
+.bx-form-section-hidden .bx-form-section-content {
+    display:none;
+}
+
+.bx-form-caption {
+    margin-bottom:5px;
+}
+
+.bx-form-required {
+    color:red;
+}
+
+.bx-form-warn {
+    color:red;
+    font-style:italic;
+}
+
+.bx-form-option-info {
+    display: inline-flex;
+    flex-direction: column;
+}
+
+.bx-form-info {
+
+}
+
+/* section toggler */
+
+.bx-form-collapsable .bx-form-section-toggler {
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+    -webkit-justify-content: center;
+    justify-content: center;
+
+    width: 1.25rem;
+    height: 1.25rem;
+
+    text-decoration:none;
+
+    -moz-transform: rotate(90deg);
+    -webkit-transform: rotate(90deg);
+    -ms-transform: rotate(90deg);
+    -o-transform: rotate(90deg);
+
+    -webkit-transition: all 0.5s ease;
+    -moz-transition: all 0.5s ease;
+    -ms-transition: all 0.5s ease;
+    -o-transition: all 0.5s ease;
+
+    line-height: 1.25rem;
+}
+
+.bx-form-collapsed .bx-form-section-toggler {
+    -moz-transform: rotate(0deg);
+    -webkit-transform: rotate(0deg);
+    -ms-transform: rotate(0deg);
+    -o-transform: rotate(0deg);
+}
+
+.bx-form-section-toggler .sys-icon {
+    font-size: 0.875rem;
+}
+
+/* form inputs */
+.bx-form-input-wrapper {
+    position: relative;
+}
+
+.bx-form-input-disabled {}
+
+.bx-form-input-slider,
+.bx-form-input-doublerange,
+.bx-form-input-select_multiple,
+.bx-form-input-select,
+.bx-form-input-radio_set,
+.bx-form-input-checkbox_set,
+.bx-form-input-number,
+.bx-form-input-time,
+.bx-form-input-datepicker,
+.bx-form-input-datetime,
+.bx-form-input-textarea,
+.bx-form-input-wrapper-password,
+.bx-form-input-text,
+.bx-form-input-price,
+.bx-form-input-wrapper-price span {
+
+    height:40px;
+    height:2.5rem;
+    width:100%;
+    
+    -moz-box-sizing: border-box;
+    -ms-box-sizing: border-box;
+    -webkit-box-sizing: border-box;
+    box-sizing: border-box;
+
+    -webkit-appearance: none;
+    -moz-appearance: none;
+    appearance: none;
+
+    padding:7px 3px 7px 3px;
+    padding:0.2rem 0.4rem;
+
+    vertical-align:middle;
+
+    border:2px solid #79bd9a;
+    border:0.125rem solid #79bd9a;
+	
+    -webkit-border-radius:0.3rem;
+    -moz-border-radius:0.3rem;	
+    border-radius:0.3rem;
+
+    outline:none;
+
+    -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=90)"; 
+    filter: alpha(opacity=90); 
+    -moz-opacity: 0.9;
+    -khtml-opacity: 0.9;
+    opacity: 0.9;    
+}
+
+.bx-form-input-slider:focus,
+.bx-form-input-doublerange:focus,
+.bx-form-input-number:focus,
+.bx-form-input-time:focus,
+.bx-form-input-datepicker:focus,
+.bx-form-input-datetime:focus,
+.bx-form-input-textarea:focus,
+.bx-form-input-text:focus,
+.bx-form-input-price:focus,
+.bx-form-input-wrapper-password.bx-form-input-focus {
+    border-color:#6caa8a;
+
+    -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; 
+    filter: alpha(opacity=100); 
+    -moz-opacity: 1.0;
+    -khtml-opacity: 1.0;
+    opacity: 1.0;
+
+    -webkit-transition: all 0.2s linear;
+    -moz-transition: all 0.2s linear;
+    -o-transition: all 0.2s linear;
+    transition: all 0.2s linear;
+}
+.bx-form-input-wrapper-password {
+    padding: 0 2.125rem 0 0;
+}
+.bx-form-input-wrapper-password > a {
+    position: absolute;
+    z-index: 1000;
+    
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+    -webkit-justify-content: center;
+    justify-content: center;
+
+    top: 0;
+    right: 0;    
+    height: 100%;
+
+    margin: 0 0.5rem;
+}
+.bx-form-input-password {
+    position: relative;
+
+    -moz-box-sizing: border-box;
+    -ms-box-sizing: border-box;
+    -webkit-box-sizing: border-box;
+    box-sizing: border-box;
+
+    width: 100%;
+    height: 100%;
+
+    margin: 0;
+    padding: 7px 3px 7px 3px;
+    padding: 0.2rem 0.4rem;
+
+    border: 0;
+    background-color: transparent;
+}
+.bx-form-input-password:focus {
+    border-color: transparent;
+    outline: none;
+    box-shadow: none;
+}
+
+.bx-form-input-select_multiple,
+.bx-form-input-radio_set,
+.bx-form-input-checkbox_set {
+    height:auto;
+}
+
+.bx-form-value .bx-form-input-radio_set,
+.bx-form-value .bx-form-input-checkbox_set {
+    line-height: 1.25;
+}
+
+.bx-form-value .bx-form-input-radio_set label,
+.bx-form-value .bx-form-input-checkbox_set label {
+    padding-left: 0.25rem;
+}
+
+.bx-form-input-textarea {
+    padding:0.5rem;
+    height:150px;
+    height:10rem;
+
+    line-height:normal;
+}
+
+.bx-form-input-wrapper-dateselect select{
+    min-width:70px;
+}
+.bx-form-input-wrapper-dateselect select.month{
+    min-width:120px;
+}
+
+.bx-form-input-datetime {
+    width:10.2rem;
+}
+
+.bx-form-datepicker-modal {
+    z-index:10001 !important;
+}
+
+.bx-form-input-number,
+.bx-form-input-time
+{
+    width:6rem;
+}
+
+.bx-form-input-wrapper-file .bx-btn {
+	float: none;
+}
+.bx-form-input-file {
+    position: fixed;
+    top: -1000px;
+
+    border: none;
+    background-color: transparent;
+    visibility: hidden;
+}
+
+.bx-form-input-wrapper-slider,
+.bx-form-input-wrapper-doublerange {
+    position:relative;
+}
+
+.bx-form-input-wrapper-slider .ui-slider,
+.bx-form-input-wrapper-doublerange .ui-slider {
+    position:absolute;
+    top:0px;
+    right:0px;
+}
+
+.bx-form-input-wrapper-slider .ui-slider .ui-slider-handle,
+.bx-form-input-wrapper-doublerange .ui-slider .ui-slider-handle { 
+    z-index: 1;
+}
+
+/* price */
+.bx-form-element .bx-form-value .bx-form-input-wrapper-price {
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: stretch;
+    align-items: stretch;
+}
+.bx-form-element .bx-form-value .bx-form-input-wrapper-price span {
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+
+    width: auto;
+    border-right-width: 0;
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+}
+.bx-form-input-wrapper-price span {
+    background-color: rgba(249, 250, 251, 1.0);
+}
+.bx-form-element .bx-form-value .bx-form-input-price:not(:first-child) {
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+}
+
+/* privacy group chooser */
+.bx-form-input-wrapper-pgc {
+    position: relative;
+}
+
+.bx-form-input-pgc {
+    position: absolute;
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+    -webkit-justify-content: center;
+    justify-content: center;
+    top: -1.375rem;
+    right: 0;
+    width: 1rem;
+    height: 1rem;
+}
+
+.bx-form-input-pgc-popup .bx-form-input-wrapper-input_set {
+    display: -webkit-flex;
+    display: flex;
+    -webkit-flex-wrap: nowrap;
+    flex-wrap: nowrap;
+    -webkit-align-items: center;
+    align-items: center;
+}
+
+/* help popup */
+.bx-form-input-help-popup {
+    max-width: 25rem;
+    max-height: 25rem;
+    overflow: auto;
+}
+
+/* switcher element */
+.bx-switcher-cont input {
+   display: none !important;
+}
+
+/* oneline element */
+.bx-form-element-wrapper-oneline .bx-form-element {
+    display: -webkit-flex;
+    display: flex;
+    -webkit-flex-flow: row nowrap;
+    flex-flow: row nowrap;
+    -webkit-align-items: center;
+    align-items: center;
+    -webkit-justify-content: flex-start;
+    justify-content: flex-start;
+}
+.bx-form-value-oneline,
+.bx-form-caption-oneline {
+    float: none;
+}
+.bx-form-caption-oneline {
+    margin-bottom: 0;
+}
+
+.bx-form-input-wrapper-submit .bx-btn {
+    float: none;
+}
+.bx-form-input-wrapper-input_set .bx-btn {
+    float:left;
+}
+
+/* extra features */
+
+.bx-form-right-line-aligned {
+    float:right;
+    margin-top: 5px;
+}
+
+/* view mode */
+.bx-form-row-view-wrapper > div {
+	position: relative;
+    display: inline-block;
+    width: 100%;
+
+    vertical-align: bottom;
+    white-space: normal;
+    text-overflow: ellipsis;
+    overflow: hidden;
+}
+.bx-form-row-view-ratevalue{
+	display: flex !important;
+	justify-content: flex-start;
+}
+.bx-form-row-view-ratevalue .bx-form-row-view-value{
+    flex-grow: 2;
+}
+.bx-form-row-view-wrapper-textarea > div {
+    vertical-align:top;
+}
+
+.bx-form-row-view-caption {
+    font-weight:bold;
+}
+
+
+/****************************************
+*		Custom reCaptcha Theme
+****************************************/
+.bx-form-input-captcha-custom {
+	background-color: #b71800;
+	padding: 0.5em;
+	border-radius: 1em;
+	position: relative;
+	overflow: auto;
+	max-width: 480px;
+	font: 0.75em/1em Helvetica, Arial, sans-serif;
+	color: #ffffff;
+}
+.bx-form-input-captcha-custom a {
+	color: #ffffff;
+	text-decoration: none;
+}
+.bx-form-input-captcha-custom img,
+.bx-form-input-captcha-custom #recaptcha_image {
+	width: 100% !important;
+	height: auto !important;
+	-webkit-border-radius: 0.5em;
+	-moz-border-radius: 0.5em;
+	border-radius: 0.5em;
+	-moz-background-clip: padding;
+	-webkit-background-clip: padding-box;
+	background-clip: padding-box;
+}
+.bx-form-input-captcha-custom .solution {
+	position: relative;
+	float: left;
+	width: 75%;
+}
+.bx-form-input-captcha-custom .solution-cnt {
+	margin-top: 0.5em;
+	padding: 1.2em 1em;
+	-webkit-border-radius: 0.5em;
+	-moz-border-radius: 0.5em;
+	border-radius: 0.5em;
+	-moz-background-clip: padding;
+	-webkit-background-clip: padding-box;
+	background-clip: padding-box;
+	background-color: #fbe098;
+	color: #000000;
+	display: block;
+}
+.bx-form-input-captcha-custom .solution-cnt input {
+	display: block;
+	width: calc(90% - 2px);
+	margin: 0.35em auto;
+	-webkit-appereance: none;
+	border: 1px solid #f7c236;
+	font: inherit;
+	font-size: 150%;
+	outline: none;
+}
+.bx-form-input-captcha-custom .solution-cnt input:focus {
+	border-color: #b71800;
+}
+.bx-form-input-captcha-custom .options {
+	position: relative;
+	float: right;
+	width: 25%;
+	
+}
+.bx-form-input-captcha-custom .options-cnt {
+	margin-top: 0.5em;
+	padding-left: 0.5em;
+	-webkit-border-radius: 0.5em;
+	-moz-border-radius: 0.5em;
+	border-radius: 0.5em;
+	-moz-background-clip: padding;
+	-webkit-background-clip: padding-box;
+	background-clip: padding-box;
+}
+.bx-form-input-captcha-custom .options-cnt a {
+	background-color: #c5523d;
+	display: block;
+	padding: 0.5em 1.0em;
+	border-top: 1px solid #000000;
+	border-left: 1px solid #000000;
+	border-right: 1px solid #000000;
+}
+.bx-form-input-captcha-custom .options-cnt a:first-child {
+	border-radius: 0.5em 0.5em 0 0;
+}
+.bx-form-input-captcha-custom .options-cnt a:last-child {
+	border-radius: 0 0 0.5em 0.5em;
+	border-bottom: 1px solid #000000;
+}
+
+
+/****************************************
+* Overwrite 3d Party MiniColors plug-in
+****************************************/
+.bx-form-input-wrapper.bx-form-input-wrapper-text .minicolors-theme-bootstrap .minicolors-swatch {
+	top: 6px;
+	left: 6px;
+
+	width: 25px;
+	width: 1.5625rem;
+	height: 25px;
+	height: 1.5625rem;
+
+	border-radius: 20px;
+	border-radius: 1.25rem;
+}
+
+/* auto-siggestion input */
+
+.bx-form-value .bx-form-input-autotoken {
+    float:left;
+    padding:0px;
+    height:auto;
+    box-shadow: none;
+    padding: 2px;
+}
+.bx-form-value .bx-form-input-autotoken b {
+    cursor:pointer;
+    display:block;
+    float:left;
+    padding-right: 1.5em; 
+    margin:0.1em;
+    font-weight:normal;
+    border-radius: 1.25rem;
+}
+.bx-form-value .bx-form-input-autotoken b.val-wo-icon {
+    padding-left: 1.5em;
+}
+.bx-form-value .bx-form-input-autotoken b:hover {
+    opacity:0.7;
+}
+.bx-form-value .bx-form-input-autotoken b:after {
+    font-family: 'sys-user', 'sys-user-fal';
+    content: "\f00d";    
+    position:absolute;    
+    padding:0 0.5em;
+    margin-left: 0.25em;
+    font-size:0.7em;
+    font-weight:bold;
+}
+.bx-form-value .bx-form-input-autotoken.bx-form-input-disabled b {
+    cursor: default;
+}
+.bx-form-value .bx-form-input-autotoken.bx-form-input-disabled b:after{
+    display: none;
+}
+.bx-form-value .bx-form-input-autotoken input {
+    border:0;
+    margin:0;
+    padding:0 0 0 5px;
+    width:auto;
+    float:left;
+    box-shadow: none;
+}
+.bx-form-value .bx-form-input-autotoken .bx-def-thumb-size {
+    width: 2.5rem;
+    height: 2.5rem;
+    line-height: 2.5rem;
+	float:left;	
+}
+.bx-form-value .bx-form-input-autotoken .bx-base-pofile-unit-wo-info {
+    display: inline-block;
+    vertical-align: middle;
+
+    margin-right: 8px;
+    margin-right: 0.5rem;
+}
+.bx-form-value .bx-form-input-labels-sel b {
+    display:inline-block;
+    line-height:1.5rem;
+    font-size:85%;    
+    padding-right:1em;
+    margin-top:0.625rem;
+    font-style:normal;
+}
+.bx-form-value .bx-form-input-labels-sel b:after {
+    display:none;
+}
+.select2-container {
+    width:100% !important;
+}
+
+/* Location field */
+
+.bx-form-input-location-cont:not(.minimized) .bx-form-input-location-country,
+.bx-form-input-location-cont:not(.minimized) .bx-form-input-location-zip,
+.bx-form-input-location-cont:not(.minimized) .bx-form-input-location-state,
+.bx-form-input-location-cont:not(.minimized) .bx-form-input-location-city {
+    width:49%;
+}
+
+.bx-form-input-location-street {
+    width:83%;
+}
+
+.bx-form-input-location-street_number {
+    width:15%;
+}
+
+.bx-form-input-location-street_number,
+.bx-form-input-location-street,
+.bx-form-input-location-state,
+.bx-form-input-location-city {
+    margin-bottom:0.25rem;
+}
+
+.bx-form-input-location-cont .bx-form-input-location-status-cont {
+    float:none;
+    width:98%;
+    line-height:normal;
+}
+.bx-form-input-location-cont.minimized .bx-form-input-location-status-cont {
+    display: flex;
+    flex-direction: column;
+    width: auto;
+}
+.bx-form-input-location-status {
+    float:left;
+}
+.bx-form-input-location-copyright {
+    float:right;
+}
+.bx-form-input-location-cont.minimized .bx-form-input-location-copyright {
+    float: none;
+    margin-top: 0.25rem;
+}
+
+.bx-form-input-location-auto-cont {
+    display:table;
+}
+.bx-form-input-location-auto-cont > div {
+    display:table-cell;
+    vertical-align:middle;
+}
+.combodate {
+    display: flex;
+}

+ 4 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/template/form_field_option_info.html

@@ -0,0 +1,4 @@
+<div class="bx-form-option-info">
+    <div>__input__</div>
+    <div>__info__</div>
+</div>

+ 21 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/template/paginate.html

@@ -0,0 +1,21 @@
+<div class="bx-paginate __paginate_class__ flex items-center" <bx_repeat:attrs>__key__="__value__" </bx_repeat:attrs>>
+    <div class="bx-paginate-btns bx-btn-group flex space-x-4">
+        __btn_prev__
+        __btn_next__
+    </div>
+    <bx_if:info>
+        <div class="bx-paginate-info pl-4">
+            __text__
+        </div>
+    </bx_if:info>
+    <bx_if:total>
+        <div class="bx-paginate-info bx-pi-total pl-4">
+            __text__
+        </div>
+    </bx_if:total>
+    <bx_if:view_all>
+        <div class="bx-paginate-view-all pl-4">
+            <a href="__lnk_url__" title="__lnk_title__">__lnk_content__</a>
+        </div>
+    </bx_if:view_all>
+</div>

+ 310 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBaseAccountForms.php

@@ -0,0 +1,310 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * System profile(account) forms functions
+ * @see BxDolProfileForms
+ */
+class BxBaseAccountForms extends BxDolProfileForms
+{
+    protected $_iProfileId;
+
+    static public $PROFILE_FIELDS = array();
+
+    public function __construct()
+    {
+        parent::__construct();
+        $this->_iProfileId = bx_get_logged_profile_id();
+    }
+
+    public function getObjectFormAdd ()
+    {
+        return BxDolForm::getObjectInstance('sys_account', 'sys_account_create');
+    }
+
+    public function getObjectFormEdit ()
+    {
+        return BxDolForm::getObjectInstance('sys_account', 'sys_account_settings_info');
+    }
+
+    public function getObjectFormDelete ()
+    {
+        return BxDolForm::getObjectInstance('sys_account', 'sys_account_settings_del_account');
+    }
+
+    public function createAccountForm ($aParams = array())
+    {
+        $bIsApi = bx_is_api();
+
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = BxDolAccount::isAllowedCreate (0)))
+            return MsgBox($sMsg);
+
+        // check and display form
+        $oForm = $this->getObjectFormAdd ();
+        if (!$oForm)
+            return MsgBox(_t('_sys_txt_error_occured'));
+
+        $oForm->aFormAttrs['action'] = !empty($aParams['action']) ? $aParams['action'] : bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=create-account'));
+        $oForm->initChecker(self::$PROFILE_FIELDS);
+
+        bx_alert('account', 'add_form_check', 0, 0, array(
+            'form_object' => &$oForm
+        ));
+
+        if (!$oForm->isSubmittedAndValid()) {
+            
+            $sCode = $oForm->getCode();
+
+            bx_alert('account', 'add_form', 0, 0, array(
+                    'form_object' => &$oForm,
+                    'form_code' => &$sCode
+            ));
+            
+            if($bIsApi){
+                if (!$oForm)
+                    return false;
+                return $oForm->getCodeAPI();
+            }
+
+            return $sCode;
+        }
+
+        // insert data into database
+        $aValsToAdd = array (
+            'email_confirmed' => 0,
+        );
+        $iAccountId = $oForm->insert ($aValsToAdd);
+        if (!$iAccountId) {
+            if (!$oForm->isValid())
+                return $bIsApi ? $oForm->getCodeAPI() : $oForm->getCode();
+            else
+                return MsgBox(_t('_sys_txt_error_account_creation'));
+        }
+
+        $iProfileId = $this->onAccountCreated($iAccountId, $oForm->isSetPendingApproval());
+
+        // perform action
+        BxDolAccount::isAllowedCreate ($iProfileId, true);
+
+        $this->_iProfileId = bx_get_logged_profile_id();
+
+        $sRelocateCustom = $oForm->getCleanValue('relocate');
+        $bRelocateCustom = !empty($sRelocateCustom);
+
+        // check and redirect
+        $aModulesProfile = array(); 
+        $aModules = BxDolModuleQuery::getInstance()->getModulesBy(array('type' => 'modules', 'active' => 1));
+        foreach($aModules as $aModule) {
+        	$oModule = BxDolModule::getInstance($aModule['name']);
+        	if($oModule instanceof iBxDolProfileService && BxDolService::call($aModule['name'], 'act_as_profile') === true)
+        		$aModulesProfile[] = $aModule;
+        }
+
+        $sDefaultProfileType = getParam('sys_account_default_profile_type');
+        if(count($aModulesProfile) == 1)
+        	$sProfileModule = $aModulesProfile[0]['name'];
+        else if(!empty($sDefaultProfileType)) 
+            $sProfileModule = $sDefaultProfileType;
+
+        if (getParam('sys_account_auto_profile_creation') && !empty($sProfileModule)) {
+            $oAccount = BxDolAccount::getInstance($iAccountId);
+            $aProfileInfo = BxDolService::call($sProfileModule, 'prepare_fields', array(array(
+                'author' => $iProfileId,
+                'name' => $oAccount->getDisplayName(),
+            )));
+            
+            $a = BxDolService::call($sProfileModule, 'entity_add', array($iProfileId, $aProfileInfo));
+
+            // in case of successful profile add redirect to the page after profile creation
+            if (0 == $a['code'] && !$bIsApi) {
+                if($bRelocateCustom)
+                    $this->_redirectAndExit($sRelocateCustom, false);
+
+                BxDolService::call($sProfileModule, 'redirect_after_add', array($a['content']));
+                return;
+            }
+            // if creation failed, redirect to create profile form
+        }
+        
+        if($bIsApi) 
+            return true;
+
+        $sRelocate = !empty($sProfileModule) ? BxDolService::call($sProfileModule, 'profile_create_url', array(false)) : '';
+        if(empty($sRelocate))
+            $sRelocate = $bRelocateCustom ? $sRelocateCustom : getParam('sys_redirect_after_account_added');
+    
+        $this->_redirectAndExit($sRelocate, true, array(
+            'account_id' => $iAccountId,
+            'profile_id' => $iProfileId,
+        ));
+
+    }
+
+    public function onAccountCreated ($iAccountId, $isSetPendingApproval, $iAction = BX_PROFILE_ACTION_MANUAL, $bNeedToLogin = true)
+    {
+        // alert
+        bx_alert('account', 'add', $iAccountId, 0);
+
+        // if email_confirmation procedure is enabled - send email confirmation letter
+        $oAccount = BxDolAccount::getInstance($iAccountId);
+        if (BxDolAccount::isNeedConfirmEmail() && $oAccount && !$oAccount->isConfirmedEmail())
+            $oAccount->sendConfirmationEmail($iAccountId);
+
+        // add account and content association
+        $iProfileId = BxDolProfile::add(BX_PROFILE_ACTION_MANUAL, $iAccountId, $iAccountId, BX_PROFILE_STATUS_PENDING, 'system');
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+
+        // approve profile if auto-approval is enabled and profile status is 'pending'
+        $sStatus = $oProfile->getStatus();
+        $isAutoApprove = !$isSetPendingApproval;
+        if ($sStatus == BX_PROFILE_STATUS_PENDING && $isAutoApprove)
+            $oProfile->approve(BX_PROFILE_ACTION_AUTO, $iProfileId, getParam('sys_account_activation_letter') == 'on');
+
+        // alert
+        bx_alert('account', 'added', $iAccountId);
+
+        // login to the created account automatically
+        if ($bNeedToLogin)
+            bx_login($iAccountId);
+
+        return $iProfileId;
+    }
+
+    public function editAccountEmailSettingsForm ($iAccountId)
+    {
+        return $this->_editAccountForm ($iAccountId, 'sys_account_settings_email');
+    }
+
+    public function editAccountPasswordSettingsForm ($iAccountId)
+    {
+        return $this->_editAccountForm ($iAccountId, 'sys_account_settings_pwd');
+    }
+
+    public function editAccountInfoForm ($iAccountId)
+    {
+        return $this->_editAccountForm ($iAccountId, 'sys_account_settings_info');
+    }
+
+    public function deleteAccountForm ($iAccountId)
+    {
+        $bIsApi = bx_is_api();
+        $oAccount = BxDolAccount::getInstance($iAccountId);
+        $aAccountInfo = $oAccount ? $oAccount->getInfo() : false;
+        if (!$aAccountInfo)
+            return $bIsApi ? _t('_sys_txt_error_account_is_not_defined') : MsgBox(_t('_sys_txt_error_account_is_not_defined'));
+
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = BxDolAccount::isAllowedDelete ($this->_iProfileId, $aAccountInfo)))
+            return $bIsApi ? $sMsg : MsgBox($sMsg);
+
+        // check and display form
+        $oForm = $this->getObjectFormDelete();
+        if(bx_get('content') !== false)
+            $oForm->aInputs['delete_content']['value'] = (int)bx_get('content');
+
+        if (!$oForm)
+            return $bIsApi ? _t('_sys_txt_error_occured') : MsgBox(_t('_sys_txt_error_occured'));
+
+        if (!$oForm->isSubmitted())
+            unset($aAccountInfo['password']);
+
+        $oForm->initChecker($aAccountInfo);
+
+        if (!$oForm->isSubmittedAndValid())
+            return $bIsApi ? $oForm->getCodeAPI() : $oForm->getCode();
+
+        // delete account
+        $oAccount = BxDolAccount::getInstance($aAccountInfo['id']);
+        if (!$oAccount->delete(false === bx_get('delete_content') ? true : (int)$oForm->getCleanValue('delete_content') != 0))
+            return $bIsApi ? _t('_sys_txt_error_account_delete') : MsgBox(_t('_sys_txt_error_account_delete'));
+
+        // logout from deleted account
+        if($iAccountId == getLoggedId())
+        	bx_logout();
+
+        // redirect to homepage
+        $this->_redirectAndExit('', false);
+    }
+
+    protected function _editAccountForm ($iAccountId, $sDisplayName)
+    {
+        $bIsApi = bx_is_api();
+        
+        $oAccount = BxDolAccount::getInstance($iAccountId);
+        $aAccountInfo = $oAccount ? $oAccount->getInfo() : false;
+        if (!$aAccountInfo)
+            return MsgBox(_t('_sys_txt_error_account_is_not_defined'));
+
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = BxDolAccount::isAllowedEdit ($this->_iProfileId, $aAccountInfo)))
+            return MsgBox($sMsg);
+
+        // check and display form
+        $oForm = BxDolForm::getObjectInstance('sys_account', $sDisplayName);
+        if (!$oForm)
+            return MsgBox(_t('_sys_txt_error_occured'));
+
+        if (!$oForm->isSubmitted())
+            unset($aAccountInfo['password']);
+
+        $oForm->initChecker($aAccountInfo);
+
+        if (!$oForm->isSubmittedAndValid())
+            return $bIsApi ? $oForm->getCodeAPI() : $oForm->getCode();
+
+        $aTrackTextFieldsChanges = array (); // track text fields changes, not-null(for example empty array) - means track, null - means don't track
+
+        // update email and email setting in DB
+        if (!$oForm->update ($aAccountInfo['id'], array(), $aTrackTextFieldsChanges)) {
+            if (!$oForm->isValid())
+                return $bIsApi ? $oForm->getCodeAPI() : $oForm->getCode();
+            else
+                return MsgBox(_t('_sys_txt_error_account_update'));
+        }
+
+        // check if email was changed
+        if (!empty($aTrackTextFieldsChanges['changed_fields']) && in_array('email', $aTrackTextFieldsChanges['changed_fields'])){
+            $oAccount = BxDolAccount::getInstance($iAccountId, true); // refresh account to clear cache 
+            $oAccount->updateEmailConfirmed(false);  // mark email as unconfirmed
+        }
+
+        // check if password was changed
+        if ($oForm->getCleanValue('password')) {
+            // relogin with new password
+			bx_alert('account', 'edited', $aAccountInfo['id'], $aAccountInfo['id'], array('action' => 'change_password'));
+            bx_logout();
+            bx_login($aAccountInfo['id']);
+        }
+
+        // check if other text info was changed - if auto-appproval is off
+        $isAutoApprove = $oForm->isSetPendingApproval() ? false : true;
+        if (!$isAutoApprove) {
+            $oProfile = BxDolProfile::getInstanceAccountProfile($aAccountInfo['id']); // get profile associated with account, not current porfile
+            $aProfileInfo = $oProfile->getInfo();
+            unset($aTrackTextFieldsChanges['changed_fields']['email']); // email confirmation is automatic and separate, don't need to deactivate whole profile if email is changed
+            if (BX_PROFILE_STATUS_ACTIVE == $aProfileInfo['status'] && !empty($aTrackTextFieldsChanges['changed_fields']))
+                $oProfile->disapprove(BX_PROFILE_ACTION_AUTO);  // change profile to 'pending' only if some text fields were changed and profile is active
+        }
+
+        // create an alert
+        bx_alert('account', 'edited', $aAccountInfo['id'], $aAccountInfo['id'], array('display' => $sDisplayName));
+
+        // display result message            
+        $sMsg = MsgBox(_t('_' . $sDisplayName . '_successfully_submitted'));
+        
+        if ($bIsApi){
+            return ['form' => $oForm->getCodeAPI(), 'msg' => _t('_' . $sDisplayName . '_successfully_submitted')];
+        }
+        
+        return $sMsg . $oForm->getCode();
+    }     
+}
+
+/** @} */

+ 678 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBaseCmtsServices.php

@@ -0,0 +1,678 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * System services related to Comments.
+ */
+class BxBaseCmtsServices extends BxDol
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    public function serviceGetAuthor($iCmtUniqId)
+    {
+        $oCmts = BxDolCmts::getObjectInstanceByUniqId($iCmtUniqId);
+        if(!$oCmts)
+            return [];
+
+        $aCmt = BxDolCmtsQuery::getCommentExtendedByUniqId($iCmtUniqId);
+        if(empty($aCmt) || !is_array($aCmt))
+            return [];
+
+        return $oCmts->serviceGetAuthor((int)$aCmt['cmt_id']);
+    }
+    
+    public function serviceGetAuthorBlock()
+    {
+        if(bx_is_api()){
+            $iCommentId = bx_process_input(bx_get('cmt_id'), BX_DATA_INT);
+            $sSystem =  bx_process_input(bx_get('sys'));
+            $iObjectId = bx_process_input(bx_get('id'), BX_DATA_INT);
+            
+            $oCmts = BxDolCmts::getObjectInstance($sSystem, $iObjectId, true);
+            $aCmt = $oCmts->getCommentRow($iCommentId);
+
+            return [bx_api_get_block('entity_author', [
+                'author_data' => BxDolProfile::getData($aCmt['cmt_author_id']),
+                'entry_date' => $aCmt['cmt_time'],
+                'menu_manage' => [],
+                'text' => bx_process_output(strip_tags($oCmts->getObjectTitle($iObjectId))),
+                'url' => bx_api_get_relative_url($oCmts->getBaseUrl())
+            ])];
+        }
+    }
+
+    public function serviceGetInfo($iCmtUniqId, $bSearchableFieldsOnly = true)
+    {
+        $oCmts = BxDolCmts::getObjectInstanceByUniqId($iCmtUniqId);
+        if(!$oCmts)
+            return [];
+
+        $aCmt = BxDolCmtsQuery::getCommentExtendedByUniqId($iCmtUniqId);
+        if(empty($aCmt) || !is_array($aCmt))
+            return [];
+
+        return $oCmts->serviceGetInfo((int)$aCmt['cmt_id'], $bSearchableFieldsOnly);
+    }
+
+    public function serviceGetBlockView($sSystem = '', $iObjectId = 0, $iCommentId = 0)
+    {
+        if (bx_is_api()){
+            $aParams['comment_id'] = bx_process_input(bx_get('cmt_id'), BX_DATA_INT);
+            $aParams['module'] =  bx_process_input(bx_get('sys'));
+            $aParams['object_id'] = bx_process_input(bx_get('id'), BX_DATA_INT);
+
+            $aRv = $this->serviceGetDataApi($aParams);
+            $aRv['browse']['data']['total_count'] = 0;
+            return [$aRv];
+        }
+        
+        if(empty($sSystem) && ($sSystem = bx_get('sys')) !== false)
+            $sSystem = bx_process_input($sSystem);
+
+        if(empty($iObjectId) && ($iObjectId = bx_get('id')) !== false)
+            $iObjectId = bx_process_input($iObjectId, BX_DATA_INT);
+
+        if(empty($iCommentId) && ($iCommentId = bx_get('cmt_id')) !== false)
+            $iCommentId = bx_process_input($iCommentId, BX_DATA_INT);
+
+        $oCmts = BxDolCmts::getObjectInstance($sSystem, $iObjectId, true);
+        if(!$oCmts)
+            return '';
+
+        return $oCmts->getCommentBlock($iCommentId);
+    }
+
+    public function serviceAlertResponseSysCmtsImagesFileDeleted($oAlert)
+    {
+        $iUniqId = (int)$oAlert->aExtras['ghost']['content_id'];
+
+        $aCmt = BxDolCmts::getGlobalInfo($iUniqId);
+        if(empty($aCmt) || !is_array($aCmt))
+            return;
+
+        $oCmts = BxDolCmts::getObjectInstance($aCmt['system_name'], 0, false);
+        if(!$oCmts)
+            return;
+
+        $oCmts->getQueryObject()->deleteImages($aCmt['system_id'], false, $oAlert->iObject);
+    }
+
+    public function serviceGetMenuItemAddonVote($sSystem, $iId, $iCmtId)
+    {
+        $oCmts = BxDolCmts::getObjectInstance($sSystem, $iId);
+
+        $oVote = $oCmts->getVoteObject($oCmts->getCommentUniqId($iCmtId));
+        if($oVote !== false)
+            return $oVote->getCounter();
+
+        return '';
+    }
+
+    public function serviceGetLiveUpdate($sSystem, $iContentId, $iProfileId, $iCount = 0)
+    {
+        $oCmts = BxDolCmts::getObjectInstance($sSystem, $iContentId);
+        if(!$oCmts || !$oCmts->isEnabled())
+            return false;
+
+        $sKey = $oCmts->getNotificationId();
+
+        bx_import('BxDolSession');
+        if((int)BxDolSession::getInstance()->getValue($sKey) == 1)
+            return false;
+
+        $iCountNew = $oCmts->getCommentsCount($iContentId, -1, BX_CMT_FILTER_OTHERS);
+        if($iCountNew == $iCount)
+            return false;
+
+        return array(
+            'count' => $iCountNew, // required
+            'method' => $oCmts->getJsObjectName() . '.showLiveUpdate(oData)', // required
+            'data' => array(
+                'code' => $oCmts->getLiveUpdate($iCount, $iCountNew)
+            ),  // optional, may have some additional data to be passed in JS method provided using 'method' param above.
+        );
+    }
+
+    public function serviceGetLiveUpdates($sSystem, $iContentId, $iProfileId, $iCount = 0)
+    {
+        $oCmts = BxDolCmts::getObjectInstance($sSystem, $iContentId);
+        if(!$oCmts || !$oCmts->isEnabled())
+            return false;
+
+        $sKey = $oCmts->getNotificationId();
+
+        bx_import('BxDolSession');
+        if((int)BxDolSession::getInstance()->getValue($sKey) == 1)
+            return false;
+
+        $iCountNew = $oCmts->getCommentsCount($iContentId, -1, BX_CMT_FILTER_OTHERS);
+        if($iCountNew == $iCount)
+            return false;
+
+        return array(
+            'count' => $iCountNew, // required
+            'method' => $oCmts->getJsObjectName() . '.showLiveUpdates(oData)', // required
+            'data' => array(
+                'code' => $oCmts->getLiveUpdates($iCount, $iCountNew)
+            ),  // optional, may have some additional data to be passed in JS method provided using 'method' param above.
+        );
+    }
+    
+    public function serviceManageTools($sType = 'common')
+    {
+        $oGrid = BxDolGrid::getObjectInstance('sys_cmts_administration');
+        if(!$oGrid)
+            return '';
+
+        $oTemplate = BxDolTemplate::getInstance();
+        $oTemplate->addJs(array('BxDolCmtsManageTools.js'));
+        $oTemplate->addCss(array('cmts_manage_tools.css'));
+        $oTemplate->addJsTranslation(array('_sys_grid_search'));
+        return array(
+            'content' =>  $oGrid->getCode()
+        );
+    }
+
+    public function serviceGetMenuAddonManageTools()
+    {
+        $iNumTotal = BxDolCmts::getGlobalNumByParams();
+
+        $iNum1 = BxDolCmts::getGlobalNumByParams([[
+            'key' => 'status_admin', 
+            'value' => 'pending', 
+            'operator' => '='
+        ]]);
+
+        $iNum2 = BxDolCmts::getGlobalNumByParams([[
+            'key' => 'reports',
+            'value' => '0', 
+            'operator' => '>'
+        ]]);
+
+        return [
+            'counter1_value' => $iNum1, 
+            'counter1_caption' => _t('_sys_menu_dashboard_manage_tools_addon_counter1_caption_profile_default'), 
+            'counter2_value' => $iNum2, 
+            'counter3_value' => $iNumTotal
+        ];
+    }
+
+    /**
+     * Comment Added for Timeline module
+     */
+    public function serviceGetTimelinePost($aEvent, $aBrowseParams = array())
+    {
+        $bCache = true;
+
+        $iCmtUniqId = $aEvent['object_id'];
+        $oCmts = BxDolCmts::getObjectInstanceByUniqId($iCmtUniqId);
+        if(!$oCmts)
+            return false;
+
+        $aCmt = BxDolCmtsQuery::getCommentExtendedByUniqId($iCmtUniqId);
+        if(empty($aCmt) || !is_array($aCmt))
+            return false;
+
+        $iCmtId = (int)$aCmt['cmt_id'];
+
+        $iUserId = bx_get_logged_profile_id();
+        $iAuthorId = (int)$aCmt['cmt_author_id'];
+        $iAuthorIdAbs = abs($iAuthorId);
+        if($iAuthorId < 0 && ((is_numeric($aEvent['owner_id']) && $iAuthorIdAbs == (int)$aEvent['owner_id']) || (is_array($aEvent['owner_id']) && in_array($iAuthorIdAbs, $aEvent['owner_id']))) && $iAuthorIdAbs != $iUserId)
+            return false;
+
+        $sCmtUrl = $oCmts->getViewUrl($iCmtId);
+
+        $sSample = '_cmt_txt_sample_comment_single_with_article';
+        $sSampleWoArticle = '_cmt_txt_sample_comment_single';
+        $sAction = '';
+        $sActionCustom = '_cmt_txt_added_sample_custom';
+        $aActionCustomMarkers = array(
+            'sample' => $sSample,
+            'sample_url' => $sCmtUrl,
+            'psample' => strip_tags($oCmts->getObjectTitle()),
+            'psample_url' => $oCmts->getBaseUrl(),
+        );
+
+        $iOwnerId = $iAuthorIdAbs;
+        if(isset($aEvent['object_privacy_view']) && (int)$aEvent['object_privacy_view'] < 0)
+            $iOwnerId = abs($aEvent['object_privacy_view']);
+
+        //--- Votes
+        $aVotes = array();
+        if(($oVotes = $oCmts->getVoteObject($iCmtUniqId)) !== false)
+            $aVotes = array(
+                'system' => $oVotes->getSystemName(),
+                'object_id' => $iCmtUniqId,
+                'count' => $aCmt['votes']
+            );
+        
+        //--- Reactions
+        $aReactions = array();
+        if(($oReactions = $oCmts->getReactionObject($iCmtUniqId)) !== false)
+            $aReactions = array(
+                'system' => $oReactions->getSystemName(),
+                'object_id' => $iCmtUniqId,
+                'count' => $aCmt['rvotes']
+            );
+
+        //--- Scores
+        $aScores = array();
+        if(($oScores = $oCmts->getScoreObject($iCmtUniqId)) !== false)
+            $aScores = array(
+                'system' => $oScores->getSystemName(),
+                'object_id' => $iCmtUniqId,
+                'score' => $aCmt['score']
+            );
+
+        //--- Reports
+        $aReports = array();
+        if(($oReports = $oCmts->getReportObject($iCmtUniqId)) !== false)
+            $aReports = array(
+                'system' => $oReports->getSystemName(),
+                'object_id' => $iCmtUniqId,
+                'count' => $aCmt['reports']
+            );
+
+        return array(
+            '_cache' => $bCache,
+            'owner_id' => $iOwnerId,
+            'object_owner_id' => $iAuthorId,
+            'icon' => 'comments',
+            'sample' => $sSample,
+            'sample_wo_article' => $sSampleWoArticle,
+            'sample_action' => $sAction,
+            'sample_action_custom' => array(
+                'content' => $sActionCustom,
+                'markers' => $aActionCustomMarkers
+            ),
+            'url' => $sCmtUrl,
+            'content' => array(
+                'sample' => $sSample,
+                'sample_wo_article' => $sSampleWoArticle,
+                'sample_action' => $sAction,
+                'url' => $sCmtUrl,
+                'title' => '',
+                'text' => $oCmts->getViewText($aCmt),
+                'images' => array(),
+                'images_attach' => array(),
+                'videos' => array(),
+                'videos_attach' => array(),
+                'files' => array(),
+                'files_attach' => $oCmts->getAttachments($iCmtId)
+            ), //a string to display or array to parse default template before displaying.
+            'date' => $aCmt['cmt_time'],
+            'views' => array(),
+            'votes' => $aVotes,
+            'reactions' => $aReactions,
+            'scores' => $aScores,
+            'reports' => $aReports,
+            'comments' => array(),
+            'title' => !empty($aCmt['cmt_text']) ? $aCmt['cmt_text'] : '', //may be empty.
+            'description' => '' //may be empty.
+        );
+    }
+
+    /**
+     * Comment added for Notifications module
+     */
+    public function serviceGetNotificationsCommentAdded($aEvent)
+    {
+        $aCommentGi = BxDolCmts::getGlobalInfo($aEvent['subobject_id']);
+        if(empty($aCommentGi) || !is_array($aCommentGi))
+            return [];
+
+        $iContentId = (int)$aEvent['object_id'];
+
+        $oComment = BxDolCmts::getObjectInstance($aCommentGi['system_name'], $iContentId);
+        if(!$oComment)
+            return [];
+
+        $aCommentSystem = $oComment->getSystemInfo();
+        
+        $oContentModule = BxDolModule::getInstance($aCommentSystem['module']);
+        if(!$oContentModule)
+            return [];
+
+        $aContentInfo = $oContentModule->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return [];
+
+        $CNF = &$oContentModule->_oConfig->CNF;
+        if(empty($CNF['FIELD_ID']) || empty($CNF['FIELD_AUTHOR']) || !(isset($CNF['FIELD_TITLE']) || isset($CNF['FIELD_TEXT'])) || empty($CNF['URI_VIEW_ENTRY']))
+            return [];
+
+        $sEntryUrl = '{bx_url_root}' . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+        $sEntryCaption = isset($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : strmaxtextlen($aContentInfo[$CNF['FIELD_TEXT']], 20, '...');
+
+        return array(
+            'entry_sample' => $CNF['T']['txt_sample_single'],
+            'entry_url' => $sEntryUrl,
+            'entry_caption' => $sEntryCaption,
+            'subentry_sample' => $CNF['T']['txt_sample_comment_single'],
+            'subentry_url' => '{bx_url_root}' . $oComment->getViewUrl((int)$aCommentGi['cmt_id'], false),
+            'lang_key' => '', //may be empty or not specified. In this case the default one from Notification module will be used.
+        );
+    }
+
+    /**
+     * Comment vote for Notifications module
+     */
+    public function serviceGetNotificationsVote($aEvent)
+    {
+        $iCmtIdUnique = (int)$aEvent['object_id'];
+
+        $aCmtInfo = BxDolCmts::getGlobalInfo($iCmtIdUnique);
+        if(empty($aCmtInfo) || !is_array($aCmtInfo))
+            return array();
+
+        $oCmts = BxDolCmts::getObjectInstance($aCmtInfo['system_name'], 0, false);
+        if(!$oCmts || !$oCmts->isEnabled())
+            return array();
+
+        $oVote = $oCmts->getVoteObject($iCmtIdUnique);
+        if(!$oVote)
+            return array();
+
+        $iCmtId = (int)$aCmtInfo['cmt_id'];
+        $sCmtUrl = str_replace(BX_DOL_URL_ROOT, '{bx_url_root}', $oCmts->serviceGetLink($iCmtId));
+        $sCmtCaption = strmaxtextlen($oCmts->serviceGetText($iCmtId), 20, '...');
+
+        return array(
+            'entry_sample' => $oCmts->getLanguageKey('txt_sample_single'),
+            'entry_url' => $sCmtUrl,
+            'entry_caption' => $sCmtCaption,
+            'entry_author' => $aCmtInfo['author_id'],
+            'subentry_sample' => $oCmts->getLanguageKey('txt_sample_vote_single'),
+            'lang_key' => '', //may be empty or not specified. In this case the default one from Notification module will be used.
+        );
+    }
+
+    /**
+     * Comment reaction for Notifications module
+     */
+    public function serviceGetNotificationsReaction($aEvent)
+    {
+    	$iCmtIdUnique = (int)$aEvent['object_id'];
+
+        $aCmtInfo = BxDolCmts::getGlobalInfo($iCmtIdUnique);
+        if(empty($aCmtInfo) || !is_array($aCmtInfo))
+            return array();
+
+        $oCmts = BxDolCmts::getObjectInstance($aCmtInfo['system_name'], 0, false);
+        if(!$oCmts || !$oCmts->isEnabled())
+            return array();
+
+        $oReaction = $oCmts->getReactionObject($iCmtIdUnique);
+        if(!$oReaction)
+            return array();
+        
+        $aSubentry = $oReaction->getTrackBy(array('type' => 'id', 'id' => (int)$aEvent['subobject_id']));
+        if(empty($aSubentry) || !is_array($aSubentry))
+            return array();
+
+        $aSubentrySampleParams = array();
+        $aReaction = $oReaction->getReaction($aSubentry['reaction']);
+        if(!empty($aReaction['title']))
+            $aSubentrySampleParams[] = $aReaction['title'];
+        else
+            $aSubentrySampleParams[] = '_undefined';
+
+        $iCmtId = (int)$aCmtInfo['cmt_id'];
+        $sCmtUrl = str_replace(BX_DOL_URL_ROOT, '{bx_url_root}', $oCmts->serviceGetLink($iCmtId));
+        $sCmtCaption = strmaxtextlen($oCmts->serviceGetText($iCmtId), 20, '...');
+
+        return array(
+            'entry_sample' => $oCmts->getLanguageKey('txt_sample_single'),
+            'entry_url' => $sCmtUrl,
+            'entry_caption' => $sCmtCaption,
+            'entry_author' => $aCmtInfo['author_id'],
+            'subentry_sample' => $oCmts->getLanguageKey('txt_sample_reaction_single'),
+            'subentry_sample_params' => $aSubentrySampleParams,
+            'lang_key' => '', //may be empty or not specified. In this case the default one from Notification module will be used.
+        );
+    }
+
+    /**
+     * Comment score -> vote up for Notifications module
+     */
+    public function serviceGetNotificationsScoreUp($aEvent)
+    {
+    	return $this->_serviceGetNotificationsScore('up', $aEvent);
+    }
+
+    /**
+     * Comment score -> vote down for Notifications module
+     */
+    public function serviceGetNotificationsScoreDown($aEvent)
+    {
+    	return $this->_serviceGetNotificationsScore('down', $aEvent);
+    }
+
+    protected function _serviceGetNotificationsScore($sType, $aEvent)
+    {
+        $iCmtIdUnique = (int)$aEvent['object_id'];
+
+        $aCmtInfo = BxDolCmts::getGlobalInfo($iCmtIdUnique);
+        if(empty($aCmtInfo) || !is_array($aCmtInfo))
+            return array();
+
+        $oCmts = BxDolCmts::getObjectInstance($aCmtInfo['system_name'], 0, false);
+        if(!$oCmts || !$oCmts->isEnabled())
+            return array();
+
+        $oScore = $oCmts->getScoreObject($iCmtIdUnique);
+        if(!$oScore)
+            return array();
+
+        $iCmtId = (int)$aCmtInfo['cmt_id'];
+        $sCmtUrl = str_replace(BX_DOL_URL_ROOT, '{bx_url_root}', $oCmts->serviceGetLink($iCmtId));
+        $sCmtCaption = strmaxtextlen($oCmts->serviceGetText($iCmtId), 20, '...');
+
+        return array(
+            'entry_sample' => $oCmts->getLanguageKey('txt_sample_single'),
+            'entry_url' => $sCmtUrl,
+            'entry_caption' => $sCmtCaption,
+            'entry_author' => $aCmtInfo['author_id'],
+            'subentry_sample' => $oCmts->getLanguageKey('txt_sample_score_' . $sType . '_single'),
+            'lang_key' => '', //may be empty or not specified. In this case the default one from Notification module will be used.
+        );
+    }
+    
+    /**
+     * @page service Service Calls
+     * @section bx_system_cmts System Services 
+     * @subsection bx_system_cmts-general General
+     * @subsubsection bx_system_cmts-get_data_api get_data_api
+     * 
+     * @code bx_srv('system', 'get_data_api', [], 'TemplCmtsServices'); @endcode
+     * 
+     * Get comments data
+     * @param $aParams array with paramenters :
+     *         "module":"bx_posts","object_id":3,"start_from":5,"order_way":"desc","is_form":false
+     * 
+     * @see TemplCmtsServices::serviceGetDataApi
+     */
+    /** 
+     * @ref bx_system_cmts-get_data_api "get_data_api"
+     * @api @ref bx_system_cmts-get_data_api "get_data_api"
+     */
+    
+    public function serviceGetDataApi($aParams)
+    {
+        if(is_string($aParams))
+            $aParams = json_decode($aParams, true);
+        
+        if (isset($aParams['action']) && isset($aParams['id']) && $aParams['action'] == 'remove'){
+            $iCmtId = $aParams['id'];
+            $oCmts = BxDolCmts::getObjectInstance($aParams['module'], $aParams['object_id']);
+            $oCmts->remove($iCmtId);
+            return 'Ok';
+        }
+        
+        if (isset($aParams['action']) && isset($aParams['id']) && $aParams['action'] == 'edit'){
+            $iCmtId = $aParams['id'];
+            $oCmts = BxDolCmts::getObjectInstance($aParams['module'], $aParams['object_id']);
+            $oForm = $oCmts->getFormEdit($iCmtId);
+            if($oForm['form']->isSubmittedAndValid()){
+                $aParams['comment_id'] = $oForm['res'];  
+                $aRv['browse'] = $this->serviceGetCommentsApi($oCmts, $aParams);
+                return $aRv;
+            }
+            $aForm = $oForm['form']->getCodeAPI();
+            $aForm['inputs']['cmt_text']['numLines'] = 1;
+            $aForm['inputs']['cmt_text']['autoheight'] = true;
+            $aForm['inputs']['cmt_submit'] = $aForm['inputs']['cmt_controls'][0];
+            unset($aForm['inputs']['cmt_controls']);
+            
+            $aForm['inputs']['cmt_submit']['icon'] = 'contact';
+            $aForm['inputs']['cmt_submit']['variant'] = 'text';
+            $aForm['inputs']['cmt_submit']['icon_only'] = true;
+            $aRv['form'] = ['id' => 'cmt_form', 'type' => 'form', 'name' => 'comment', 'data' => $aForm, 'request' => ['immutable' => true]];
+            return $aRv;
+        }
+        
+        
+        $aParams['parent_id'] = !isset($aParams['parent_id']) ? 0 : $aParams['parent_id'];
+        $aParams['start_from'] = !isset($aParams['start_from']) ? 0 : $aParams['start_from'];
+        $aParams['order_way'] = !isset($aParams['order_way']) ? 'asc' : $aParams['order_way'];
+        $aParams['is_form'] = !isset($aParams['is_form']) ? true : $aParams['is_form'];
+        $aParams['insert'] = 'before';
+        
+        $oCmts = BxDolCmts::getObjectInstance($aParams['module'], $aParams['object_id']);
+        
+        if (!$oCmts || !$oCmts->isEnabled())
+            return false;
+        
+        $aRv = [
+            'id' => '1',
+            'url' => '/api.php?r=system/get_data_api/TemplCmtsServices/&params[]=',
+            'type' => 'comments'
+        ];
+        
+        $oForm = $oCmts->getFormPost($aParams['parent_id']);
+        $bIsList =  false;
+        if (isset($oForm['form']) && $aParams['is_form']){
+            $aForm = $oForm['form']->getCodeAPI();
+            $aForm['inputs']['cmt_text']['numLines'] = 1;
+            $aForm['inputs']['cmt_text']['autoheight'] = true;
+            $aForm['inputs']['cmt_submit']['icon'] = 'contact';
+            $aForm['inputs']['cmt_submit']['variant'] = 'text';
+            $aForm['inputs']['cmt_submit']['icon_only'] = true;
+            
+            // add view (form + new comment)
+            if($oForm['form']->isSubmittedAndValid()){
+               $aParams['insert'] = $aParams['order_way'] == 'desc' ? 'after' : 'before';
+                $aParams['comment_id'] = $oForm['res'];
+                $bIsList = true;
+                $aForm['inputs']['cmt_parent_id']['value'] = 0;
+            }
+            
+            $aRv['form'] = ['id' => 'cmt_form', 'type' => 'form', 'name' => 'comment', 'data' => $aForm, 'request' => ['immutable' => true]];
+
+            // default view (form + list)
+            if (!$oForm['form']->isSubmitted()){
+                $bIsList = true;
+            }
+        }
+        else{
+            // list only view
+            $bIsList = true;                  
+            $aParams['insert'] = 'before';
+        }
+        if ($bIsList)
+            $aRv['browse'] = $this->serviceGetCommentsApi($oCmts, $aParams);
+
+        return $aRv;
+    }
+    
+    public function serviceGetCommentsApi($oCmts, $aParams)
+    {
+        $mixedResult = $oCmts->isViewAllowed();
+        if($mixedResult !== CHECK_ACTION_RESULT_ALLOWED)
+            return $mixedResult; // TODO: error checking
+
+        if (isset($aParams['comment_id']))
+            $aParams['comment_id'] = explode(',', $aParams['comment_id']);
+       
+        $aBp = !isset($aParams['aBp']) ? [] : $aParams['aBp'];
+        $aDp = ['in_designbox' => false, 'show_empty' => false];
+        
+        $aDp['type'] = !getParam('sys_api_comments_flat') ? 'threaded' : 'flat';
+        $aBp['type'] = 'head';
+        $oCmts->getParams($aBp, $aDp);
+        $oCmts->prepareParams($aBp, $aDp);
+        
+        $aBp['order']['way'] = $aParams['order_way'];
+        $aBp['order_way'] = $aParams['order_way'];
+        $aBp['start'] = $aParams['start_from'] ; 
+        $aPp = $aBp['per_view'];
+        $aBp['per_view'] =  $aBp['per_view'] + 1; 
+        if (isset($aParams['per_view'])){
+             $aBp['per_view'] = $aParams['per_view'];
+        }
+
+        $aCmts = isset($aParams['comment_id']) ? array_map(function($value) {return ['cmt_id' => $value];}, $aParams['comment_id']) : $oCmts->getCommentsArray($aBp['vparent_id'], $aBp['filter'], $aBp['order'], $aBp['start'], $aBp[($aBp['init_view'] != -1 ? 'init' : 'per') . '_view']);
+
+        
+        $aParams['start_from'] = 0;
+        if (count($aCmts) == $aBp['per_view']){
+            $aBp['per_view'] = $aPp;
+            $aCmts = array_slice($aCmts, 0, $aBp['per_view']); 
+            $aParams['start_from'] = $aBp['start'] + $aBp['per_view'];
+        }
+        $aCmtsRv = [];
+        foreach ($aCmts as $aCmt) {
+            $oCmt = $oCmts->getCommentStructure($aCmt['cmt_id'], $aBp, $aDp);
+            $sKey = array_keys($oCmt)[0];
+            if ($oCmt[$sKey]['data']['cmt_parent_id'] > 0){
+                $aParent = $oCmts->getCommentSimple((int)$oCmt[$sKey]['data']['cmt_parent_id']);
+                $oCmt[$sKey]['data']['cmt_parent'] = [
+                    'data' => $aParent,
+                    'author_data' => BxDolProfile::getData($aParent['cmt_author_id'])
+                ];
+            }
+            $aCmtsRv[] = $oCmt;
+        }
+        
+        $aData = [
+            'unit' => 'comments',
+            'start' => $aParams['start_from'],
+            'count' => count($aCmts),
+            'per_view' => $aBp['per_view'],
+            'total_count' => $oCmts->getCommentsCount(),
+            'order' => $aParams['order_way'],
+            'view' => $aDp['type'],
+            'module' => $oCmts->getSystemName(), 
+            'object_id' => $oCmts->getId(),
+            'max_level' => $oCmts->getSystemInfo()['number_of_levels'],
+            'data' => $aCmtsRv,
+        ];
+        if (isset($aParams['mode']) &&  $aParams['mode'] == 'feed')
+            return $aData;
+        
+        $aRv = [
+            'id' => 'cmt_list', 
+            'type' => 'browse', 
+            'insert' => $aParams['insert'], 
+            'data' => $aData
+        ];
+        
+        if (isset($aParams['comment_id'])){
+            $aRv['new'] = $aParams['comment_id'];
+        }
+       
+        return $aRv;
+    }
+}
+
+/** @} */

+ 2492 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBaseFormView.php

@@ -0,0 +1,2492 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+class BxBaseFormView extends BxDolForm
+{
+    protected static $_isToggleJsAdded = false;
+
+    protected static $_isCssJsAdded = false;
+    protected static $_isCssJsUiAdded = false;
+    protected static $_isCssJsUiSortableAdded = false;
+    protected static $_isCssJsMinicolorsAdded = false;
+    protected static $_isCssJsLabelsAdded = false;
+    protected static $_isCssJsTimepickerAdded = false;
+    protected static $_isCssJsAddedViewMode = false;
+
+    /**
+     * Enable or disable error message displaying
+     */
+    protected $bEnableErrorIcon = true;
+
+    /**
+     * HTML Code of this form
+     */
+    protected $sCode;
+
+    /**
+     * Code which will be added to the beginning of the form.
+     * For example, hidden inputs.
+     * For internal use only
+     */
+    protected $_sCodeAdd = '';
+
+    /**
+     * for internal use only
+     */
+    protected $_isSectionOpened = false;
+
+    /**
+     * Default divider for several inputs
+     */
+    protected $_sDivider = '<span class="bx-def-margin-left"></span>';
+
+    /**
+     * Alternative divider for several inputs
+     */
+    protected $_sDividerAlt = '<br />';
+
+    /**
+     * Form is added dynamically.
+     */
+    protected $_bDynamicMode = false;
+
+    /**
+     * Form is submitted dynamically (using Ajax Submit).
+     */
+    protected $_bAjaxMode = false;
+    
+    /**
+     * Use absolute Action URL which is needed in Ajax Mode.
+     */
+    protected $_bAbsoluteActionUrl = false;
+
+    /**
+     * Form is displayed in view mode.
+     */
+    protected $_bViewMode = false;
+
+    /**
+     * Show or not sections which have no fileds
+     */
+    protected $_bShowEmptySections = false;
+
+    /**
+     * Function name for generation close form section HTML.
+     */
+    protected $_sSectionClose = 'getCloseSection';
+
+    /**
+     * Function name for generation open form section HTML.
+     */
+    protected $_sSectionOpen = 'getOpenSection';
+
+    /**
+     * JS files list for form
+     */
+    protected $_aJs = array();
+
+    /**
+     * CSS files list for form
+     */
+    protected $_aCss = array();
+
+    protected $_sJsClassName;
+    protected $_sJsObjectName;
+    
+    protected $_aHtmlIds;
+    
+    /**
+     * Constructor
+     *
+     * @param array $aInfo Form contents
+     *
+     * $aInfo['params'] = array(
+     *     'remove_form' => true|false,
+     * );
+     *
+     * @return BxBaseFormView
+     */
+    function __construct($aInfo, $oTemplate)
+    {
+        parent::__construct($aInfo, $oTemplate);
+
+        $this->_bAjaxMode = isset($this->aParams['ajax_mode']) && $this->aParams['ajax_mode'];
+        $this->_bViewMode = isset($this->aParams['view_mode']) && $this->aParams['view_mode'];
+
+        if($this->_bViewMode) {
+            $this->_sSectionClose = 'getCloseSectionViewMode';
+            $this->_sSectionOpen = 'getOpenSectionViewMode';
+        }
+
+        $this->_sJsClassName = 'BxDolForm';
+
+        $sName = !empty($aInfo['params']['display']) ? $aInfo['params']['display'] : '';
+        if(empty($sName) && !empty($aInfo['params']['object']))
+            $sName = $aInfo['params']['object'];
+
+        $this->_sJsObjectName = 'oForm' . bx_gen_method_name($sName, array('_' , '-'));
+
+        $sHtmlId = str_replace(array('_' , ' '), array('-', '-'), $sName);
+        $this->_aHtmlIds = array(
+            'help_popup' => $sHtmlId . '-help-popup-',
+            'pgc' =>  $sHtmlId . '-pgc-',
+            'pgc_popup' => $sHtmlId . '-pgc-popup-',
+            'pgc_form' => $sHtmlId . '-pgc-form-',
+        );
+    }
+
+    public function performActionGetHelp()
+    {
+        $sInput = bx_process_input(bx_get('input'));
+        if(empty($sInput) || empty($this->aInputs[$sInput]['help']))
+            return;
+
+        echo $this->oTemplate->parseHtmlByName('form_field_help_popup.html', array(
+            'content' => _t($this->aInputs[$sInput]['help']),
+            'bx_if:show_edit' => array(
+                'condition' => isAdmin(),
+                'content' => array(
+                    'link' => BX_DOL_URL_STUDIO . bx_append_url_params('builder_forms.php', array(
+                        'page' => 'fields',
+                        'module' => $this->aParams['module'],
+                        'object' => $this->aParams['object'],
+                        'display' => $this->aParams['display']
+                    ))
+                )
+            )
+        ));
+    }
+
+    public function performActionChangePrivacyGroup()
+    {
+        $iAuthorId = (int)bx_get_logged_profile_id();
+        if(empty($iAuthorId))
+            return echoJson(array());
+
+        $iInputId = bx_process_input(bx_get('input_id'), BX_DATA_INT);
+        if(empty($iInputId))
+            return echoJson(array());
+
+        $sPrivacyObject = bx_process_input(bx_get('privacy_object'));
+        $sPrivacyField = BxDolPrivacy::getFieldName($sPrivacyObject);
+        if(empty($sPrivacyField))
+            return echoJson(array());
+
+        $sPrivacyGroup = bx_process_input(bx_get($sPrivacyField));
+        if(empty($sPrivacyField))
+            return echoJson(array());
+
+        if(!BxDolFormQuery::setInputPrivacy($iInputId, $iAuthorId, $sPrivacyField, $sPrivacyGroup))
+            return echoJson(array());
+
+        return echoJson(array(
+            'code' => 0,
+            'form_id' => $this->getId(),
+            'chooser_id' => $this->_aHtmlIds['pgc'] . $iInputId,
+            'icon' => $this->_getPrivacyIcon($sPrivacyGroup),
+            'eval' => $this->getJsObjectName() . '.pgcOnSelectGroup(oData);'
+        ));
+    }
+
+    public function performActionGetPrivacyGroupChooser()
+    {
+        $iInputId = bx_process_input(bx_get('input_id'), BX_DATA_INT);
+        $sPrivacyObject = bx_process_input(bx_get('privacy_object'));
+        if(empty($iInputId) || empty($sPrivacyObject))
+            return '';
+
+        $sFormId = $this->_aHtmlIds['pgc_form'] . $iInputId;
+        $aFormActionParams = array();
+        if(!empty($this->aParams['object']) && !empty($this->aParams['display']))
+            $aFormActionParams = array(
+                'o' => $this->aParams['object'], 
+                'd' => $this->aParams['display']
+            );
+        $aFormActionParams['a'] = 'change_privacy_group';
+
+        $iAuthorId = bx_get_logged_profile_id();
+        $mixedPrivacyGroup = $this->_getPrivacyGroup($sPrivacyObject, $iInputId, $iAuthorId);
+
+        $oPrivacy = BxDolPrivacy::getObjectInstance($sPrivacyObject);
+        $aInputPgc = $oPrivacy->getGroupChooser($sPrivacyObject, $iAuthorId);
+        $aInputPgc['value'] = $mixedPrivacyGroup;
+
+        if(!isset($aInputPgc['content']))
+            $aInputPgc['content'] = '';
+        $aInputPgc['content'] .= $oPrivacy->addCssJs(true);
+        $aInputPgc['content'] .= $oPrivacy->initGroupChooser($sPrivacyObject, $iAuthorId, array(
+            'content_id' => $iInputId,
+            'group_id' => $mixedPrivacyGroup,
+            'html_ids' => array(
+                'form' => $sFormId
+            )
+        ));
+        
+        $aFormInfo = array(
+            'form_attrs' => array(
+                'id' => $sFormId,
+                'action' => BX_DOL_URL_ROOT . bx_append_url_params('form.php', $aFormActionParams),
+                'method' => 'post'
+            ),
+            'inputs' => array (
+                'input_id' => array(
+                    'type' => 'hidden',
+                    'name' => 'input_id',
+                    'value' => $iInputId,
+                    'db' => array (
+                        'pass' => 'Int',
+                    ),
+                ),
+                'privacy_object' => array(
+                    'type' => 'hidden',
+                    'name' => 'privacy_object',
+                    'value' => $sPrivacyObject,
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+                'privacy_group_chooser' => $aInputPgc,
+                'controls' => array(
+                    'name' => 'controls',
+                    'type' => 'input_set',
+                    array(
+                        'type' => 'button',
+                        'name' => 'do_submit',
+                        'value' => _t('_sys_submit'),
+                        'attrs' => array(
+                            'onclick' => "$('#" . $sFormId . "').submit()",
+                        ),
+                    ),
+                    array (
+                        'type' => 'reset',
+                        'name' => 'do_cancel',
+                        'value' => _t('_Cancel'),
+                        'attrs' => array(
+                            'onclick' => "$(this).parents('.bx-popup-applied:visible:first').dolPopupHide()",
+                            'class' => 'bx-def-margin-sec-left',
+                        ),
+                    )
+                )
+            )
+        );
+        $oForm = new BxTemplFormView($aFormInfo);
+
+        echo $this->oTemplate->parseHtmlByName('form_field_privacy_popup.html', array(
+            'form_id' => $oForm->getId(),
+            'form' => $oForm->getCode(true),
+        ));
+    }
+
+    function setShowEmptySections($b)
+    {
+        $this->_bShowEmptySections = $b;
+    }
+
+    function setAjaxMode($bAjaxMode)
+    {
+        $this->_bAjaxMode = (bool)$bAjaxMode;
+    }
+
+    function isAjaxMode()
+    {
+        return $this->_bAjaxMode;
+    }
+
+    function setAbsoluteActionUrl($sUrl)
+    {
+        if(empty($sUrl))
+            return;
+
+        $this->aFormAttrs['action'] = $sUrl;
+
+        $this->_bAbsoluteActionUrl = true;
+    }
+
+    /**
+     * Return Form code
+     * @param $bDynamicMode - set it to true if form is added via JS/AJAX call, for example form in AJAX popup.
+     * @return string
+     */
+    function getCode($bDynamicMode = false)
+    {
+        if(!$bDynamicMode && bx_is_dynamic_request())
+            $bDynamicMode = true;
+
+        $this->_bDynamicMode = $bDynamicMode;
+        $this->aFormAttrs = $this->_replaceMarkers($this->aFormAttrs);
+
+        $sInclude = '';
+        $this->sCode = false;
+        bx_alert('system', 'form_output', 0, 0, array(
+            'dynamic' => $this->_bDynamicMode,
+            'object' => &$this,
+            'code' => &$this->sCode,
+            'include' => &$sInclude
+        ));
+
+        if($this->sCode === false)
+            $this->sCode = $this->genForm();
+
+        $this->addCssJs();
+        $sDynamicCssJs = $this->_processCssJs();
+        return $sInclude . $sDynamicCssJs . $this->sCode;
+    }
+
+    function getCodeAPI()
+    {
+        $this->aFormAttrs = $this->_replaceMarkers($this->aFormAttrs);
+    
+        
+        $this->sCode = false;
+        bx_alert('system', 'form_output', 0, 0, array(
+            'dynamic' => $this->_bDynamicMode,
+            'object' => &$this,
+            'code' => &$this->sCode,
+            'include' => &$sInclude
+        ));
+
+        if($this->sCode === false)
+            $this->genForm();
+        
+        // TODO: process inputs to translate titles, alerts, etc
+        $keysToRemove = [];
+        
+        foreach ($this->aInputs as &$aInput) {
+            if (!isset($aInput['visible_for_levels']) || self::isVisible($aInput)) {
+                if (isset($aInput['type']) && 'files' == $aInput['type']){
+                    $oStorage = BxDolStorage::getObjectInstance($aInput['storage_object']);
+                    $aInput['ext_allow'] = $oStorage->getObjectData()['ext_allow'];
+                    $aInput['ext_deny'] = $oStorage->getObjectData()['ext_deny'];
+                    $aInput['ghost_template'] = '';
+                    $aInput['value'] = '';
+                    $aInput['values'] = '';
+                    $aInput['values_src'] = '';
+                }
+                
+                if (isset($aInput['type']) && 'block_header' == $aInput['type']){
+                    $aInput['name'] = $key;
+                }
+                
+                if (isset($aInput['type']) && 'block_header' == $aInput['type']){
+                    $aInput['name'] = $key;
+                }
+
+                if (isset($aInput['type']) && 'location' == $aInput['type']){
+                    $aLocationIndexes = BxDolForm::$LOCATION_INDEXES;
+                    $aVars = [];
+                    $o = BxDolLocationField::getObjectInstance(getParam('sys_location_field_default'));
+                    foreach ($aLocationIndexes as $sKey)
+                        $aVars[$sKey] = $o->getLocationVal($aInput, $sKey, $this);
+                    if ($aVars['country']) {
+                        $aCountries = BxDolFormQuery::getDataItems('Country');
+                        $sLocationString = ($aVars['street_number'] ? $aVars['street_number'] . ', ' : '') . ($aVars['street'] ? $aVars['street'] . ', ' : '') . ($aVars['city'] ? $aVars['city'] . ', ' : '') . ($aVars['state'] ? $aVars['state'] . ', ' : '') . $aCountries[$aVars['country']];
+                        $aVars['location_string'] = $sLocationString;
+                    }
+                    $aInput['value'] = $aVars;
+                }
+
+                if (isset($aInput['type']) && 'textarea' == $aInput['type']){
+                    if (isset($aInput['value'], $aInput['html']) && (int)$aInput['html'] == 0)
+                        $aInput['value'] = strip_tags($aInput['value']);
+                }
+            }
+            else{
+                $keysToRemove[] = $key;
+            }
+        }
+        
+        foreach ($keysToRemove as $key) {
+            unset($this->aInputs[$key]);
+        }
+    
+        return ['inputs' => $this->aInputs, 'attrs' => $this->aFormAttrs, 'params' => $this->aParams];
+    }
+
+    public function getJsClassName()
+    {
+        return $this->_sJsClassName;
+    }
+
+    public function getJsObjectName()
+    {
+        return $this->_sJsObjectName;
+    }
+
+    public function getJsScript($bWrap = false)
+    {
+        $sJsObjName = $this->getJsObjectName();
+        $sJsObjClass = $this->getJsClassName();
+
+        $sCode = "if(window['" . $sJsObjName . "'] == undefined) window['" . $sJsObjName . "'] = new " . $sJsObjClass . "(" . json_encode(array(
+            'sObjName' => $sJsObjName,
+            'sName' => $this->getName(),
+            'sObject' => isset($this->aParams['object']) ? $this->aParams['object'] : '',
+            'sDisplay' => isset($this->aParams['display']) ? $this->aParams['display'] : '',
+            'sRootUrl' => BX_DOL_URL_ROOT,
+            'aHtmlIds' => $this->_aHtmlIds,
+            'bLeavePageConfirmation' => getParam('sys_form_lpc_enable') == 'on',
+            'sTxtLeavePageConfirmation' => _t('_sys_leave_page_confirmation')
+        )) . ");";
+
+        return $bWrap ? $this->oTemplate->_wrapInTagJsCode($sCode) : $sCode;
+    }
+
+    /**
+     * Generate the whole form
+     *
+     * @return string
+     */
+    function genForm()
+    {
+        $this->_sCodeAdd = '';
+
+        $sTable = $this->genRows();
+
+        $sHtmlBefore = isset($this->aParams['html_before']) ? $this->aParams['html_before'] : '';
+        $sHtmlAfter = isset($this->aParams['html_after']) ? $this->aParams['html_after'] : '';
+
+        if (!empty($this->aParams['remove_form']) || (isset($this->aParams['view_mode']) && $this->aParams['view_mode'])) {
+            $sForm = <<<BLAH
+                    $sHtmlBefore
+                    {$this->_sCodeAdd}
+                    <div class="bx-form-advanced-wrapper {$this->id}_wrapper">
+                        $sTable
+                    </div>
+                    $sHtmlAfter
+BLAH;
+        } else {
+            $sFormAttrs = bx_convert_array2attrs($this->aFormAttrs, 'bx-form-advanced');
+
+            $sAjaxFormJs = '';
+            if($this->_bAjaxMode)
+            	$sAjaxFormJs = <<<BLAH
+	            	$("form#{$this->id}").ajaxForm({ 
+			            dataType: "json",
+			            beforeSubmit: function (formData, jqForm, options) {
+			                bx_loading($("form#{$this->id}"), true);
+			            },
+			            success: function (oData) {
+			            	bx_loading($("form#{$this->id}"), false);
+			
+			                processJsonData(oData);
+			            }
+			        });
+BLAH;
+
+            $sJsCodeDynamic = '';
+            if($this->_bDynamicMode)
+                $sJsCodeDynamic = 'function() {' . $this->getJsScript() . '}';
+
+            $sForm = <<<BLAH
+                $sHtmlBefore
+                <form $sFormAttrs>
+                    {$this->_sCodeAdd}
+                    <div class="bx-form-advanced-wrapper {$this->id}_wrapper">
+                        $sTable
+                    </div>
+                </form>
+                <script>
+                    $(document).ready(function() {
+                        $(this).addWebForms($sJsCodeDynamic);
+                    });
+                    $sAjaxFormJs
+                </script>
+                $sHtmlAfter
+BLAH;
+        }
+
+        if(!$this->_bDynamicMode)
+            $sForm = $sForm . $this->getJsScript(true);
+
+        return $sForm;
+    }
+
+    /**
+     * Generate Table HTML code
+     *
+     * @return string
+     */
+    function genRows()
+    {
+        // add CSRF token if it's needed.
+        if (!(isset($this->aParams['view_mode']) && $this->aParams['view_mode']) && getParam('sys_security_form_token_enable') == 'on' && (!isset($this->aParams['csrf']['disable']) || (isset($this->aParams['csrf']['disable']) && $this->aParams['csrf']['disable'] !== true)) && ($mixedCsrfToken = BxDolForm::getCsrfToken()) !== false) {
+            $this->aInputs['csrf_token'] = array(
+                'type' => 'hidden',
+                'name' => 'csrf_token',
+                'value' => $mixedCsrfToken,
+                'db' => array ('pass' => 'Xss'),
+                'visible_for_levels' => PHP_INT_MAX,
+            );
+        }
+
+        // add 'Ajax Mode' flag
+        if($this->_bAjaxMode)
+            $this->aInputs['ajax_mode'] = array(
+                'type' => 'hidden',
+                'name' => 'ajax_mode',
+                'value' => 1,
+                'db' => array ('pass' => 'Int'),
+                'visible_for_levels' => PHP_INT_MAX,
+            );
+
+        // add 'Absolute Action Url' flag
+        if($this->_bAbsoluteActionUrl)
+            $this->aInputs['absolute_action_url'] = array(
+                'type' => 'hidden',
+                'name' => 'absolute_action_url',
+                'value' => 1,
+                'db' => array ('pass' => 'Int'),
+                'visible_for_levels' => PHP_INT_MAX,
+            );
+
+        // check if we need to generate open section clause
+        $sOpenSection = '';
+        foreach ($this->aInputs as $aInput) {
+            if (isset($aInput['type']) && 'hidden' == $aInput['type'])
+                continue;
+            if (isset($aInput['type']) && 'block_header' != $aInput['type'])
+                $sOpenSection = $this->{$this->_sSectionOpen}();
+            break;
+        }
+
+        // generate rows contents
+        $sCont = '';
+        $sContHeader = '';
+        $sContFields = '';
+        $sFuncGenRow = isset($this->aParams['view_mode']) && $this->aParams['view_mode'] ? 'genViewRow' : 'genRow';
+        foreach ($this->aInputs as &$aInput) {
+    
+            if (!isset($aInput['visible_for_levels']) || self::isVisible($aInput)) {
+                if ('block_header' == $aInput['type'] && !$this->_bShowEmptySections) {
+                    // don't show section with no fields or with empty fields
+                    if ($sContHeader) {                        
+                        if ($sContFields)
+                            $sCont .= $sContHeader . $sContFields;
+                        else
+                            $sContHeader = '';
+                        $sContFields = '';
+                    } 
+                    else {
+                        $sCont .= $sContFields;
+                        $sContFields = '';
+                    }
+                    $sContHeader = $this->$sFuncGenRow($aInput);
+                } else {
+                    $sContFields .= $this->$sFuncGenRow($aInput);
+                }
+            }
+        }
+        $sCont .= $sContHeader . $sContFields;
+
+        $sCloseSection = $this->{$this->_sSectionClose}();
+
+        return $sOpenSection . $sCont . $sCloseSection;
+    }
+
+    /**
+     * Generate single Table Row
+     *
+     * @param  array  $aInput
+     * @return string
+     */
+    function genRow(&$aInput)
+    {
+        if (!isset($aInput['type']))
+            $aInput['type'] = false;
+
+        if (!empty($aInput['name'])) {
+            $sCustomMethod = 'genCustomRow' . $this->_genMethodName($aInput['name']);
+            if (method_exists($this, $sCustomMethod))
+                return $this->$sCustomMethod($aInput);
+        }
+        
+        switch ($aInput['type']) {
+
+            case 'block_header':
+                $sRow = $this->genRowBlockHeader($aInput);
+            break;
+
+            case 'block_end':
+                $sRow = $this->genBlockEnd($aInput);
+            break;
+
+            case 'hidden':
+                // do not generate row for hidden inputs
+                $sRow = '';
+                $this->_sCodeAdd .= $this->genInput($aInput);
+            break;
+
+            case 'select_box':
+                $sRow = $this->genRowCustom($aInput, 'genInputSelectBox');
+            break;
+
+            case 'files':
+                $sRow = $this->genRowCustom($aInput, 'genInputFiles');
+            break;
+
+            case 'switcher':
+            case 'checkbox':
+                $sRow = $this->genRowStandard($aInput, true);
+            break;
+            
+            default:
+                $sRow = $this->genRowStandard($aInput);
+        }
+
+        return $sRow;
+    }
+
+    public function isInputVisible($mixedInput)
+    {
+        if(!is_array($mixedInput) && isset($this->aInputs[$mixedInput]))
+            $mixedInput = $this->aInputs[$mixedInput];
+
+        if(empty($mixedInput) || !is_array($mixedInput))
+            $mixedInput = BxDolFormQuery::getInputByName($this->aParams['object'], $mixedInput);
+
+        if(empty($mixedInput) || !is_array($mixedInput))
+            return false;
+
+        if(!empty($mixedInput['privacy']) && !empty($this->_iAuthorId) && !$this->_isInputVisibleByPrivacy($mixedInput))
+            return false;
+
+        return true;
+    }
+
+    protected function _isInputVisibleByPrivacy($aInput)
+    {
+        $mixedResult = checkActionModule($this->_iAuthorId, 'set form fields privacy', 'system');
+        if($mixedResult[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return true;            
+
+        $aInputPrivacy = BxDolFormQuery::getInputPrivacy($aInput['id'], $this->_iAuthorId);
+        if(empty($aInputPrivacy) || !is_array($aInputPrivacy))
+            if(BxDolFormQuery::setInputPrivacy($aInput['id'], $this->_iAuthorId, BxDolPrivacy::getFieldName($this->_sPrivacyObjectView), $this->_sPrivacyGroupDefault))
+                $aInputPrivacy = BxDolFormQuery::getInputPrivacy($aInput['id'], $this->_iAuthorId);
+
+        if((empty($aInputPrivacy) || !is_array($aInputPrivacy)) && $this->_sPrivacyGroupDefault != BX_DOL_PG_ALL)
+            return false;
+
+        $oPrivacy = BxDolPrivacy::getObjectInstance($this->_sPrivacyObjectView);            
+        if($oPrivacy && !$oPrivacy->check($aInputPrivacy['id']))
+            return false;
+
+        return true;
+    }
+
+    /**
+     * Generate single Table Row for view mode
+     *
+     * @param  array  $aInput
+     * @return string
+     */
+    function genViewRow(&$aInput)
+    {
+        if (!isset($aInput['type']))
+            $aInput['type'] = false;
+
+        if(!$this->isInputVisible($aInput))
+            return '';
+
+        if (!empty($aInput['name'])) {
+            $sCustomMethod = 'genCustomRow' . $this->_genMethodName($aInput['name']);
+            if (method_exists($this, $sCustomMethod))
+                return $this->$sCustomMethod($aInput);
+        }
+
+        switch ($aInput['type']) {
+
+            case 'block_header':
+                $sRow = $this->genRowBlockHeader($aInput);
+                break;
+
+            case 'block_end':
+                $sRow = $this->genBlockEnd($aInput);
+                break;
+
+            case 'nested_form':
+                $sRow =  $this->genNestedForm($aInput);
+                break;
+
+            case 'textarea':
+                $sRow = $this->{'genViewRowWrapped' . ($aInput['html'] != 0 ? 'Html' : '')}($aInput);
+                break;
+
+            default:
+                $sRow = $this->genViewRowWrapped($aInput);
+        }
+
+        return $sRow;
+    }
+
+    /**
+     * Generate complete wrapped row for view mode form
+     *
+     * @param  array  $aInput
+     * @return string
+     */
+    function genViewRowWrapped(&$aInput)
+    {
+        return $this->_genViewRowWrapped($aInput);
+    }
+
+    function genViewRowWrappedHtml(&$aInput)
+    {
+        return $this->_genViewRowWrapped($aInput, [
+            'class_value' => 'bx-def-vanilla-html max-w-none'
+        ]);
+    }
+
+    protected function _genViewRowWrapped(&$aInput, $aParams = [])
+    {
+        $sValue = $this->genViewRowValue($aInput);
+        if (null === $sValue)
+            return '';
+
+        $sClass = !empty($aParams['class']) ? $aParams['class'] : '';
+        $sClassValue = !empty($aParams['class_value']) ? $aParams['class_value'] : '';
+
+        $aTmplVarsIcon = [];
+        if(!empty($aInput['icon'])) {
+            list ($sIcon, $sIconUrl, $sIconA, $sIconHtml) = BxTemplFunctions::getInstanceWithTemplate($this->oTemplate)->getIcon($aInput['icon']);
+
+            $aTmplVarsIcon = [
+                'bx_if:icon' => [
+                    'condition' => (bool)$sIcon,
+                    'content' => ['icon' => $sIcon],
+                ],
+                'bx_if:icon-html' => [
+                    'condition' => (bool)$sIconHtml,
+                    'content' => ['icon' => $sIconHtml],
+                ],
+                'bx_if:image_inline' => [
+                    'condition' => false,
+                    'content' => ['image' => ''],
+                ]
+            ];
+        }
+
+        return $this->oTemplate->parseHtmlByName('form_view_row.html', [
+            'type' => $aInput['type'], 
+            'class' => $sClass,
+            'bx_if:show_icon' => [
+                'condition' => !empty($aTmplVarsIcon),
+                'content' => $aTmplVarsIcon
+            ],
+            'caption' => isset($aInput['caption']) ? bx_process_output($aInput['caption']) : '',
+            'class_value' => $sClassValue,
+            'value' => $sValue
+        ]);
+    }
+
+    /**
+     * Generate value for view mode row
+     *
+     * @param  array  $aInput
+     * @return string
+     */
+    function genViewRowValue(&$aInput)
+    {
+        if (!empty($aInput['name'])) {
+            $sCustomMethod = 'genCustomViewRowValue' . $this->_genMethodName($aInput['name']);
+            if (method_exists($this, $sCustomMethod))
+                return $this->$sCustomMethod($aInput);
+        }
+
+        switch ($aInput['type']) {
+
+            case 'hidden':
+                $sValue = null;
+            break;
+
+            case 'select':
+            case 'radio_set':
+                $sValue = $this->genViewRowValueForSelect($aInput);
+            break;
+
+            case 'dateselect':    
+            case 'datepicker':
+                $sValue = null;
+                if (empty($aInput['value']) || !$aInput['value'] || '0000-00-00' == $aInput['value'])
+                    break;
+
+                if (isset($aInput['db']['pass']) && ('DateUtc' == $aInput['db']['pass'] || 'DateTimeUtc' == $aInput['db']['pass']) && !is_numeric($aInput['value'])) 
+                    $sValue = BxTemplFunctions::getInstance()->timeForJsFullDate($aInput['value'], isset($aInput['date_format']) ? $aInput['date_format']: BX_FORMAT_DATE, true, true);
+                else
+                    $sValue = BxTemplFunctions::getInstance()->{is_numeric($aInput['value']) ? 'timeForJs' : 'timeForJsFullDate'}($aInput['value'], isset($aInput['date_format']) ? $aInput['date_format'] : BX_FORMAT_DATE, true);
+            break;
+
+            case 'date_time':
+            case 'datetime':
+                $sValue = null;
+                if(empty($aInput['value']) || !$aInput['value'] || '0000-00-00 00:00:00' == $aInput['value'] || '0000-00-00 00:00' == $aInput['value'])
+                    break;
+
+                if (isset($aInput['db']['pass']) && ('DateUtc' == $aInput['db']['pass'] || 'DateTimeUtc' == $aInput['db']['pass']) && !is_numeric($aInput['value'])) 
+                    $sValue = BxTemplFunctions::getInstance()->timeForJsFullDate($aInput['value'], isset($aInput['date_format']) ? $aInput['date_format'] : BX_FORMAT_DATE_TIME, true, true);
+                else
+                    $sValue = BxTemplFunctions::getInstance()->{is_numeric($aInput['value']) ? 'timeForJs' : 'timeForJsFullDate'}($aInput['value'], isset($aInput['date_format']) ? $aInput['date_format'] : BX_FORMAT_DATE_TIME, true);
+            break;
+
+            case 'checkbox_set':
+            case 'select_multiple':
+                $sValue = null;
+                if (!empty($aInput['value']) && is_array($aInput['value'])) {
+                    $sValue = '';
+                    foreach ($aInput['value'] as $sVal)
+                        $sValue .= $aInput['values'][$sVal] . ', ';
+                    $sValue = trim ($sValue, ', ');
+                }
+            break;
+
+            case 'checkbox':
+            case 'switcher':
+                $sValue = isset($aInput['checked']) && $aInput['checked'] ? _t('_sys_form_checkbox_value_on') : _t('_sys_form_checkbox_value_off');
+            break;
+
+            case 'textarea':
+                if (isset($aInput['value']) && '' !== $aInput['value']) 
+                    $sValue = (isset($aInput['html']) && $aInput['html']) || (isset($aInput['code']) && $aInput['code']) ? $aInput['value'] : bx_process_output($aInput['value'], BX_DATA_TEXT_MULTILINE);
+                else
+                    $sValue = null;
+            break;
+
+            case 'text':
+                if(isset($aInput['value']) && '' !== $aInput['value'])
+                    $sValue = bx_linkify(bx_process_output($aInput['value']));
+                else 
+                    $sValue = null;
+                break;
+
+            case 'price':
+                if(isset($aInput['value']) && $aInput['value'] !== '')
+                    $sValue = _t_format_currency(bx_process_output($aInput['value']));
+                else
+                    $sValue = null;
+                break;
+
+            default:
+                if(isset($aInput['value']) && '' !== $aInput['value']) {
+                    $sValue = $aInput['value'];
+                    if(get_mb_substr($sValue, 0, 1) == '_')
+                        $sValue = _t($sValue);
+
+                    $sValue = bx_process_output($sValue);
+                }
+                else
+                    $sValue = null;
+        }
+
+        return $sValue;
+    }
+
+    function genViewRowValueForSelect(&$aInput)
+    {
+        if (!isset($aInput['value']) || !$aInput['value'])
+            return null;
+        $s = isset($aInput['value']) && isset($aInput['values'][$aInput['value']]) ? $aInput['values'][$aInput['value']] : null;
+        if (isset($aInput['values_list_name'])  && ($oCategory = BxDolCategory::getObjectInstanceByFormAndList($this->aFormAttrs['name'], $aInput['values_list_name'])) !== false)
+            return $oCategory->getCategoryLink($s, $aInput['value']);
+        return $s;
+    }
+
+    protected function genCustomRowBirthday(&$aInput)
+    {
+        if(!$this->_bViewMode)
+            return $this->genRowStandard($aInput);
+
+        $aInput['caption_src'] = '_sys_form_input_age';
+        $aInput['caption'] = _t($aInput['caption_src']);
+        return $this->genViewRowWrapped($aInput);
+    }
+
+    protected function genCustomRowCf(&$aInput)
+    {
+        $aInput = BxDolContentFilter::getInstance()->getInput($aInput);
+        if($aInput['type'] == 'hidden') {
+            $this->_sCodeAdd .= $this->genInput($aInput);
+            return '';
+        }
+
+        return $this->genRowStandard($aInput);
+    }
+
+    protected function genCustomViewRowValueBirthday(&$aInput)
+    {
+        if(!isset($aInput['value']) || !$aInput['value'] || in_array($aInput['value'], array('0000-00-00', '0000-00-00 00:00:00', '0000-00-00Z', '0000-00-00 00:00:00Z')))
+            return null;
+
+        return bx_birthday2age($aInput['value']);
+    }
+
+    /**
+     * Generate standard row
+     *
+     * @param  array  $aInput
+     * @return string
+     */
+    function genRowStandard(&$aInput, $isOneLine = false)
+    {
+        $sCaption = isset($aInput['caption']) ? bx_process_output($aInput['caption'], BX_DATA_HTML) : '';
+
+        $sRequired = !empty($aInput['required']) ? '<span class="bx-form-required">*</span>' : '';
+        
+        $sHelp = !empty($aInput['help']) ? ' <a href="javascript:void(0)" onclick="javascript:' . $this->getJsObjectName() . '.showHelp(this, \'' . $aInput['name'] . '\')"><i class="sys-icon question-circle"></i></a> ' : '';
+
+        $sClassAdd = !empty($aInput['error']) ? ' bx-form-error' : '';
+        $sInfoIcon = !empty($aInput['info']) ? $this->genInfoIcon($aInput['info']) : '';
+
+        $sErrorIcon = $this->genErrorIcon(empty($aInput['error']) ? '' : $aInput['error']);
+
+        $sClassWrapper = 'bx-form-element-wrapper';
+        if($isOneLine)
+            $sClassWrapper .= ' ' . $sClassWrapper . '-oneline';
+        $sClassWrapper .= ' bx-def-margin-top-auto';
+
+        if (isset($aInput['name'])) {
+            if (!isset($aInput['tr_attrs']) || !is_array($aInput['tr_attrs']))
+                $aInput['tr_attrs'] = [];
+            $aInput['tr_attrs']['id'] = "bx-form-element-" . $aInput['name'];
+        }
+        $sTrAttrs = bx_convert_array2attrs(isset($aInput['tr_attrs']) && is_array($aInput['tr_attrs']) ? $aInput['tr_attrs'] : array(), $sClassWrapper);
+
+        $sClassOneLineCaption = '';
+        $sClassOneLineValue = '';
+        if ($isOneLine) {
+            $sClassOneLineCaption = ' bx-form-caption-oneline bx-form-caption-oneline-' . $aInput['type'] . ' bx-def-margin-sec-left';
+            $sClassOneLineValue = ' bx-form-value-oneline bx-form-value-oneline-' . $aInput['type'];
+            if (!isset($aInput['attrs']) || !is_array($aInput['attrs']))
+                $aInput['attrs'] = [];
+            $aInput['attrs']['id'] = $this->getInputId($aInput);
+            if ($sCaption)
+                $sCaption = '<label for="' . $aInput['attrs']['id'] . '">' . $sCaption . '</label>';
+        }
+
+        $sInput = $this->genInput($aInput);
+        if(isset($aInput['error_updated']) && $aInput['error_updated'] === true)
+            $sErrorIcon = $this->genErrorIcon(empty($aInput['error']) ? '' : $aInput['error']);
+        if(empty($sErrorIcon)) 
+            $sErrorIcon = '';
+
+        $aTmplVarsRow = array(
+            'bx_if:show_caption' => array(
+                'condition' => !empty($sCaption),
+                'content' => array(
+                    'class_caption' => $sClassOneLineCaption,
+                    'caption' => $sCaption,
+                    'required' => $sRequired,
+                    'help' => $sHelp,
+                )
+            ),
+            'class_value' => $sClassAdd . $sClassOneLineValue,
+            'value' => $this->genWrapperInput($aInput, $sInput),
+            'info' => $sInfoIcon,
+            'error' => $sErrorIcon,
+        );
+
+        return $this->oTemplate->parseHtmlByName('form_row_standard.html', array(
+            'tr_attrs' => $sTrAttrs,
+            'bx_if:show_one_line' => array(
+                'condition' => $isOneLine,
+                'content' => $aTmplVarsRow
+            ),
+            'bx_if:show_lined' => array(
+                'condition' => !$isOneLine,
+                'content' => $aTmplVarsRow
+            ),
+            'info' => $sInfoIcon,
+            'error' => $sErrorIcon
+        ));
+    }
+
+    function genWrapperInput($aInput, $sContent)
+    {
+        $aAttrs = $this->_genWrapperInputAttrs($aInput);
+
+        $sClass = "bx-form-input-wrapper bx-form-input-wrapper-{$aInput['type']}";
+        if(isset($aInput['html']) && $aInput['html'] && $this->isHtmlEditor($aInput['html'], $aInput))
+            $sClass .= ' bx-form-input-wrapper-html';
+
+        return $this->oTemplate->parseHtmlByName('form_input_wrapper.html', array(
+            'attrs' =>  bx_convert_array2attrs($aAttrs, $sClass),
+            'content' => $sContent
+        ));
+    }
+
+    protected function _genWrapperInputAttrs(&$aInput)
+    {
+        return isset($aInput['attrs_wrapper']) && is_array($aInput['attrs_wrapper']) ? $aInput['attrs_wrapper'] : [];
+    }
+
+    /**
+     * Generate custom row
+     *
+     * @param  array  $aInput
+     * @param  string $sCustomMethod custom method to generate code for input
+     * @return string
+     */
+    function genRowCustom(&$aInput, $sCustomMethod)
+    {
+        $sCaption = isset($aInput['caption']) ? bx_process_output($aInput['caption']) : '';
+
+        $sRequired = !empty($aInput['required']) ? '<span class="bx-form-required">*</span>' : '';
+        
+        $sHelp = !empty($aInput['help']) ? ' <a href="javascript:void(0)" onclick="javascript:' . $this->getJsObjectName() . '.showHelp(this, \'' . $aInput['name'] . '\')"><i class="sys-icon question-circle"></i></a> ' : '';
+
+        $sClassAdd = !empty($aInput['error']) ? ' bx-form-error' : '';
+        $sInfoIcon = !empty($aInput['info']) ? $this->genInfoIcon($aInput['info']) : '';
+
+        $sErrorIcon = $this->genErrorIcon(empty($aInput['error']) ? '' : $aInput['error']);
+        $sInput = $this->$sCustomMethod($aInput, $sInfoIcon, $sErrorIcon);
+
+        if (isset($aInput['name'])) {
+            if (!isset($aInput['tr_attrs']) || !is_array($aInput['tr_attrs']))
+                $aInput['tr_attrs'] = [];
+            $aInput['tr_attrs']['id'] = "bx-form-element-" . $aInput['name'];
+        }
+        $sTrAttrs = bx_convert_array2attrs(empty($aInput['tr_attrs']) ? array() : $aInput['tr_attrs'], "bx-form-element-wrapper bx-def-margin-top");
+
+        return $this->oTemplate->parseHtmlByName('form_row_custom.html', array(
+            'tr_attrs' => $sTrAttrs,
+            'bx_if:show_caption' => array(
+                'condition' => !empty($sCaption),
+                'content' => array(
+                    'class_caption' => '',
+                    'caption' => $sCaption,
+                    'required' => $sRequired,
+                    'help' => $sHelp
+                )
+            ),
+            'class_value' => $sClassAdd,
+            'value' => $sInput,
+        ));
+    }
+
+    /**
+     * Generate Block Headers row
+     *
+     * @param  array  $aInput
+     * @return string
+     */
+    function genRowBlockHeader(&$aInput)
+    {
+        $aAttrs = empty($aInput['attrs']) ? '' : $aInput['attrs'];
+
+        // if there is no caption - show divider only
+
+        if (empty($aInput['caption'])) {
+            $sCode = $this->{$this->_sSectionClose}();
+            $sCode .= $this->{$this->_sSectionOpen}($aAttrs);
+            return $sCode;
+        }
+
+        // if section is collapsed by default, add necessary code
+
+        $sClassAddCollapsable = 'bx-form-collapsable';
+        if (isset($aInput['collapsed']) and $aInput['collapsed'])
+            $sClassAddCollapsable .= ' bx-form-collapsed bx-form-section-hidden';
+
+        // display section with caption
+
+        $sCode = $this->{$this->_sSectionClose}();
+
+        if (empty($aAttrs))
+            $aAttrs = array('class' => 'bx-form-collapsable ' . $sClassAddCollapsable);
+        else
+            $aAttrs['class'] .= ' bx-form-collapsable ' . $sClassAddCollapsable;
+
+        if (isset($this->aParams['view_mode']) && $this->aParams['view_mode'])
+            $sTitle = '<div class="bx-form-section-title bx-def-font-grayed bx-def-font-large">' . bx_process_output($aInput['caption'], BX_DATA_HTML) . (!empty($aInput['info']) ? '<br /><span>' . bx_process_output($aInput['info']) . '</span>' : '') . '</div>';
+        else
+            $sTitle = '<div class="bx-form-section-title bx-def-font-grayed bx-def-font-large"><a href="javascript:void(0);">' . bx_process_output($aInput['caption'], BX_DATA_HTML) . '</a>' . (!empty($aInput['info']) ? '<br /><span>' . bx_process_output($aInput['info']) . '</span>' : '') . '</div>';
+
+        if (isset($aInput['name'])) {
+            if (!isset($aInput['tr_attrs']) || !is_array($aInput['tr_attrs']))
+                $aInput['tr_attrs'] = [];
+            $aInput['tr_attrs']['id'] = "bx-form-section-" . $aInput['name'];
+        }
+
+        $sCode .= $this->{$this->_sSectionOpen}($aAttrs, $sTitle, !empty($aInput['tr_attrs']) ? $aInput['tr_attrs'] : []);
+
+        return $sCode;
+    }
+
+    function genBlockEnd()
+    {
+        $aNextTbodyAdd = false; // need to have some default
+        $sCode = '';
+        $sCode .= $this->{$this->_sSectionClose}();
+        $sCode .= $this->{$this->_sSectionOpen}($aNextTbodyAdd);
+        return $sCode;
+    }
+
+    /**
+     * Generate HTML Input Element
+     *
+     * @param  array  $aInput
+     * @return string Output HTML Code
+     */
+    function genInput(&$aInput)
+    {
+        if (!empty($aInput['name'])) {
+            $sCustomMethod = 'genCustomInput' . $this->_genMethodName($aInput['name']);
+            if (method_exists($this, $sCustomMethod))
+                return $this->$sCustomMethod($aInput);
+        }
+
+        switch ($aInput['type']) {
+
+            case 'datepicker':
+                if (!isset($aInput['attrs']))
+                    $aInput['attrs'] = array();
+                $aInput['attrs']['autosuggestion'] = 'off'; 
+                
+                $sInput = $this->genInputStandard($aInput);
+            break;
+                
+            // standard inputs (and non-standard, interpreted as standard)
+            case 'text':
+            case 'datepicker':
+            case 'dateselect':
+            case 'date_time':
+            case 'datetime':
+            case 'number':
+            case 'time':
+            case 'checkbox':
+            case 'radio':
+            case 'image':
+            case 'slider':
+            case 'doublerange':
+            case 'hidden':
+                $sInput = $this->genInputStandard($aInput);
+            break;
+            
+            case 'password':
+            	$sInput = $this->genInputPassword($aInput);
+            	break;
+            
+            case 'price':
+            	$sInput = $this->genInputPrice($aInput);
+            	break;
+
+            case 'file':
+            	$sInput = $this->genInputFile($aInput);
+            	break;
+
+            case 'rgb':
+            case 'rgba':
+            	$sClass = 'bx-form-input-' . $aInput['type'];
+            	$aInput['attrs']['class'] = !empty($aInput['attrs']['class']) ? $aInput['attrs']['class'] . ' ' . $sClass : $sClass; 
+
+            	$aInput['type'] = 'text';
+                $sInput = $this->genInputStandard($aInput);
+            break;
+                
+            case 'rgb-list':
+                $sInput = $this->genInputSelectRgb($aInput);
+            break;    
+
+            case 'switcher':
+                $sInput = $this->genInputSwitcher($aInput);
+            break;
+
+            case 'button':
+            case 'reset':
+            case 'submit':
+                $sInput = $this->genInputButton($aInput);
+            break;
+
+            case 'textarea':
+                $sInput = $this->genInputTextarea($aInput);
+            break;
+
+            case 'select':
+                $sInput = $this->genInputSelect($aInput);
+            break;
+
+            case 'select_multiple':
+                $sInput = $this->genInputSelectMultiple($aInput);
+            break;
+
+            case 'checkbox_set':
+                $sInput = $this->genInputCheckboxSet($aInput);
+            break;
+
+            case 'radio_set':
+                $sInput = $this->genInputRadioSet($aInput);
+            break;
+
+            case 'input_set': // numeric array of inputs
+                $sInput = '';
+                $sDivider = isset($aInput['dv']) ? $aInput['dv'] : ' ';
+                foreach ($aInput as $iKey => $aSubInput) {
+                    if (!is_int($iKey) or !$aSubInput)
+                        continue; // parse only integer keys and existing values
+
+                    $sInput .= $this->genInput($aSubInput); // recursive call
+                    $sInput .= $sDivider;
+                }
+                $sInput .= '<div class="bx-clear"></div>';
+            break;
+
+            case 'custom':
+                $sInput = isset($aInput['content']) ? $aInput['content'] : '';
+            break;
+
+            case 'captcha':
+                $sInput = $this->genInputCaptcha($aInput);
+            break;
+
+            case 'location':
+                $sInput = $this->genInputLocation($aInput);
+            break;
+            
+            case 'nested_form':
+                $sInput = $this->genInputNestedForm($aInput);
+                break;
+
+            case 'value':
+                $sInput = '';
+                if(!isset($aInput['value']))
+                    break;
+
+                $sInput = $aInput['value'];
+                if(get_mb_substr($sInput, 0, 1) == '_') 
+                    $sInput = _t($sInput);
+            break;
+
+            default:
+                //unknown control type
+                $sInput = 'Unknown control type';
+        }
+
+        // create input label
+        $sInput .= $this->genLabel($aInput);
+
+        // create input privacy group chooser
+        $sInput .= $this->genPrivacyGroupChooser($aInput);
+
+        return $sInput;
+    }
+
+    /**
+     * Generate new Input Element id
+     *
+     * @param  array  $aInput
+     * @return string
+     */
+    function getInputId(&$aInput)
+    {
+        if (isset($aInput['id']))
+            return $aInput['id'];
+
+        $sName = md5($aInput['name']);
+
+        $sID = $this->id . '_input_' . $sName;
+
+        if ( // multiple elements cause identical id's
+            (
+                (
+                    $aInput['type'] == 'checkbox' and
+                    substr($aInput['name'], -2) == '[]' // it is multiple element
+                ) or
+                $aInput['type'] == 'radio' // it is always multiple (i think so... hm)
+            ) and
+            isset($aInput['value']) // if we can make difference
+        ) {
+            $sValue = md5($aInput['value']);
+
+            // add value
+            $sID .= '_' . $sValue;
+        }
+
+        $sID = trim($sID, '_');
+
+        $aInput['id'] = $sID; // just for repeated calls
+
+        return $sID;
+    }
+
+    /**
+     * Generate standard Input Element
+     *
+     * @param  array  $aInput
+     * @return string
+     */
+    function genInputStandard(&$aInput)
+    {
+        $aAttrs = $this->_genInputStandardAttrs($aInput);
+
+        return  $this->oTemplate->parseHtmlByName('form_field_standard.html', [
+            'attrs' => bx_convert_array2attrs($aAttrs, "bx-def-font-inputs bx-form-input-{$aInput['type']}")
+        ]);
+    }
+
+    protected function _genInputStandardAttrs(&$aInput)
+    {
+        // clone attributes for system use ;)
+        $aAttrs = empty($aInput['attrs']) ? array() : $aInput['attrs'];
+
+        // add default className to attributes
+        $aAttrs['type'] = $aInput['type'];
+        if ('datetime' == $aAttrs['type'])
+            $aAttrs['type'] = 'date_time';
+
+        switch($aAttrs['type']) {
+            case 'datepicker':
+            case 'date_time':
+                $this->addCssJsTimepicker ();
+                break;
+            case 'slider':
+            case 'doublerange':
+                $this->addCssJsUi ();
+                break;
+            case 'dateselect':
+                $this->_addJs(array(
+                    'combodate/combodate.js',
+                ), "'undefined' === typeof($.combodate)");
+                break;
+            case 'text':
+                if (isset($aAttrs['class']) && false !== strpos($aAttrs['class'], 'bx-form-input-rgb'))
+                    $this->addCssJsMinicolors ();
+                break;
+        }
+
+        if ('doublerange' == $aAttrs['type'] && (!isset($aInput['value']) || !$aInput['value']) && !empty($aAttrs['min']) && !empty($aAttrs['max'])) {
+            $aInput['value'] = $aAttrs['min'] . '-' . $aAttrs['max'];
+        }
+
+        if (isset($aInput['name'])) $aAttrs['name'] = $aInput['name'];
+        if (isset($aInput['value'])) $aAttrs['value'] = $aInput['value'];
+        if (isset($aInput['db']['pass']) && ('DateUtc' == $aInput['db']['pass'] || 'DateTimeUtc' == $aInput['db']['pass'])) $aAttrs['data-utc'] = 1;
+
+        if (!isset($aAttrs['data-format-24h']))
+            $aAttrs['data-frmt-24h'] = getParam('sys_format_input_24h');
+        if (!isset($aAttrs['data-format-date']))
+            $aAttrs['data-frmt-date'] = getParam('sys_format_date');
+        if (!isset($aAttrs['data-format-datetime']))
+            $aAttrs['data-frmt-datetime'] = getParam('sys_format_datetime');
+
+        // for inputs with labels generate id
+        if (isset($aInput['label']))
+            $aAttrs['id'] = $this->getInputId($aInput);
+
+        // for checkboxes
+        if (isset($aInput['checked']) and $aInput['checked'])
+            $aAttrs['checked'] = 'checked';
+
+        return $aAttrs;
+    }
+
+    /**
+     * Generate Switcher Input Element (based on checkbox)
+     *
+     * @param  array  $aInput
+     * @return string
+     */
+    function genInputSwitcher(&$aInput)
+    {
+        $aInput['type'] = 'checkbox';
+        $sCheckbox = $this->genInputStandard($aInput);
+
+        $aInput['type'] = 'switcher';
+
+        $sClass = 'off';
+        if(isset($aInput['checked']) && $aInput['checked'])
+            $sClass = 'on';
+
+        return $this->oTemplate->parseHtmlByName('form_field_switcher.html', [
+            'class' => $sClass,
+            'checkbox' => $sCheckbox
+        ]);
+    }
+
+    /**
+     * Generate Button Input Element
+     *
+     * @param  array  $aInput
+     * @return string
+     */
+    function genInputButton(&$aInput)
+    {
+        $aAttrs = $this->_genInputButtonAttrs($aInput);
+
+        $sClassAdd = "bx-def-font-inputs bx-form-input-{$aInput['type']} bx-btn";
+        if($aInput['type'] == 'submit')
+            $sClassAdd .= ' bx-btn-primary';
+
+        return $this->oTemplate->parseHtmlByName('form_field_button.html', [
+            'attrs' => bx_convert_array2attrs($aAttrs, $sClassAdd),
+            'value' => $aInput['value']
+        ]);
+    }
+    
+    protected function _genInputButtonAttrs(&$aInput)
+    {
+        $aAttrs = !empty($aInput['attrs']) ? $aInput['attrs'] : [];
+
+        // add default className to attributes
+        $aAttrs['type'] = $aInput['type'];
+        if(isset($aInput['value']))
+            $aAttrs['value'] = $aInput['value'];
+
+        if(isset($aInput['name'])) 
+            $aAttrs['name'] = $aInput['name'];
+        
+        return $aAttrs;
+    }
+
+    /**
+     * Generate Textarea Element
+     *
+     * @param  array  $aInput
+     * @return string
+     */
+    function genInputTextarea(&$aInput)
+    {
+        $aAttrs = $this->_genInputTextareaAttrs($aInput);
+
+        $sUniq = genRndPwd(10, false);
+        
+        $sClassAdd = "bx-def-font-inputs bx-form-input-{$aInput['type']}";
+        if(isset($aInput['html']) && $aInput['html'] && $this->addHtmlEditor($aInput['html'], $aInput, $sUniq)){
+            $sClassAdd .= ' bx-form-input-html ' . $sUniq;
+        }
+
+        $sValue = isset($aInput['value']) ? bx_process_output((isset($aInput['html']) && $aInput['html']) || (isset($aInput['code']) && $aInput['code']) ? $aInput['value'] : strip_tags($aInput['value']), BX_DATA_TEXT, array('no_process_macros')) : '';
+
+        return $this->oTemplate->parseHtmlByName('form_field_textarea.html', [
+            'attrs' => bx_convert_array2attrs($aAttrs, $sClassAdd),
+            'value' => $sValue
+        ]);
+    }
+
+    protected function _genInputTextareaAttrs(&$aInput)
+    {
+        $aAttrs = !empty($aInput['attrs']) ? $aInput['attrs'] : array();
+
+        $aAttrs['name'] = $aInput['name'];
+
+        // for inputs with labels generate id
+        if (isset($aInput['label']))
+            $aAttrs['id'] = $this->getInputId($aInput);
+        
+        return $aAttrs;
+    }
+
+    function isHtmlEditor($iViewMode, &$aInput)
+    {
+		return BxDolEditor::getObjectInstance(false, $this->oTemplate) !== false;
+    }
+
+    function addHtmlEditor($iViewMode, &$aInput, $sUniq)
+    {
+        $oEditor = BxDolEditor::getObjectInstance(false, $this->oTemplate);
+        if (!$oEditor)
+            return false;
+
+        $this->_sCodeAdd .= $oEditor->attachEditor ('#' . $this->aFormAttrs['id'] . ' [name=' . $aInput['name'] . ']', $iViewMode, $this->_bDynamicMode, ['form_id' => $this->aFormAttrs['id'], 'element_name' => $aInput['name'], 'query_params' => $this->getHtmlEditorQueryParams($aInput), 'uniq' => $sUniq]);
+
+        return true;
+    }
+    
+    function getHtmlEditorQueryParams($aInput)
+    {
+        $aQueryParams = ['i' => $aInput['name'], 'f' => $this->aFormAttrs['id'], 'fi' => ''];
+        
+        bx_alert('system', 'editor_query_params', 0, 0, array(
+            'form' => $this,
+            'override_result' => &$aQueryParams
+        ));
+        
+        return $aQueryParams;
+    }
+
+    /**
+     * Generate Select Box Element
+     *
+     * @param  array  $aInput
+     * @return string
+     */
+    function genInputSelectBox(&$aInput, $sInfo = '', $sError = '')
+    {
+        $aNewInput = $aInput;
+
+        $aNewInput['type'] = 'select';
+        $aNewInput['name'] .= '[]';
+
+        $sInput = $this->genInput($aNewInput);
+        return <<<BLAH
+                <div class="bx-form-input-wrapper input-wrapper-{$aInput['type']}">
+                   $sInput
+                </div>
+                $sInfo
+                $sError
+BLAH;
+
+    }
+
+    /**
+     * Generate Browse File Element
+     *
+     * @param  array  $aInput
+     * @return string
+     */
+    function genInputFile(&$aInput)
+    {
+    	$sOnChange = "$(this).parents('.bx-form-input-wrapper-file:first').find('.bx-fif-value').html($(this).val());";
+    	if(!empty($aInput['attrs']['onchange']))
+    		$aInput['attrs']['onchange'] = $sOnChange . $aInput['attrs']['onchange'];
+    	else
+    		$aInput['attrs']['onchange'] = $sOnChange;
+
+    	$sInput = $this->genInputStandard($aInput);
+    	return '<label class="bx-btn">' . $sInput . '<span class="bx-fif-label">' . _t('_sys_form_txt_select_file') . '</span></label><span class="bx-fif-value bx-def-margin-thd-left"></span>';
+    }
+
+    /**
+     * Generate Select Box Element
+     *
+     * @param  array  $aInput
+     * @return string
+     */
+    function genInputFiles(&$aInput, $sInfo = '', $sError = '')
+    {
+        if(empty($aInput['uploaders']) || !is_array($aInput['uploaders']))
+            return '';
+
+        $sUniqId = !empty($aInput['uploaders_id']) ? $aInput['uploaders_id'] : genRndPwd (8, false);
+        $bInitGhosts = isset($aInput['init_ghosts']) && !$aInput['init_ghosts'] ? 0 : 1;
+        $bInitReordering = empty($aInput['init_reordering']) ? 0 : 1;
+
+        $oUploader = null;
+        $sUploadersButtons = $sUploadersJs = '';
+        foreach ($aInput['uploaders'] as $sUploaderObject) {
+            $oUploader = BxDolUploader::getObjectInstance($sUploaderObject, $aInput['storage_object'], $sUniqId, $this->oTemplate);
+            if(!$oUploader)
+                continue;
+
+            //--- Get Button code.
+            $aAttrs = !empty($aInput['attrs']) ? $aInput['attrs'] : array();
+            if(empty($aInput['attrs']['disabled']))
+                $aAttrs = array_merge($aAttrs, array('onclick' => $oUploader->getNameJsInstanceUploader() . '.showUploaderForm();'));
+
+            $aParamsButtons = [
+                'content_id' => isset($aInput['content_id']) ? $aInput['content_id'] : '',
+                'storage_private' => isset($aInput['storage_private']) ? $aInput['storage_private'] : '1',
+                'attrs' => bx_convert_array2attrs($aAttrs),
+                'btn_class' => !empty($aInput['attrs']['disabled']) ? 'bx-btn-disabled' : '',
+                'button_title' => bx_js_string($oUploader->getUploaderButtonTitle(isset($aInput['upload_buttons_titles']) ? $aInput['upload_buttons_titles'] : false)),
+            ];
+
+            $sUploadersButtons .= $oUploader->getUploaderButton($aParamsButtons);
+
+            //--- Get JS code.
+            $sGhostTemplate = $this->genGhostTemplate($aInput);
+
+            $aParamsJs = array_merge($oUploader->getUploaderJsParams(), 
+                [
+                    'content_id' => isset($aInput['content_id']) ? $aInput['content_id'] : '',
+                    'storage_private' => isset($aInput['storage_private']) ? $aInput['storage_private'] : '1',
+                    'is_init_ghosts' => $bInitGhosts,
+                    'is_init_reordering' => $bInitReordering
+                ]
+            );
+            if(isset($aInput['images_transcoder']) && $aInput['images_transcoder'])
+                $aParamsJs['images_transcoder'] = bx_js_string($aInput['images_transcoder']);
+
+            $sUploadersJs .= $oUploader->getUploaderJs($sGhostTemplate, isset($aInput['multiple']) ? $aInput['multiple'] : true, $aParamsJs, $this->_bDynamicMode);
+        }
+
+        if(!$oUploader)
+            return '';
+
+        if($bInitReordering) {
+            $this->addCssJsUi();
+            $this->addCssJsUiSortable();
+        }
+
+        return $this->oTemplate->parseHtmlByName('form_field_uploader.html', array(
+            'uploaders_buttons' => $sUploadersButtons,
+            'info' => $sInfo,
+            'error' => $sError,
+            'id_container_errors' => $oUploader ? $oUploader->getIdContainerErrors() : '',
+            'id_container_result' => $oUploader ? $oUploader->getIdContainerResult() : '',
+            'uploader_instance_name' => $oUploader ? $oUploader->getNameJsInstanceUploader() : '',
+            'uploaders_js' => $sUploadersJs,
+        ));
+    }
+
+    protected function genGhostTemplate(&$aInput)
+    {
+        $sGhostTemplate = false;
+        if (isset($aInput['ghost_template']) && is_object($aInput['ghost_template'])) { // form is not submitted and ghost template is BxDolFormNested object
+
+            $oFormNested = $aInput['ghost_template'];
+            if ($oFormNested instanceof BxDolFormNestedGhost)
+                $sGhostTemplate = $oFormNested->genForm();
+
+        } elseif (isset($aInput['ghost_template']) && is_array($aInput['ghost_template']) && isset($aInput['ghost_template']['inputs'])) { // form is not submitted and ghost template is form array
+
+            $oFormNested = new BxDolFormNestedGhost($aInput['name'], $aInput['ghost_template'], $this->aParams['db']['submit_name'], $this->oTemplate);
+            $sGhostTemplate = $oFormNested->getCode();
+
+        } elseif (isset($aInput['ghost_template']) && is_array($aInput['ghost_template']) && $aInput['ghost_template']) { // form is submitted and ghost template is array of BxDolFormNested objects
+
+            $sGhostTemplate = array ();
+            foreach ($aInput['ghost_template'] as $iFileId => $oFormNested)
+                if (is_object($oFormNested) && $oFormNested instanceof BxDolFormNestedGhost)
+                    $sGhostTemplate[$iFileId] = $oFormNested->genForm();
+
+        } elseif (isset($aInput['ghost_template']) && is_string($aInput['ghost_template'])) { // ghost template is just string template, without nested form
+
+            $sGhostTemplate = $aInput['ghost_template'];
+
+        }
+
+        return $sGhostTemplate;
+    }
+
+    protected function genCustomInputUsernamesSuggestions ($aInput)
+    {
+        $bDisabled = isset($aInput['attrs']['disabled']) && $aInput['attrs']['disabled'] == 'disabled';
+
+        $aAttrs = $this->_genCustomInputUsernamesSuggestionsAttrs($aInput, $bDisabled);
+
+        $aTmplVarsVals = [];
+        $sValue = '';
+        if(!empty($aInput['value'])) {
+            $sCustomMethod = 'genCustomInput' . $this->_genMethodName($aInput['name']) . 'Value';
+            if(method_exists($this, $sCustomMethod))
+                $aTmplVarsVals = $this->$sCustomMethod($aInput);
+            else if(is_array($aInput['value']) || (is_numeric($aInput['value']))) {
+                if (!is_array($aInput['value']))
+                    $aInput['value'] = [$aInput['value']];
+                    
+                foreach($aInput['value'] as $sVal) {
+                    if(!$sVal || !($oProfile = BxDolProfile::getInstance($sVal)))
+                        continue;
+
+                   $aTmplVarsVals[] = [
+                       'item_unit' => $oProfile->getUnit(0, ['template' => ['name' => 'unit_wo_info', 'size' => 'icon']]),
+                       'item_name' => $oProfile->getDisplayName(),
+                       'name' => $aInput['name'] . (isset($aInput['custom']['only_once']) && $aInput['custom']['only_once'] == 1 ? '' : '[]'),
+                       'value' => $sVal
+                   ];
+                }
+            }
+            else if(is_string($aInput['value']))
+                $sValue = $aInput['value'];
+        }
+
+        $aTmplVarsInputText = [];
+        if(!$bDisabled) {
+            $aInputText = $aInput;
+            $aInputText['type'] = 'text';
+            $aInputText['value'] = '';
+            $aInputText['attrs'] = $this->_genCustomInputUsernamesSuggestionsTextAttrs($aInputText, $bDisabled);       
+            unset($aInputText['name']);
+
+            $aTmplVarsInputText['input'] = $this->genInputStandard($aInputText);
+        }
+
+        $this->addCssJsUi();
+
+        $sJsCode = '';
+        if(!$bDisabled) {
+            $sJsCode = $this->oTemplate->parseHtmlByName('form_field_custom_suggestions_js.html', [
+                'id' => $aAttrs['id'],
+                'name' => $aInput['name'],
+                'url_get_recipients' => $aInput['ajax_get_suggestions'],
+                'b_img' => isset($aInput['custom']['b_img']) ? (int)$aInput['custom']['b_img'] : 1,
+                'only_once' => isset($aInput['custom']['only_once']) ? 1 : 0,
+                'on_select' => isset($aInput['custom']['on_select']) ? $aInput['custom']['on_select']: 'null',
+                'placeholder' => bx_html_attribute(isset($aInput['placeholder']) ? $aInput['placeholder'] : _t('_sys_form_paceholder_profiles_suggestions'), BX_ESCAPE_STR_QUOTE),
+            ]);
+
+            if($this->_bDynamicMode)
+                $sJsCode = $this->oTemplate->addJsPreloadedWrapped([
+                    'jquery-ui/jquery-ui.min.js'
+                ], $sJsCode);
+            else 
+                $sJsCode = $this->oTemplate->addJsCodeOnLoadWrapped($sJsCode);
+        }       
+
+        return $this->oTemplate->parseHtmlByName('form_field_custom_suggestions.html', array(
+            'name' => $aInput['name'],
+            'attrs' => bx_convert_array2attrs($aAttrs),
+            'bx_repeat:vals' => $aTmplVarsVals,
+            'value' => $sValue,
+            'bx_if:input' => array(
+                'condition' => !$bDisabled,
+                'content' => $aTmplVarsInputText
+            ),
+            'js_code' => $sJsCode
+        ));
+    }
+
+    protected function _genCustomInputUsernamesSuggestionsAttrs (&$aInput, $bDisabled = false)
+    {
+        $aAttrs = ['id' => $aInput['name'] . time() . mt_rand(0, 100)];
+
+        $aAttrs['class'] = 'bx-form-input-autotoken bx-form-input-text bx-def-font-inputs';
+        if($bDisabled)
+            $aAttrs['class'] .= ' bx-form-input-disabled';
+        if(!empty($aInput['attrs']['class']))
+            $aAttrs['class'] .= ' ' . $aInput['attrs']['class'];
+
+        return $aAttrs;
+    }
+
+    protected function _genCustomInputUsernamesSuggestionsTextAttrs (&$aInput, $bDisabled = false)
+    {
+        $aAttrs = ['value' => '', 'autocomplete' => 'off', 'autocapitalize' => 'off', 'autocorrect' => 'off'];
+
+        if(!empty($aInput['attrs']) && is_array($aInput['attrs'])) {
+            if(isset($aInput['attrs']['class']))
+                unset($aInput['attrs']['class']);
+
+            $aAttrs = array_merge($aAttrs, $aInput['attrs']);
+        }
+
+        return $aAttrs;
+    }
+
+    protected function genCustomViewRowValueLabels ($aInput)
+    {        
+        if (empty($aInput['value']) || !($oMetatags = BxDolMetatags::getObjectInstance($aInput['meta_object'])) || !$oMetatags->keywordsIsEnabled())
+            return null;
+        
+        if (!empty($aInput['value']) && !is_array($aInput['value']))
+            $aInput['value'] = unserialize($aInput['value']);
+        
+        $s = ''; 
+        foreach ($aInput['value'] as $sLabel)
+            $s .= '<a href="' . $oMetatags->keywordsGetHashTagUrl($sLabel, $aInput['content_id']) . '"><b class="bx-def-label val">' . trim($sLabel) . '</b></a>';
+
+        return $this->oTemplate->parseHtmlByName('form_field_labels_view.html', array(
+            'values' => $s,
+        ));
+    }
+
+    protected function genCustomInputLabels (&$aInput)
+    {
+        $oLabel = BxDolLabel::getInstance();
+
+        $sName = !empty($aInput['name']) ? $aInput['name'] : 'labels';
+
+        $sValue = '';
+        if(!empty($aInput['value'])) {
+            if(!is_array($aInput['value']))
+                $aInput['value'] = unserialize($aInput['value']);
+
+            if(is_array($aInput['value']))
+                foreach($aInput['value'] as $sLabel)
+                    $sValue .= $oLabel->getLabel($sName, $sLabel);
+        }
+
+        $sKeyPlaceholder = $aInput['caption_src'] . '_placeholder';
+        if(strcmp($sKeyPlaceholder, _t($sKeyPlaceholder)) != 0)
+            $sValue .= $oLabel->getLabelPlaceholder($sKeyPlaceholder);                   
+
+        $aInputLabels = array(
+            'type' => 'custom',
+            'name' => $sName,
+            'caption' => '',
+            'value' => $sValue,
+            'ajax_get_suggestions' => BX_DOL_URL_ROOT . bx_append_url_params('label.php', array(
+                'action' => 'labels_list',
+            )),
+            'attrs' => array(
+                'class' => 'bx-form-input-labels',
+                'disabled' => 'disabled'
+            )
+        );
+        
+        if (bx_is_api()){
+            $aInput['type'] = 'labels';
+            //$aInput['ajax_get_suggestions'] = 'system/select_labels/TemplLabelsServices&params[]=';
+            $aInput['values'] = bx_srv('system', 'get_labels', [], 'TemplLabelsServices');
+
+        }
+
+        return $this->oTemplate->parseHtmlByName('label_select_field.html', array(
+            'js_object' => $oLabel->getJsObjectName(),
+            'js_code' => $oLabel->getJsCodeForm(),
+            'html_id' => $oLabel->getFormFieldId($aInput),
+            'name' => $sName,
+            'input_labels' => $this->genCustomInputUsernamesSuggestions($aInputLabels)
+        ));
+    }
+
+    /**
+     * Generate Select Element	
+     *
+     * @param  array  $aInput
+     * @return string
+     */
+    function genInputSelect(&$aInput)
+    {
+        $sCurValue = isset($aInput['value']) ? $aInput['value'] : '';
+        return $this->_genInputSelect($aInput, false, $sCurValue, '_isSelected');
+    }
+    
+    /**
+     * Generate Select RGB Element	
+     *
+     * @param  array  $aInput
+     * @return string
+     */
+    function genInputSelectRgb(&$aInput)
+    {
+        $aInput['type'] = 'select';
+        
+        $sCurValue = isset($aInput['value']) ? $aInput['value'] : '';
+        
+        $aInput['values_list_name'] = 'sys_colors';
+        $aInput['values'] = self::getDataItems('sys_colors', isset(self::$TYPES_SET[$aInput['type']]));
+        $aInput['attrs'] = ['class' => 'bx-form-input-rgb-list'];
+        
+        foreach ($aInput['values'] as $sOptValue => $sOptTitle) {
+            $aInput['values'][$sOptValue] = ['key' => $sOptValue, 'value' => $sOptTitle, 'class' => 'bg-' . $sOptValue, 'style' => 'color:#fff'];
+        }
+
+        return $this->_genInputSelect($aInput, false, $sCurValue, '_isSelected');
+    }
+
+    /**
+     * Generate Multiple Select Element
+     *
+     * @param  array  $aInput
+     * @return string
+     */
+    function genInputSelectMultiple(&$aInput)
+    {
+        $aCurValues = array();
+        if (isset($aInput['value']) && $aInput['value'])
+            $aCurValues = is_array($aInput['value']) ? $aInput['value'] : array();
+        return $this->_genInputSelect($aInput, true, $aCurValues, '_isSelectedMultiple');
+    }
+
+    /**
+     * Generate Checkbox Set Element
+     *
+     * @param  array  $aInput
+     * @return string
+     */
+    function genInputCheckboxSet(&$aInput)
+    {
+        $aCurValues = array();
+        if (isset($aInput['value']) && $aInput['value'])
+            $aCurValues = is_array($aInput['value']) ? $aInput['value'] : array();
+        return $this->_genInputsSet($aInput, 'checkbox', $aCurValues, '_isSelectedMultiple', '[]');
+    }
+    /**
+     * Generate Radiobuttons Set Element
+     *
+     * @param  array  $aInput
+     * @return string
+     */
+    function genInputRadioSet(&$aInput)
+    {
+        $sCurValue = isset($aInput['value']) ? $aInput['value'] : '';
+        return $this->_genInputsSet($aInput, 'radio', $sCurValue, '_isSelected');
+    }
+
+    function _isSelected ($sValue, $sCurValue)
+    {
+        return ((string)$sValue === (string)$sCurValue);
+    }
+
+    function _isSelectedMultiple ($sValue, $aCurValues)
+    {
+        return in_array($sValue, $aCurValues);
+    }
+
+    function _genInputSelect(&$aInput, $isMultiple, $mixedCurrentVal, $sIsSelectedFunc)
+    {
+        $aOptions = [];
+        if(isset($aInput['values']) && is_array($aInput['values'])) {
+            foreach ($aInput['values'] as $sOptValue => $sOptTitle) {
+                $aOption = array(
+                    'bx_if:show_option_group_begin' => [
+                        'condition' => false,
+                        'content' => []
+                    ],
+                    'bx_if:show_option_group_end' => [
+                        'condition' => false,
+                        'content' => []
+                    ],
+                    'bx_if:show_option' => [
+                        'condition' => false,
+                        'content' => []
+                    ]
+                );
+
+                $sOptAttrs = "";
+                if(is_array($sOptTitle)) {
+                    if(isset($sOptTitle['type'])) {
+                        switch($sOptTitle['type']) {
+                            case 'group_header':
+                                $aOption = array_merge($aOption, [
+                                    'bx_if:show_option_group_begin' => [
+                                        'condition' => true,
+                                        'content' => [
+                                            'opt_label' => bx_process_output($sOptTitle['value'])
+                                        ]
+                                    ]
+                                ]);
+                                $aOptions[] = $aOption;
+                                break;
+
+                            case 'group_end':
+                                $aOption = array_merge($aOption, [
+                                    'bx_if:show_option_group_end' => [
+                                        'condition' => true,
+                                        'content' => []
+                                    ]
+                                ]);
+                                $aOptions[] = $aOption;
+                                break;
+                        }
+
+                        continue;
+                    }
+
+                    $aOptAttrs = $this->_genInputSelectOptionAttrs($sOptTitle);
+                    if(!empty($aOptAttrs))
+                        $sOptAttrs = bx_convert_array2attrs($aOptAttrs);
+
+                    $sOptValue = $sOptTitle['key'];
+                    $sOptTitle = $sOptTitle['value'];
+                }
+
+                $aOption = array_merge($aOption, [
+                    'bx_if:show_option' => [
+                        'condition' => true,
+                        'content' => [
+                            'opt_title' => bx_process_output($sOptTitle),
+                            'opt_value' => !empty($sOptValue) ? bx_html_attribute($sOptValue) : '',
+                            'opt_attrs' => $sOptAttrs,
+                            'bx_if:show_selected' => [
+                                'condition' => $this->$sIsSelectedFunc($sOptValue, $mixedCurrentVal),
+                                'content' => []
+                            ]
+                        ]
+                    ]
+                ]);
+
+                $aOptions[] = $aOption;
+            }
+        }
+
+        $aAttrs = $this->_genInputSelectAttrs($aInput, $isMultiple);
+
+        return $this->_parseInputSelect('form_field_select.html', [
+            'attrs' => bx_convert_array2attrs($aAttrs, "bx-def-font-inputs bx-form-input-{$aInput['type']}"),
+            'bx_repeat:options' => $aOptions,
+            'content' => !empty($aInput['content']) ? $aInput['content'] : ''
+        ]);
+    }
+
+    protected function _genInputSelectAttrs(&$aInput, $isMultiple)
+    {
+        $aAttrs = !empty($aInput['attrs']) ? $aInput['attrs'] : [];
+
+        $aAttrs['name'] = $aInput['name'];
+        if ($isMultiple) {
+            $aAttrs['name'] .= '[]';
+            $aAttrs['multiple'] = 'multiple';
+        }
+
+        // for inputs with labels generate id
+        if (isset($aInput['label']))
+            $aAttrs['id'] = $this->getInputId($aInput);
+        
+        return $aAttrs;
+    }
+
+    protected function _genInputSelectOptionAttrs(&$aOption)
+    {
+        $aAttrs = [];
+        if(!empty($aOption['attrs']) && is_array($aOption['attrs']))
+            $aAttrs = $aOption['attrs'];
+
+        if(isset($aOption['class']))
+            $aAttrs['class'] = !empty($aAttrs['class']) ? $aAttrs['class'] . ' ' . $aOption['class'] : $aOption['class'];
+
+        if(isset($aOption['style']))
+            $aAttrs['style'] = !empty($aAttrs['style']) ? $aAttrs['style'] . ' ' . $aOption['style'] : $aOption['style'];
+        
+        return $aAttrs;
+    }
+
+    protected function _parseInputSelect($sTmplName, $aTmplVars)
+    {
+        return $this->oTemplate->parseHtmlByName($sTmplName, $aTmplVars);
+    }
+
+    function _genInputsSet(&$aInput, $sType, $mixedCurrentVal, $sIsCheckedFunc, $sNameAppend = '')
+    {
+        $aAttrs = empty($aInput['attrs']) || 'radio_set' == $aInput['type'] || 'checkbox_set' == $aInput['type'] ? array() : $aInput['attrs'];
+
+        $aAttrs['name']  = $aInput['name'];
+
+        // for inputs with labels generate id
+        if (isset($aInput['label']))
+            $aAttrs['id'] = $this->getInputId($aInput);
+
+        $sAttrs = bx_convert_array2attrs($aAttrs, "bx-form-input-{$aInput['type']}");
+
+        // generate options
+        $sDivider = isset($aInput['dv']) ? $aInput['dv'] : $this->_sDivider;
+        $iDividerThreshold = !empty($aInput['dv_thd']) ? (int)$aInput['dv_thd'] : 3;
+
+        $sOptions = '';
+
+        $aAdditionalFieldInfo = [];
+        if (in_array($aInput['type'], ['radio_set', 'checkbox_set']) && !empty($aInput['values_list_name']))
+            $aAdditionalFieldInfo = BxDolFormQuery::getDataItems($aInput['values_list_name'], false, BX_DATA_VALUES_ALL);
+
+        if (isset($aInput['values']) && is_array($aInput['values'])) {
+            if (count($aInput['values']) > $iDividerThreshold && $sDivider == $this->_sDivider)
+                $sDivider = $this->_sDividerAlt;
+            
+            // generate complex input using simple standard inputs
+            foreach ($aInput['values'] as $sValue => $sLabel) {
+                if(is_array($sLabel)) {
+                    $sValue = $sLabel['key'];
+                    $sLabel = $sLabel['value'];
+                }
+
+                // create new simple input
+                $aNewInput = array(
+                    'type'    => $sType,
+                    'name'    => $aInput['name'] . $sNameAppend,
+                    'value'   => $sValue,
+                    'checked' => $this->$sIsCheckedFunc($sValue, $mixedCurrentVal),
+                    'label'   => $sLabel,
+                    'attrs'   => !empty($aInput['attrs']) && ('radio_set' == $aInput['type'] || 'checkbox_set' == $aInput['type']) ? $aInput['attrs'] : false,
+                );
+                
+                if (isset($aInput['label_as_html']))
+                    $aNewInput['label_as_html'] = $aInput['label_as_html'];
+
+                $sNewInput = $this->genInput($aNewInput);
+
+                // add additional info for the field
+                if (isset($aAdditionalFieldInfo[$sValue]) && !empty($aAdditionalFieldInfo[$sValue]['LKey2']))
+                    $sNewInput = $this->genFiledItemInfoWrapper($sNewInput, _t($aAdditionalFieldInfo[$sValue]['LKey2']));
+
+                // attach new input to complex
+                $sOptions .= ($sNewInput . $sDivider);
+            }
+        }
+
+        // generate element
+        $sCode = <<<BLAH
+            <div $sAttrs>
+                $sOptions
+            </div>
+BLAH;
+
+        return $sCode;
+    }
+
+    function genInputLocation(&$aInput)
+    {
+        $o = BxDolLocationField::getObjectInstance(getParam('sys_location_field_default'));
+        if (!$o)
+            return _t('_sys_txt_error_occured');
+
+        return $o->genInputLocation($aInput, $this);
+    }
+    
+    public function setLocationVals ($aInput, $aVals)
+    {
+        $o = BxDolLocationField::getObjectInstance(getParam('sys_location_field_default'));
+        if (!$o)
+            return false;
+
+        return $o->setLocationVals ($aInput, $aVals, $this);
+    }
+
+    public function setLocationVal ($aInput, $sIndex, $sVal)
+    {
+        $o = BxDolLocationField::getObjectInstance(getParam('sys_location_field_default'));
+        if (!$o)
+            return false;
+
+        return $o->setLocationVal ($aInput, $sIndex, $sVal, $this);
+    }
+
+    protected function getLocationVal ($aInput, $sIndex)
+    {
+        $o = BxDolLocationField::getObjectInstance(getParam('sys_location_field_default'));
+        if (!$o)
+            return false;
+
+        return $o->getLocationVal ($aInput, $sIndex, $this);
+    }
+
+    function genInputPassword(&$aInput)
+    {
+        return $this->oTemplate->parseHtmlByName('form_field_password.html', array('input' => $this->genInputStandard($aInput)));
+    }
+
+    function genInputPrice(&$aInput)
+    {
+        $sCurrency = '';
+        if(!isset($aInput['value_currency'])) {
+            $sCurrency = BxDolPayments::getInstance()->getOption('default_currency_code');
+            if(empty($sCurrency))
+                $sCurrency = getParam('currency_code');
+        }
+        else
+            $sCurrency = $aInput['value_currency'];
+
+        return $this->oTemplate->parseHtmlByName('form_field_price.html', array(
+            'bx_if:show_currency' => [
+                'condition' => !empty($sCurrency),
+                'content' => [
+                    'currency' => $sCurrency,
+                ]
+            ],
+            'input' => $this->genInputStandard($aInput)
+        ));
+    }
+
+    function genInputNestedForm(&$aInput)
+    {
+        return '';
+    }
+    
+    function genNestedForm(&$aInput)
+    {
+        return '';
+    }
+    
+    
+    function genInputCaptcha(&$aInput)
+    {
+        $aAttrs = empty($aInput['attrs']) ? array() : $aInput['attrs'];
+
+        // for inputs with labels generate id
+        if (isset($aInput['label']))
+            $aAttrs['id'] = $this->getInputId($aInput);
+
+        $sAttrs = bx_convert_array2attrs($aAttrs, "bx-form-input-{$aInput['type']}");
+
+        $oCaptcha = BxDolCaptcha::getObjectInstance();
+
+        return "<div $sAttrs>" . ($oCaptcha ? $oCaptcha->display($this->_bDynamicMode) : _t('_sys_txt_captcha_not_available')) . "</div>";
+    }
+
+    /**
+     * Generate Label Element
+     *
+     * @param  string $aInput   input array
+     * @return string HTML code
+     */
+    function genLabel(&$aInput)
+    {
+        if (!isset($aInput['label']) || empty($aInput['label']))
+            return '';
+
+        $sInputID = $this->getInputId($aInput);
+        
+        $sLabel = bx_process_output($aInput['label']);
+        if (isset($aInput['label_as_html']) && $aInput['label_as_html'] == true)
+            $sLabel = $aInput['label'];
+        
+        return '<label for="' . $sInputID . '">' . $sLabel . '</label>';
+    }
+
+    function genPrivacyGroupChooser(&$aInput, $sPrivacyObject = '')
+    {
+        if(!isset($aInput['privacy']) || empty($aInput['privacy']))
+            return '';
+
+        $iAuthorId = bx_get_logged_profile_id();
+        $mixedResult = checkActionModule($iAuthorId, 'set form fields privacy', 'system');
+        if($mixedResult[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return '';
+
+        if(empty($sPrivacyObject))
+            $sPrivacyObject = $this->_sPrivacyObjectView;
+
+        $iInputId = (int)$aInput['id'];
+        $mixedPrivacyGroup = $this->_getPrivacyGroup($sPrivacyObject, $iInputId, $iAuthorId);
+
+        if(!isset($aInput['attrs_wrapper']['class']))
+            $aInput['attrs_wrapper']['class'] = '';
+        $aInput['attrs_wrapper']['class'] .= ' bx-form-input-wrapper-pgc';
+
+        return $this->oTemplate->parseHtmlByName('form_field_privacy.html', array(
+            'js_object' => $this->getJsObjectName(),
+            'html_id' => $this->_aHtmlIds['pgc'] . $iInputId,
+            'id' => $iInputId,
+            'privacy_object' => $sPrivacyObject,
+            'icon' => $this->_getPrivacyIcon($mixedPrivacyGroup)
+        ));
+    }
+
+    function genFiledItemInfoWrapper($sInput, $sInfoValue)
+    {
+        $sInfo = $this->genInfoIcon($sInfoValue) ;
+        return $this->oTemplate->parseHtmlByName('form_field_option_info.html', [
+            'input' => $sInput,
+            'info' => $sInfo
+        ]);
+    }
+
+    function genInfoIcon($sInfo)
+    {
+        return '<div class="bx-form-info bx-def-font-grayed bx-def-font-small">' . bx_process_output($sInfo, BX_DATA_HTML) . '</div>';
+    }
+
+    function genErrorIcon( $sError = '' )
+    {
+        if ($this->bEnableErrorIcon) {
+            $sStyle = '';
+            if (!$sError)
+                $sStyle = 'style="display:none;"';
+            return '<div class="bx-form-warn" ' . $sStyle . '>' . $sError . '</div>';
+        }
+    }
+
+    function getOpenSection($aAttrs = array(), $sTitle = '', $aWrapperAttrs = [])
+    {
+        if (!$this->_isSectionOpened) {
+
+            if (!$aAttrs || !is_array($aAttrs))
+                $aAttrs = array();
+
+            if ($sTitle)
+                $sClassesAdd = "bx-form-section-header";
+            else
+                $sClassesAdd = "bx-form-section-divider";
+
+            $sAttrs = bx_convert_array2attrs($aAttrs, "bx-form-section bx-def-padding-sec-top bx-def-border-top " . $sClassesAdd);
+
+            $sWrapperAttrs = bx_convert_array2attrs($aWrapperAttrs, "bx-form-section-wrapper bx-def-margin-top");
+
+            $this->_isSectionOpened = true;
+
+            return "<!-- form header content begins -->\n <div $sWrapperAttrs> <div $sAttrs> $sTitle <div class=\"bx-form-section-content bx-def-padding-top bx-def-padding-bottom" . ($sTitle ? ' bx-def-padding-left bx-def-padding-right' : '') . "\">\n";
+
+        } else {
+
+            return '';
+        }
+    }
+
+    function getCloseSection()
+    {
+        if ($this->_isSectionOpened) {
+
+            $this->_isSectionOpened = false;
+            return "</div> </div> </div> \n<!-- form header content ends -->\n";
+
+        } else {
+
+            return '';
+        }
+    }
+
+    function getOpenSectionViewMode($aAttrs = array(), $sTitle = '', $aWrapperAttrs = [])
+    {
+        if (!$this->_isSectionOpened) {
+
+            if (!$aAttrs || !is_array($aAttrs))
+                $aAttrs = array();
+
+            if ($sTitle)
+                $sClassesAdd = "bx-form-section-header";
+            else
+                $sClassesAdd = "bx-form-section-divider";
+
+            $sAttrs = bx_convert_array2attrs($aAttrs, "bx-form-section bx-form-view-section bx-def-padding-sec-top bx-def-border-top " . $sClassesAdd);
+
+            $sWrapperAttrs = bx_convert_array2attrs($aWrapperAttrs, "bx-form-section-wrapper bx-def-margin-top");
+
+            $this->_isSectionOpened = true;
+
+            return "<!-- form header content begins -->\n <div $sWrapperAttrs> <div $sAttrs> $sTitle <div class=\"bx-form-section-content bx-def-padding-top\">\n";
+
+        } else {
+
+            return '';
+        }
+    }
+
+    function getCloseSectionViewMode()
+    {
+        if ($this->_isSectionOpened) {
+
+            $this->_isSectionOpened = false;
+            return "</div> </div> </div> \n<!-- form header content ends -->\n";
+
+        } else {
+
+            return '';
+        }
+    }
+
+    public static function getJsCalendarLangs ()
+    {
+        return array ('ar-dz' => 1, 'ar' => 1, 'at' => 1, 'az' => 1, 'be' => 1, 'bg' => 1, 'bn' => 1, 'bs' => 1, 'cat' => 1, 'ckb' => 1, 'cs' => 1, 'cy' => 1, 'da' => 1, 'de' => 1, 'default' => 1, 'eo' => 1, 'es' => 1, 'et' => 1, 'fa' => 1, 'fi' => 1, 'fo' => 1, 'fr' => 1, 'ga' => 1, 'gr' => 1, 'he' => 1, 'hi' => 1, 'hr' => 1, 'hu' => 1, 'hy' => 1, 'id' => 1, 'index' => 1, 'is' => 1, 'it' => 1, 'ja' => 1, 'ka' => 1, 'km' => 1, 'ko' => 1, 'kz' => 1, 'lt' => 1, 'lv' => 1, 'mk' => 1, 'mn' => 1, 'ms' => 1, 'my' => 1, 'nl' => 1, 'nn' => 1, 'no' => 1, 'pa' => 1, 'pl' => 1, 'pt' => 1, 'ro' => 1, 'ru' => 1, 'si' => 1, 'sk' => 1, 'sl' => 1, 'sq' => 1, 'sr-cyr' => 1, 'sr' => 1, 'sv' => 1, 'th' => 1, 'tr' => 1, 'uk' => 1, 'uz' => 1, 'uz_latn' => 1, 'vn' => 1, 'zh-tw' => 1, 'zh' => 1);
+    }
+    
+    public static function getCssJsCalendar()
+    {
+        $aLangs = self::getJsCalendarLangs();
+        $sLang = BxDolLanguages::getInstance()->detectLanguageFromArray($aLangs);
+
+        return [[
+                BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'flatpickr/dist/|flatpickr.min.css'
+            ], [
+                'flatpickr/dist/flatpickr.min.js',
+                'flatpickr/dist/l10n/' . $sLang . '.js',
+            ]
+        ];
+    }
+    
+    function addCssJsUi ()
+    {
+        if (self::$_isCssJsUiAdded)
+            return;
+
+        $this->_addJs(array(
+            'jquery-ui/jquery-ui.min.js',
+        ), "'undefined' === typeof(jQuery.ui.position)");
+        
+        $this->_addCss('jquery-ui/jquery-ui.min.css');
+
+        self::$_isCssJsUiAdded = true;
+    }
+    
+    function addCssJsUiSortable ()
+    {
+        if (self::$_isCssJsUiSortableAdded)
+            return;
+
+        $this->_addJs(array(
+            'jquery-ui/jquery-ui.min.js'
+        ), "'undefined' === typeof(jQuery.ui.position)");
+
+        self::$_isCssJsUiSortableAdded = true;
+    }
+
+    function addCssJsTimepicker ()
+    {
+        if (self::$_isCssJsTimepickerAdded)
+            return; 
+
+        list($aCss, $aJs) = self::getCssJsCalendar();
+
+        $this->_addCss($aCss);
+        $this->_addJs($aJs, "'undefined' === typeof(flatpickr)");        
+
+        self::$_isCssJsTimepickerAdded = true;
+    }
+
+    function addCssJsMinicolors ()
+    {
+        if (self::$_isCssJsMinicolorsAdded)
+            return;       
+
+        $this->_addCss(BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'jquery-minicolors/|jquery.minicolors.css');
+        $this->_addJs('jquery-minicolors/jquery.minicolors.min.js', "'undefined' === typeof($.minicolors)");
+
+        self::$_isCssJsMinicolorsAdded = true;
+    }
+
+    function addCssJsViewMode ()
+    {
+        if (self::$_isCssJsAddedViewMode)
+            return;
+
+        $this->_addCss('forms.css');
+
+        self::$_isCssJsAddedViewMode = true;
+    }
+
+    function addCssJsCore ()
+    {
+        if (self::$_isCssJsAdded)
+            return;
+
+        $this->_addCss('forms.css');
+        $this->_addJs('jquery.webForms.js', "'undefined' === typeof($.fn.addWebForms)");
+        $this->_addJs(array('BxDolForm.js', 'BxDolNestedForm.js'), "true");
+
+        self::$_isCssJsAdded = true;
+    }
+
+    function addCssJs ()
+    {
+        if (isset($this->aParams['view_mode']) && $this->aParams['view_mode']) {
+
+            $this->addCssJsViewMode ();
+
+        } else { 
+
+            $this->addCssJsCore();            
+        }
+    }
+
+    protected function _processCssJs()
+    {
+        $sRet = '';
+        if ($this->_bDynamicMode) {
+            $sRet .= $this->oTemplate->addCss($this->_aCss, true);
+
+
+            $sRet .= "\n<script>\n";
+            $sRet .= "\nglJsLoadOnaddWebForms = [];\n";
+            foreach ($this->_aJs as $sCondition => $aJs) {
+                $sJs = $this->oTemplate->addJs($aJs, true);
+                if (!$sJs)
+                    continue;
+                
+                $sRet .= "if ($sCondition)\n";
+                if (preg_match_all("/src=\"(.*?)\"/", $sJs, $aMatches))
+                    $sRet .= "\tglJsLoadOnaddWebForms = glJsLoadOnaddWebForms.concat(" . json_encode($aMatches[1]) . ");\n";
+            }
+            $sRet .= "\n</script>\n";
+        }
+        else {
+            $this->oTemplate->addCss($this->_aCss);
+            
+            $aJs = array();
+            foreach ($this->_aJs as $sCondition => $a)
+                $aJs = array_merge($aJs, $a);
+            $this->oTemplate->addJs($aJs);
+        }
+        return $sRet;
+    }
+
+    protected function _addJs($mixed, $sJsCondition)
+    {
+        if (!is_array($mixed))
+            $mixed = array($mixed);
+        $this->_aJs[$sJsCondition] = $mixed;
+    }
+
+    protected function _addCss($mixed)
+    {
+        if (!is_array($mixed))
+            $mixed = array($mixed);
+        foreach ($mixed as $s)
+            $this->_aCss[$s] = $s;
+    }
+}
+
+/** @} */

+ 934 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBaseGrid.php

@@ -0,0 +1,934 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * Grid representation.
+ * @see BxDolGrid
+ */
+class BxBaseGrid extends BxDolGrid
+{
+    protected $_oTemplate;
+    protected $_aPopupOptions = false;
+    protected $_aQueryAppend = [];
+    protected $_aQueryAppendExclude = false; // an array of keys which shouldn't be pathed in http requests, but can be stored (used) in 'Query Append' array.
+    protected $_aQueryAppendExcludeApi = false;
+    protected $_aQueryReset = false;
+    protected $_aConfirmMessages = false;
+    protected $_bSelectAll = false;
+    protected $_isDisplayPopupOnTextOverflow = true;
+
+    public function __construct ($aOptions, $oTemplate)
+    {
+        parent::__construct ($aOptions);
+
+        if ($oTemplate)
+            $this->_oTemplate = $oTemplate;
+        else
+            $this->_oTemplate = BxDolTemplate::getInstance();
+
+        $this->_aPopupOptions = [];
+
+        $this->_aQueryAppend = [
+            $this->_oTemplate->getCodeKey() => $this->_oTemplate->getCode()
+        ];
+        $this->_aQueryAppendExclude = [];
+        $this->_aQueryAppendExcludeApi = [$this->_oTemplate->getCodeKey()];
+
+        $this->_aQueryReset = [
+            $this->_aOptions['filter_get'], 
+            $this->_aOptions['order_get_field'], 
+            $this->_aOptions['order_get_dir'], 
+            $this->_aOptions['paginate_get_start'], 
+            $this->_aOptions['paginate_get_per_page']
+        ];
+
+        $this->_aConfirmMessages = [];
+    }
+
+    public function performActionDisplay()
+    {
+        if($this->_bIsApi)
+            return $this->getCodeAPI();
+
+        require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+
+        echoJson(array(
+            'grid' => $this->getCode(false), 
+            'total_count' => $this->_iTotalCount,
+            'total_count_f' => $this->_getCounter()
+        ));
+    }
+
+    public function performActionReorder()
+    {
+        $this->_replaceMarkers ();
+
+        $aOrder = bx_get($this->_sObject . '_row');
+        $iOrder = 0;
+        foreach ($aOrder as $mixedId)
+            $this->_updateOrder($mixedId, ++$iOrder);
+
+        echoJson(array());
+    }
+
+    public function performActionDelete()
+    {
+        $this->_replaceMarkers ();
+
+        $aResult = [];
+        $iAffected = 0;
+        $aIds = bx_get('ids');
+        if (!$aIds || !is_array($aIds))
+            return $this->_getActionResult($aResult);
+
+        foreach ($aIds as $mixedId)
+            $iAffected += $this->_delete($mixedId) ? 1 : 0;
+
+        if($iAffected)
+            $aResult = !$this->_bIsApi ? ['grid' => $this->getCode(false)] : [];
+        else
+            $aResult = ['msg' => _t("_sys_grid_delete_failed")];
+
+        return $this->_getActionResult($aResult);
+    }
+
+    public function performActionEnable($mixedChecked = null)
+    {
+        $this->_replaceMarkers ();
+
+        $aIds = bx_get('ids');
+        if (!$aIds || !is_array($aIds)) {
+            echoJson(array());
+            exit;
+        }
+
+        $iChecked = (int)($mixedChecked !== null ? $mixedChecked : bx_get('checked'));
+        
+        $aAffectedIds = array ();
+        foreach ($aIds as $mixedId)
+            if ($this->_enable($mixedId, $iChecked))
+                $aAffectedIds[] = preg_match("/^[\d\w]+$/", $mixedId) ? $mixedId : (int)$mixedId;
+
+        $sAction = $iChecked ? 'enable' : 'disable';
+
+        if($this->_bIsApi)
+            return $aAffectedIds;
+
+        echo echoJson(array($sAction => $aAffectedIds));
+    }
+
+    public function getCode ($isDisplayHeader = true)
+    {
+        $this->_replaceMarkers ();
+
+        if ($isDisplayHeader && empty($this->_aOptions['paginate_url'])) {
+            // reset page query params if grid is just initialized and it uses AJAX paginate
+            $this->resetQueryParams();
+        }
+
+        $sPaginate = '';
+        $aData = array();
+
+        $sIdWrapper = 'bx-grid-wrap-' . $this->_sObject;
+        $sIdContainer = 'bx-grid-cont-' . $this->_sObject;
+        $sIdTable = 'bx-grid-table-' . $this->_sObject;
+
+        $sFilter = bx_unicode_urldecode(bx_process_input(bx_get($this->_aOptions['filter_get'])));
+        $sOrderField = bx_unicode_urldecode(bx_process_input(bx_get($this->_aOptions['order_get_field'])));
+        $sOrderDir = 0 === strcasecmp('desc', bx_get($this->_aOptions['order_get_dir'])) ? 'DESC' : 'ASC';
+
+        $iStart = 0;
+        if($this->_aOptions['paginate_get_start'] && ($iStartGet = (int)bx_get($this->_aOptions['paginate_get_start'])) >= 0)
+            $iStart = $iStartGet;
+
+        $iPerPage = 10;
+        if($this->_aOptions['paginate_get_per_page'] && ($iPerPageGet = (int)bx_get($this->_aOptions['paginate_get_per_page'])) > 0)
+            $iPerPage = $iPerPageGet;
+        else if($this->_aOptions['paginate_per_page'])
+            $iPerPage = (int)$this->_aOptions['paginate_per_page'];            
+
+        if ($this->_aOptions['paginate_get_start']) {
+
+            $aData = $this->_getData ($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage + 1);
+
+            $sPageUrl = false;
+            if (!empty($this->_aOptions['paginate_url'])) {
+
+                $sPageUrl = $this->_aOptions['paginate_url'];
+
+                $aParamsAppend = array();
+                if ($sFilter) {
+                    $aParamsAppend['filter'] = bx_process_input(bx_get($this->_aOptions['filter_get']));
+                }
+                if ($sOrderField) {
+                    $aParamsAppend['order_field'] = bx_process_input(bx_get($this->_aOptions['order_get_field']));
+                    $aParamsAppend['order_dir'] = bx_process_input(bx_get($this->_aOptions['order_get_dir']));
+                }
+                if ($aParamsAppend)
+                    $sPageUrl = bx_append_url_params($sPageUrl, $aParamsAppend);
+            }
+
+            $aPaginateParams = array(
+                'start' => $iStart,
+                'per_page' => $iPerPage,
+                'page_url' =>  $sPageUrl ? $sPageUrl : "javascript:glGrids." . $this->_sObject . ".reload('{start}'); void(0);",
+            );
+
+            $oPaginate = new BxTemplPaginate($aPaginateParams, $this->_oTemplate);
+            $oPaginate->setNumFromDataArray($aData);
+
+            if (isset($this->_aOptions['paginate_simple']) && false !== $this->_aOptions['paginate_simple'])
+                $sPaginate = $oPaginate->getSimplePaginate($this->_aOptions['paginate_simple']);
+            else
+                $sPaginate = $oPaginate->getPaginate();
+        } else {
+            $aData = $this->_getData ($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage);
+        }
+
+        if((empty($aData) || !is_array($aData)) && isset($this->_aBrowseParams['empty_message']) && !(bool)$this->_aBrowseParams['empty_message'])
+            return '';
+        
+        $sPopupOptions = '{}';
+        if (!empty($this->_aPopupOptions) && is_array($this->_aPopupOptions))
+            $sPopupOptions = json_encode($this->_aPopupOptions);
+
+        $aQueryAppend = array_merge(is_array($this->_aQueryAppend) ? $this->_aQueryAppend : array(), is_array($this->_aMarkers) ? $this->_aMarkers : array());
+        if(!empty($this->_aQueryAppendExclude) && is_array($this->_aQueryAppendExclude))
+            $aQueryAppend = array_diff_key($aQueryAppend, array_flip($this->_aQueryAppendExclude));
+
+        $sQueryAppend = '{}';
+        if (!empty($aQueryAppend) && is_array($aQueryAppend))
+            $sQueryAppend = json_encode($aQueryAppend);
+
+        $sConfirmMessages = '{}';
+        if (!empty($this->_aConfirmMessages) && is_array($this->_aConfirmMessages))
+            $sConfirmMessages = json_encode($this->_aConfirmMessages);
+
+        $iColumns = count($this->_aOptions['fields']);
+        $aVarsHead = $this->_getRowHead();
+
+        $sClassHeader = '';
+        if($this->_aOptions['show_total_count'] == 0)
+            $sClassHeader .= ' bx-gh-no-counter';
+
+        $aVars = array (
+            'object' => $this->_sObject,
+            'csrf_token' => BxDolForm::getCsrfToken(),
+            'id_table' => $sIdTable,
+            'id_cont' => $sIdContainer,
+            'id_wrap' => $sIdWrapper,
+            'class_table_wrapper' => $this->_aOptions['responsive'] ? 'bx-grid-table-wrapper-responsive' : '',
+            'sortable' => empty($this->_aOptions['field_order']) ? 0 : 1,
+            'sorting' => empty($this->_aOptions['sorting_fields']) ? 0 : 1,
+            'sorting_field' => $sOrderField,
+            'sorting_dir' => $sOrderDir,
+        	'bx_if:display_head' => array(
+        		'condition' => !empty($aVarsHead),
+        		'content' => array(
+        			'bx_repeat:row_header' => $aVarsHead,
+        			'columns' => $iColumns,
+        		)
+        	),
+            'bx_repeat:rows_data' => $this->_getRowsDataDesign ($aData),
+            'paginate_get_start' => $this->_aOptions['paginate_get_start'],
+            'paginate_get_per_page' => $this->_aOptions['paginate_get_per_page'],
+            'start' => $iStart,
+            'per_page' => $iPerPage,
+            'filter' => bx_js_string($sFilter, BX_ESCAPE_STR_APOS),
+            'filter_get' => bx_js_string($this->_aOptions['filter_get']),
+            'order_field' => bx_js_string($sOrderField, BX_ESCAPE_STR_APOS),
+            'order_dir' => bx_js_string($sOrderDir, BX_ESCAPE_STR_APOS),
+            'order_get_field' => bx_js_string($this->_aOptions['order_get_field']),
+            'order_get_dir' => bx_js_string($this->_aOptions['order_get_dir']),
+            'popup_options' => $sPopupOptions,
+            'query_append' => $sQueryAppend,
+            'confirm_messages' => $sConfirmMessages,
+            'columns' => $iColumns,
+            'bx_if:display_footer' => array(
+        		'condition' => !empty($this->_aOptions['actions_bulk']) || !empty($sPaginate),
+        		'content' => array(
+		            'bx_if:actions_bulk' => array (
+		                'condition' => !empty($this->_aOptions['actions_bulk']),
+		                'content' => array(
+		                    'actions_bulk' => $this->_getActions ('bulk'),
+		                ),
+		            ),
+		            'paginate' => $sPaginate,
+				)
+			),
+            'bx_if:display_header' => array (
+                'condition' => $isDisplayHeader,
+                'content' => array (
+                    'class' => $sClassHeader,
+                    'bx_if:actions_independent' => array (
+                        'condition' => !empty($this->_aOptions['actions_independent']),
+                        'content' => array(
+                            'actions_independent' => $this->_getActions ('independent'),
+                        ),
+                    ),
+                    'bx_if:filter' => array (
+                        'condition' => !empty($this->_aOptions['filter_fields']) || !empty($this->_aOptions['filter_fields_translatable']),
+                        'content' => array(
+                            'controls' => $this->_getFilterControls(),
+                        ),
+                    ),
+                    'bx_if:counter' => array (
+                        'condition' => $this->_aOptions['show_total_count'] == 1,
+                        'content' => array(
+                            'counter' => $this->_getCounter(),
+                        ),
+                    ),
+                ),
+            ),
+        );
+
+        $this->_addJsCss();
+
+        return $this->_oTemplate->parseHtmlByName('grid.html', $aVars);
+    }
+
+    /**
+     * Get grid code API.
+     * @return array
+     */
+    public function getCodeAPI()
+    {
+        $this->_replaceMarkers();
+
+        $sFilter = bx_unicode_urldecode(bx_process_input(bx_get($this->_aOptions['filter_get'])));
+        $sOrderField = bx_unicode_urldecode(bx_process_input(bx_get($this->_aOptions['order_get_field'])));
+        $sOrderDir = 0 === strcasecmp('desc', bx_get($this->_aOptions['order_get_dir'])) ? 'DESC' : 'ASC';
+
+        $iStart = 0;
+        if($this->_aOptions['paginate_get_start'] && ($iStartGet = (int)bx_get($this->_aOptions['paginate_get_start'])) >= 0)
+            $iStart = $iStartGet;
+
+        $iPerPage = 10;
+        if($this->_aOptions['paginate_get_per_page'] && ($iPerPageGet = (int)bx_get($this->_aOptions['paginate_get_per_page'])) > 0)
+            $iPerPage = $iPerPageGet;
+        else if($this->_aOptions['paginate_per_page'])
+            $iPerPage = (int)$this->_aOptions['paginate_per_page']; 
+
+        $aData = $this->_getData($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage);
+        if(!empty($aData) && is_array($aData))
+            $aData = $this->decodeDataAPI($aData);
+
+        $aQueryAppend = [];
+        if(!empty($this->_aQueryAppend) && is_array($this->_aQueryAppend))
+            $aQueryAppend = array_merge($aQueryAppend, $this->_aQueryAppend);
+        if(!empty($this->_aMarkers) && is_array($this->_aMarkers))
+            $aQueryAppend = array_merge($aQueryAppend, $this->_aMarkers);
+        if(!empty($this->_aQueryAppendExclude) && is_array($this->_aQueryAppendExclude))
+            $aQueryAppend = array_diff_key($aQueryAppend, array_flip($this->_aQueryAppendExclude));
+        if(!empty($this->_aQueryAppendExcludeApi) && is_array($this->_aQueryAppendExcludeApi))
+            $aQueryAppend = array_diff_key($aQueryAppend, array_flip($this->_aQueryAppendExcludeApi));       
+
+        return [
+            'settings' => [
+                'object' => $this->_aOptions['object'], 
+                'field_id' => $this->_aOptions['field_id'], 
+                'start' => $iStart, 
+                'per_page' => $iPerPage,
+                'filters' => $this->_getFilterControlsAPI(),
+                'query_append' => $aQueryAppend
+            ],
+            'header' => $this->_getRowHeadAPI(),
+            'data' => defined('BX_API_PAGE') ? [] : $aData,
+            'actions' => [
+                'independent' => $this->_getActionsAPI('independent'), 
+                'bulk' => $this->_getActionsAPI('bulk')
+            ]
+        ];
+    }
+
+    public function decodeDataAPI($aData)
+    {
+        $aDataRv = [];
+        foreach($aData as $iKey => $aRow) {
+            $aDataRv[$iKey] = [];
+            foreach($this->_aOptions['fields'] as $sKey => $aField){
+                $aDataRv[$iKey]['id'] = $aData[$iKey]['id'];
+                $sMethod = '_getCellDefault';
+                $sCustomMethod = '_getCell' . $this->_genMethodName($sKey);
+               
+                if (method_exists($this, $sCustomMethod)){
+                    $sMethod = $sCustomMethod;
+                    $s = $this->$sMethod($aData[$iKey][$sKey], $sKey, $aField, $aRow);
+                    $aDataRv[$iKey][$sKey] = $s;
+                }
+                else{
+                    $aDataRv[$iKey][$sKey] = ['type' => 'text', 'value'=> $aData[$iKey][$sKey]];    
+                }
+               
+            }
+        }
+        return $aDataRv;
+    }
+    
+    public function getFormBlockAPI($oForm, $sAction, $iId = 0)
+    {
+        return [
+            bx_api_get_block('form', $oForm->getCodeAPI(), [
+                'ext' => [
+                    'name' => $this->_oModule->getName(), 
+                    'title' => $this->getFormBlockTitleAPI($sAction, $iId),
+                    'request' => ['url' => $this->getFormCallBackUrlAPI($sAction, $iId), 'immutable' => true]]
+                ]
+            )
+        ];
+    }
+
+    public function getFormBlockTitleAPI($sAction, $iId)
+    {
+        return '';
+    }
+
+    public function getFormCallBackUrlAPI($sAction, $iId)
+    {
+        return '';
+    }
+
+    /**
+     * Reset query params, like filter and page number
+     */
+    public function resetQueryParams()
+    {
+        foreach ($this->_aQueryReset as $sKey) {
+            unset($_GET[$sKey]);
+            unset($_POST[$sKey]);
+        }
+    }
+
+    protected function _getRowHead()
+    {
+        $aRet = array();
+        foreach ($this->_aOptions['fields'] as $sKey => $a) {
+
+            $sMethod = '_getCellHeaderDefault';
+            $sCustomMethod = '_getCellHeader' . $this->_genMethodName($sKey);
+            if (method_exists($this, $sCustomMethod))
+                $sMethod = $sCustomMethod;
+
+            $aRet[] = array('header_cell' => $this->$sMethod($sKey, $a));
+        }
+        return $aRet;
+    }
+
+    protected function _getRowHeadAPI()
+    {
+        $aHeader = [];
+
+        foreach($this->_aOptions['fields'] as $sKey => $aField) {
+            $sMethod = '_getCellHeader' . $this->_genMethodName($sKey);
+            if(method_exists($this, $sMethod))
+                $aHeader[] = $this->$sMethod($sKey, $aField);
+            else
+                $aHeader[] = [
+                    'name' => bx_process_output($aField['name']),
+                    'title' => bx_process_output($aField['title']),
+                    'width' => $aField['width']
+                ];
+        }
+
+        return $aHeader;
+    }
+
+    protected function _getCellHeaderDefault ($sKey, $aField)
+    {
+        $sHeader = bx_process_output($aField['title']);
+
+        if (!empty($this->_aOptions['sorting_fields']) && is_array($this->_aOptions['sorting_fields']) && in_array($sKey, $this->_aOptions['sorting_fields'])) {
+            $sHeader = '<a href="javascript:void(0);" class="bx-grid-sort-handle">' . $sHeader . '</a><span class="bx-grid-sort-indi"></span>';
+            $aField['attr_head']['bx_grid_sort_head'] = $sKey;
+        }
+
+        $sAttr = $this->_convertAttrs(
+            $aField, 'attr_head',
+            'bx-def-padding-sec-bottom bx-def-padding-sec-top bx-grid-header-' . $sKey, // add default classes
+            isset($aField['width']) ? 'width:' . $aField['width'] : false // add default styles
+        );
+
+        return $this->_getCellHeaderWrapper ($sKey, $aField, $sHeader, $sAttr);
+    }
+
+    protected function _getCellHeaderCheckbox ($sKey, $aField)
+    {
+        if($this->_bIsApi){
+            return [
+                'name' => bx_process_output($aField['name']),
+                'title' => bx_process_output($aField['title']),
+                'width' => $aField['width']
+            ];   
+        }
+        
+    	$aAttr = array(
+    		'type' => 'checkbox',
+    		'id' => $this->_sObject . '_check_all',
+    		'name' => $this->_sObject . '_check_all',
+    		'onclick' => "$('input[name=" . $this->_sObject . "_check]:not([disabled])').attr('checked', this.checked)"
+    	);
+    	if($this->_bSelectAll)
+    		$aAttr['checked'] = 'checked';
+
+    	$aField['attr'] = isset($aField['attr']) && is_array($aField['attr']) ? array_merge($aAttr, $aField['attr']) : $aAttr;
+
+        $sAttrHead = $this->_convertAttrs(
+            $aField, 'attr_head',
+            'bx-def-padding-sec-bottom bx-def-padding-sec-top bx-grid-header-' . $sKey, // add default classes
+            isset($aField['width']) ? 'width:' . $aField['width'] : false // add default styles
+        );
+        return $this->_getCellHeaderWrapper ($sKey, $aField, ' <input ' . $this->_convertAttrs($aField, 'attr') . ' /> ', $sAttrHead);
+    }
+
+    protected function _getCellHeaderWrapper ($sKey, $aField, $sHeader, $sAttr)
+    {
+        return '<th ' . $sAttr . '>' . $sHeader . '</th>';
+    }
+
+    /**
+     * Check if the whole row is disabled.
+     * When row is disabled - checkbox is not selectable, actions aren't clickable and text is grayed out.
+     * By default all rows aren't disabled.
+     * @param $aRow row array
+     * @return boolean
+     */
+    protected function _isRowDisabled($aRow)
+    {
+        if (isset($aRow[$this->_aOptions['field_active']]) && !$this->_switcherState2Checked($aRow[$this->_aOptions['field_active']]))
+            return true;
+        return false;
+    }
+
+    /**
+     * Determine how actions are disabled when whole row is disabled.
+     * @param $aRow row array
+     * @return null - disable/enable actions when row is disabled/enabled, true - actions are always disabled, false - actions are always enabled
+     */
+    protected function _getActionsDisabledBehavior($aRow)
+    {
+        return null;
+    }
+
+    /**
+     * Check if the checkbox is disabled.
+     * @param $aRow row array
+     * @return boolean
+     */
+    protected function _isCheckboxDisabled($aRow)
+    {
+        return $this->_isRowDisabled($aRow);
+    }
+
+    /**
+     * Is checkbox checked by default ?
+     * By default no one checkbox is selected.
+     * @return boolean
+     */
+    protected function _isCheckboxSelected($mixedValue, $sKey, $aField, $aRow)
+    {
+        return $this->_bSelectAll;
+    }
+
+    /**
+     * Is switcher on by default ?
+     * By default no one switcher is on.
+     * @return boolean
+     */
+    protected function _isSwitcherOn($mixedValue, $sKey, $aField, $aRow)
+    {
+        return $this->_switcherState2Checked($mixedValue);
+    }
+
+    /**
+     * Convert switcher checked status to the actual value ?
+     * @return boolean
+     */
+    protected function _switcherChecked2State($isChecked)
+    {
+        return $isChecked ? 1 : 0;
+    }
+
+    /**
+     * Convert switcher value to checked(boolean) value ?
+     * @return boolean
+     */
+    protected function _switcherState2Checked($mixedState)
+    {
+        return $mixedState ? true : false;
+    }
+
+    protected function _getRowsDataDesign (&$aData)
+    {
+        $aGrid = array();
+
+        if(empty($aData))
+            $aGrid[] = array(
+                'id_row' => 0,
+                'row_class' => 'bx-grid-table-row-empty',
+                'row' => '<td class="bx-def-padding-sec-topbottom" colspan="' . count($this->_aOptions['fields']) . '">' . MsgBox(_t('_Empty')) . '</td>'
+            );
+        else
+            foreach ($aData as $aRow)
+                $aGrid[] = array(
+                    'id_row' => $this->_getRowId($aRow),
+                    'row_class' => $this->_isRowDisabled($aRow) ? 'bx-grid-table-row-disabled bx-def-font-grayed' : '',
+                    'row' => $this->_getRowDesign($aRow)
+                );
+
+        return $aGrid;
+    }
+
+    protected function _getRowId($mixedRow)
+    {
+        if(is_string($mixedRow))
+            $sId = rand(1, getrandmax());
+        else
+            $sId = $mixedRow[$this->_aOptions['field_id']];
+
+        return $this->_sObject . '_row_' . $sId;
+    }
+
+    protected function _getRowDesign($mixedRow)
+    {
+        $sRow = '';
+        if(is_string($mixedRow))
+            $sRow = '<td class="bx-def-padding-sec-topbottom bx-def-font-bold" colspan="' . count($this->_aOptions['fields']) . '">' . $mixedRow . '</td>';
+        else
+            foreach($this->_aOptions['fields'] as $sKey => $aField)
+                $sRow .= $this->_getCellDesign($sKey, $aField, $mixedRow);
+
+        return $sRow;
+    }
+
+    protected function _getCellDesign($sKey, $aField, $aRow)
+    {
+        $sMethod = '_getCellDefault';
+        if ($this->_aOptions['field_order'] == $sKey)
+            $sMethod = '_getCellOrder';
+
+        $sCustomMethod = '_getCell' . $this->_genMethodName($sKey);
+        if (method_exists($this, $sCustomMethod))
+            $sMethod = $sCustomMethod;
+
+        $mixedValue = $this->_getCellData($sKey, $aField, $aRow);
+
+        if ($aField['translatable'])
+            $mixedValue = _t($mixedValue);
+
+        $mixedValue = $this->_limitMaxLength($mixedValue, $sKey, $aField, $aRow, $this->_isDisplayPopupOnTextOverflow);
+
+        return $this->$sMethod($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellCheckbox ($mixedValue, $sKey, $aField, $aRow)
+    {
+        if($this->_bIsApi)
+            return ['type' => 'checkbox', 'data' => $aRow[$this->_aOptions['field_id']]];
+
+        $sAttr = $this->_convertAttrs(
+            $aField, 'attr_cell',
+            'bx-def-padding-sec-bottom bx-def-padding-sec-top', // add default classes
+            isset($aField['width']) ? 'width:' . $aField['width'] : false  // add default styles
+        );
+        $sDisabled = ($this->_isCheckboxDisabled($aRow) ? 'disabled="disabled"' : '');
+        $sSelected = ($this->_isCheckboxSelected($mixedValue, $sKey, $aField, $aRow) ? 'checked="checked"' : '');
+        $sVal = $aRow[$this->_aOptions['field_id']];
+        return '<td ' . $sAttr . '><input type="checkbox" name="'. $this->_sObject . '_check" value="' . $sVal . '" ' . $sDisabled . ' ' . $sSelected . '/></td>';
+    }
+
+    protected function _getCellSwitcher ($mixedValue, $sKey, $aField, $aRow)
+    {
+        if($this->_bIsApi)
+            return ['type' => 'switcher', 'data' => $aRow[$this->_aOptions['field_active']], 'fld' => $this->_aOptions['field_active']];
+
+        $sAttr = $this->_convertAttrs(
+            $aField, 'attr_cell',
+            'bx-def-padding-sec-bottom bx-def-padding-sec-top', // add default classes
+            isset($aField['width']) ? 'width:' . $aField['width'] : false  // add default styles
+        );
+
+        $oForm = new BxTemplFormView(array(), $this->_oTemplate);
+        $oForm->addCssJs();
+        $aInput = array(
+            'type' => 'switcher',
+            'name' => $this->_sObject . '_switch_' . $aRow[$this->_aOptions['field_id']],
+            'caption' => '',
+            'attrs' => array (
+                'bx_grid_action_single' => 'enable',
+                'bx_grid_action_confirm' => '',
+                'bx_grid_action_data' => $aRow[$this->_aOptions['field_id']],
+            ),
+            'value' => $aRow[$this->_aOptions['field_id']],
+            'checked' => $this->_isSwitcherOn(isset($aRow[$this->_aOptions['field_active']]) ? $aRow[$this->_aOptions['field_active']] : false, $sKey, $aField, $aRow),
+        );
+        $sSwitcher = $oForm->genInput($aInput);
+        return '<td ' . $sAttr . '>' . $sSwitcher . '</td>';
+    }
+
+    protected function _getCellOrder ($mixedValue, $sKey, $aField, $aRow)
+    {
+        if($this->_bIsApi)
+            return ['type' => 'order', 'data'=> $mixedValue];    
+
+        $sAttr = $this->_convertAttrs(
+            $aField, 'attr_cell',
+            'bx-def-padding-sec-bottom bx-def-padding-sec-top', // add default classes
+            isset($aField['width']) ? 'width:' . $aField['width'] : false  // add default styles
+        );
+
+        return '<td ' . $sAttr . '><div id="' . $this->_sObject . '_cell_' . $aRow[$this->_aOptions['field_id']] . '" class="bx-grid-drag-handle"><i class="sys-icon align-justify"></i></div></td>';
+    }
+
+    protected function _getCellActions ($mixedValue, $sKey, $aField, $aRow)
+    {
+        $sAttr = $this->_convertAttrs(
+            $aField, 'attr_cell',
+            'bx-def-padding-sec-bottom bx-def-padding-sec-top', // add default classes
+            isset($aField['width']) ? 'width:' . $aField['width'] : false  // add default styles
+        );
+
+        $mixedDisabledBehavior = $this->_getActionsDisabledBehavior($aRow);
+        $mixedActions = $this->_getActions('single', $aRow[$this->_aOptions['field_id']], false, null === $mixedDisabledBehavior ? $this->_isRowDisabled($aRow) : $mixedDisabledBehavior, null !== $mixedDisabledBehavior, $aRow);
+
+        if($this->_bIsApi)
+            return ['type' => 'actions', 'data' => $mixedActions];
+
+        return '<td ' . $sAttr . '><div class="bx-grid-cell-single-actions-wrapper bx-def-margin-thd-neg">' . $mixedActions . '</div></td>';
+    }
+
+    protected function _getCellDefault ($mixedValue, $sKey, $aField, $aRow)
+    {
+        if($this->_bIsApi)
+            return ['type' => 'text', 'value'=> $mixedValue];    
+
+        $sAttr = $this->_convertAttrs(
+            $aField, 'attr_cell',
+            'bx-def-padding-sec-bottom bx-def-padding-sec-top', // add default classes
+            isset($aField['width']) ? 'width:' . $aField['width'] : false // add default styles
+        );
+        return '<td ' . $sAttr . '>' . $mixedValue . '</td>';
+    }
+
+    protected function _getActions ($sType, $sActionData = false, $isSmall = false, $isDisabled = false, $isPermanentState = false, $aRow = array())
+    {
+        $sActionsType = 'actions_' . $sType;
+        if (empty($this->_aOptions[$sActionsType]) || !is_array($this->_aOptions[$sActionsType]))
+            return '';
+
+        $mixedResult = $this->_bIsApi ? [] : '';
+        foreach ($this->_aOptions[$sActionsType] as $sKey => $a) {
+            if(!$a['active'] || (!$a['title'] && !$a['icon']))
+                continue;
+
+            $sFunc = '_getAction' . $this->_genMethodName($sKey);
+            if (!method_exists($this, $sFunc))
+                $sFunc = empty($a) ? '_getActionDivider' : '_getActionDefault';
+
+            if (!isset($a['attr']['bx_grid_action'])) {
+                $a['attr']['bx_grid_action_' . $sType] = $sKey;
+                if (!empty($sActionData))
+                    $a['attr']['bx_grid_action_data'] = $sActionData;
+                if (isset($a['confirm']))
+                    $a['attr']['bx_grid_action_confirm'] = $a['confirm'] ? 1 : 0;
+                $a['attr']['bx_grid_action_reset_paginate'] = false === strpos($sKey, 'delete') ? 0 : 1; // reset paginate after deleting row 
+            }
+
+            if ($isPermanentState && !isset($a['attr']['bx_grid_permanent_state']))
+                $a['attr']['bx_grid_permanent_state'] = 1;
+
+            if ($this->_bIsApi)
+                $mixedResult[] = $this->$sFunc($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+            else
+                $mixedResult .= $this->$sFunc($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+        }
+
+        return $mixedResult;
+    }
+
+    protected function _getActionsAPI ($sType)
+    {
+        $sActionsType = 'actions_' . $sType;
+        if(empty($this->_aOptions[$sActionsType]) || !is_array($this->_aOptions[$sActionsType]))
+            return [];
+
+        
+        
+        foreach ($this->_aOptions[$sActionsType] as $sKey => $aAction){
+            $sFunc = '_getAction' . $this->_genMethodName($sType . '_' . $sKey);
+            if (method_exists($this, $sFunc)){
+                $this->_aOptions[$sActionsType][$sKey] = $this->$sFunc($aAction);
+            }
+            else{
+                $this->_aOptions[$sActionsType][$sKey]['type'] = 'modal';
+                $this->_aOptions[$sActionsType][$sKey]['action'] = $sKey;
+                $this->_aOptions[$sActionsType][$sKey]['params'] = '';
+            }
+        }
+        return $this->_aOptions[$sActionsType];
+    }
+
+    protected function _getActionDelete($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if($this->_bIsApi)
+            return array_merge($a, ['name' => $sKey, 'type' => 'callback', 'on_callback' => 'hide_row']);
+
+    	return $this->_getActionDefault($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getActionDefault ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        
+        if ($a['icon_only'] && empty($a['attr']['title']) && !empty($a['title']))
+            $a['attr']['title'] = $a['title'];
+
+        if ($this->_bIsApi) {
+            $sFunc = '_getAction' . $this->_genMethodName($sType . '_' . $sKey);
+            if (method_exists($this, $sFunc)) {
+                $aAction = $this->$sFunc($a, ['id'=> $aRow[$this->_aOptions['field_id']]]);
+                if(empty($aAction) || !is_array($aAction))
+                    return [];
+
+                return array_merge($a, ['name' => $sKey], $aAction);
+            }
+            else {
+                return array_merge($a, ['name' => $sKey, 'type' => 'modal', 'action' => $sKey, 'params' => '&id=' . $aRow[$this->_aOptions['field_id']] ]);
+            }
+            
+        }
+
+        $sAttr = $this->_convertAttrs(
+            $a, 'attr',
+            'bx-btn bx-def-margin-thd' . ($isSmall ? ' bx-btn-small' : '') . ($isDisabled ? ' bx-btn-disabled' : '') // add default classes
+        );
+
+        $sIcon = '';
+        $sImage = '';
+        if (!empty($a['icon'])) {
+            if (false === strpos($a['icon'], '.'))
+                $sIcon = '<i class="sys-icon ' . $a['icon'] . '"></i>';
+            elseif ($sIconUrl = $this->_oTemplate->getIconUrl($a['icon']))
+                $sImage = '<img style="background-image:url(' . $sIconUrl . ');" src="' . $this->_oTemplate->getIconUrl('spacer.gif') .'" />';
+        }
+
+        return '<button ' . $sAttr . '>' . $sIcon . $sImage . ($a['icon_only'] || empty($a['title']) ? '' : '<u>' . $a['title'] . '</u>') . '</button>';
+    }
+
+    protected function _getActionDivider ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        return '<div class="bx-grid-actions-divider bx-def-margin-sec-left"> | </div>';
+    }
+
+    protected function _getActionResult($aResult)
+    {
+        return $this->_bIsApi ? $aResult : echoJson($aResult);
+    }
+
+    protected function _getFilterControls ()
+    {
+        $oForm = new BxTemplFormView(array());
+
+        $aInput = array(
+            'type' => 'text',
+            'name' => 'keyword',
+            'attrs' => array(
+                'id' => 'bx-grid-search-' . $this->_sObject
+            )
+        );
+
+        $this->_oTemplate->addCss('forms.css');
+        return $oForm->genRow($aInput);
+    }
+
+    protected function _getFilterControlsAPI($aFilters = [])
+    {
+        if(!empty($this->_aOptions['filter_fields']) || !empty($this->_aOptions['filter_fields_translatable']))
+            $aFilters['search'] = [];
+
+        return $aFilters;
+    }
+
+    protected function _getCounter()
+    {
+        return _t('_sys_grid_total_count', $this->_iTotalCount);
+    }
+
+    protected function _limitMaxLength ($mixedValue, $sKey, $aField, $aRow, $isDisplayPopupOnTextOverflow, $bReturnString = true)
+    {
+        if ($aField['chars_limit'] > 0)
+            $mixedValue = BxTemplFunctions::getInstance()->getStringWithLimitedLength($mixedValue, $aField['chars_limit'], $isDisplayPopupOnTextOverflow, $bReturnString);
+        return $mixedValue;
+    }
+
+    protected function _convertAttrs ($aField, $sAttrName, $sClasses = false, $sStyles = false)
+    {
+        if (!empty($aField['hidden_on'])) {
+            $aHiddenOn = array(
+                pow(2, BX_DB_HIDDEN_PHONE - 1) => 'bx-def-media-phone-hide',
+                pow(2, BX_DB_HIDDEN_TABLET - 1) => 'bx-def-media-tablet-hide',
+                pow(2, BX_DB_HIDDEN_DESKTOP - 1) => 'bx-def-media-desktop-hide',
+                pow(2, BX_DB_HIDDEN_MOBILE - 1) => 'bx-def-mobile-app-hide'
+            );
+            foreach ($aHiddenOn as $iHiddenOn => $sClass)
+                if ((int)$aField['hidden_on'] & $iHiddenOn)
+                    $sClasses .= ' ' . $sClass;
+        }
+            
+        return bx_convert_array2attrs(
+            isset($aField[$sAttrName]) && is_array($aField[$sAttrName]) ? $aField[$sAttrName] : array(),
+            $sClasses,
+            $sStyles
+        );
+    }
+
+    protected function _updateOrder($mixedId, $iOrder)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sTable = $this->_aOptions['table'];
+        $sFieldId = $this->_aOptions['field_id'];
+        $sFieldOrder = $this->_aOptions['field_order'];
+        $sQuery = $oDb->prepare("UPDATE `{$sTable}` SET `{$sFieldOrder}` = ? WHERE `{$sFieldId}` = ?", $iOrder, $mixedId);
+        return $oDb->query($sQuery);
+    }
+
+    protected function _delete ($mixedId)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sTable = $this->_aOptions['table'];
+        $sFieldId = $this->_aOptions['field_id'];
+        $sQuery = $oDb->prepare("DELETE FROM `{$sTable}` WHERE `{$sFieldId}` = ?", $mixedId);
+        return $oDb->query($sQuery);
+    }
+
+    protected function _enable ($mixedId, $isChecked)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sTable = $this->_aOptions['table'];
+        $sFieldId = $this->_aOptions['field_id'];
+        $sFieldActive = $this->_aOptions['field_active'];
+        $sQuery = $oDb->prepare("UPDATE `{$sTable}` SET `$sFieldActive` = ? WHERE `{$sFieldId}` = ?", $this->_switcherChecked2State($isChecked), $mixedId);
+        return $oDb->query($sQuery);
+    }
+
+    protected function _addJsCss()
+    {
+        if ($this->_aOptions['field_order']) {            
+            $this->_oTemplate->addJs(array(
+                'jquery-ui/jquery-ui.custom.min.js',
+                'URI.min.js',
+            ));
+        }
+
+        $this->_oTemplate->addJs('BxDolGrid.js');
+        $this->_oTemplate->addCss('grid.css');
+
+        $this->_oTemplate->addJsTranslation('_sys_grid_confirmation');
+    }
+}
+
+/** @} */

+ 1208 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBasePage.php

@@ -0,0 +1,1208 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+require_once(BX_DIRECTORY_PATH_INC . 'design.inc.php');
+
+/**
+ * Page representation.
+ * @see BxDolPage
+ */
+class BxBasePage extends BxDolPage
+{
+    protected $_oTemplate;
+
+    protected $_sStorage; //--- Storage object for page's images like custom cover, HTML block attachments, etc.
+    protected $_oPageCacheObject = null;
+    protected $_oBlockCacheObject = null;
+    
+    protected $_sJsClassName = '';
+    protected $_sJsObjectName = '';
+    protected $_aHtmlIds = [];
+
+    protected $_bStickyColumns = false;
+
+    protected $_bSubPage = false;
+
+    public function __construct ($aObject, $oTemplate)
+    {
+        parent::__construct ($aObject);
+
+        if ($oTemplate)
+            $this->_oTemplate = $oTemplate;
+        else
+            $this->_oTemplate = BxDolTemplate::getInstance();
+
+        $this->_sStorage = 'sys_images';
+        
+        $this->_sJsClassName = 'BxDolPage';
+        $this->_sJsObjectName = 'oBxDolPage';
+        
+        $this->_bStickyColumns = isset($this->_aObject['sticky_columns']) && $this->_aObject['sticky_columns'] == 1;
+
+        $sHtmlId = str_replace(array('_' , ' '), array('-', '-'), $this->_sObject);
+        $this->_aHtmlIds = array(
+            'help_popup' => $sHtmlId . '-help-popup-',
+        );
+        $this->addMarkers(array('site_title' => getParam('site_title')));
+    }
+
+    public function performActionGetHelp ()
+    {
+        $iBlockId = (int)bx_get('block_id');
+        if(empty($iBlockId))
+            return;
+
+        $aBlock = $this->_oQuery->getPageBlock($iBlockId);
+        if(empty($aBlock) || !is_array($aBlock) || empty($aBlock['help']))
+            return;
+
+        echo $this->_oTemplate->parseHtmlByName('designbox_help_popup.html', array(
+            'content' => _t($aBlock['help'])
+        ));
+    }
+    
+    public function performActionGetShare ()
+    {
+        $aEmbedData = BxDolPage::getEmbedData(bx_get('url'));
+        if(!isset($aEmbedData['url'])) 
+            return; 
+
+        $sMenu = '';
+        if(getParam('sys_a2a_enable') != 'on') {
+            if(($oMenu = BxDolMenu::getObjectInstance('sys_social_sharing')) !== false) {
+                $oMenu->addMarkers([
+                    'url' => $aEmbedData['url'],
+                    'img_url_encoded' => '',
+                    'title_encoded' => ''
+                ]);
+                $sMenu = $oMenu->getCode();
+            }
+        }
+        else
+            $sMenu = getParam('sys_a2a_code');
+
+        echo PopupBox('sys_share_popup', _t('_sys_txt_share_popup_header'), $this->_oTemplate->parseHtmlByName('designbox_share_popup.html', [
+            'url' => bx_process_output($aEmbedData['url']),
+            'menu' => $sMenu,
+            'code' => htmlspecialchars($aEmbedData['html']),
+        ]), true);
+    }
+    
+    public function performActionEmbed ()
+    {
+        $aCover = $this->getPageCoverImage();
+        $sTitle = $this->_getPageTitle();
+        $this->_oTemplate->getEmbed($this->_oTemplate->parseHtmlByName('embed_card.html', [
+            'title' => $sTitle,
+            'url' => BxDolPermalinks::getInstance()->permalink($this->_aObject['url']),
+            'bx_if:thumb' => [
+                'condition' => $this->isPageCover() && count($aCover) > 0,
+                'content' => [
+                    'title' => $sTitle,
+                    'url' => BxDolPermalinks::getInstance()->permalink($this->_aObject['url']),
+                    'background' => BxDolCover::getInstance(BxDolTemplate::getInstance())->getCoverImageUrl($aCover)
+                ],
+            ],
+            'bx_if:no_thumb' => [
+                'condition' => count($aCover) == 0 || !$this->isPageCover(),
+                'content' => [
+                    'title' => $sTitle,
+                    'url' => BxDolPermalinks::getInstance()->permalink($this->_aObject['url']),
+                ]
+            ]
+        ]
+        ));
+    }
+
+    /**
+     * Very similar to BxBasePage::getCode
+     * but adds css and js files which are needed for the corect page display
+     */ 
+    public function getCodeDynamic ()
+    {
+        // TODO: remake to use use collect* template methods
+
+        $oTemplate = BxDolTemplate::getInstance();
+
+        // get js&css before the page code is generated
+        $aCssBefore = $oTemplate->getCss();
+        $aJsBefore = $oTemplate->getJs();
+
+        // generate page code
+        $sContent = $this->getCode();
+
+        // get js&css after the page code is generated
+        $aCssAfter = $oTemplate->getCss();
+        $aJsAfter = $oTemplate->getJs();
+
+        // compare files which were added before and after page code is generated
+        $f = function ($a1, $a2) {
+            return strcasecmp($a1['url'], $a2['url']);
+        };
+        $aCssNew = array_udiff($aCssAfter, $aCssBefore, $f);
+        $aJsNew = array_udiff($aJsAfter, $aJsBefore, $f);
+
+        // add newly added js&css files in static mode
+        $sCss = $sJs = '';
+        foreach ($aCssNew as $a)
+            $sCss .= $oTemplate->addCss($a['url'], true);
+        foreach ($aJsNew as $a)
+            $sJs .= $oTemplate->addJs($a['url'], true);
+
+        return $sJs . $sCss . $sContent;
+    }
+    
+    /**
+     * Get page code with automatic caching, adding necessary css/js files and system template vars.
+     * @return string.
+     */
+    public function getCode ()
+    {
+        if (bx_get('dynamic') && ($iBlockId = (int)bx_get('pageBlock'))) {
+
+            if (self::isLockedFromUnauthenticated($this->_aObject['uri']) || !$this->_isVisiblePage($this->_aObject)) {
+                header('HTTP/1.0 403 Forbidden');
+                exit;
+            }
+
+            bx_alert('system', 'page_output_block', 0, false, array(
+                'page_name' => $this->_sObject,
+                'page_object' => $this,
+                'page_query' => $this->_oQuery,
+                'block_id' => (int)$iBlockId,
+            ));
+
+            header( 'Content-type:text/html;charset=utf-8' );
+
+            BxDolTemplate::getInstance()->collectingStart();
+
+            $s = $this->_getBlockOnlyCode($iBlockId);
+
+            $sCss = bx_get('includedCss');
+            $sJs = bx_get('includedJs');
+            echo BxDolTemplate::getInstance()->collectingEndGetCode($sCss ? @json_decode($sCss) : [], $sJs ? @json_decode($sJs) : []);
+
+            echo $s;
+            exit;
+        }
+
+        if (($mixedAvailable = $this->_isAvailablePage($this->_aObject)) !== true) 
+            return $this->_getPageNotFoundMsg($mixedAvailable);
+
+        if (($mixedVisible = $this->_isVisiblePage($this->_aObject)) !== true)
+            return $this->_getPageAccessDeniedMsg($mixedVisible);
+
+        $this->_addJsCss();
+
+        if (!$this->_bSubPage) {
+            $this->_addSysTemplateVars();
+
+            $this->_selectMenu();
+
+            $this->_setSubmenu(array());
+        }
+
+        if (bx_is_dbg() || !getParam('sys_page_cache_enable') || !$this->_aObject['cache_lifetime'] || (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST')) {
+            $sPageCode = $this->_getPageCode();
+        }
+        else {
+            $oCache = $this->_getPageCacheObject();
+            $sKey = $this->_getPageCacheKey();
+            $sKeyCssJs = 'cssjs_' . $sKey;
+
+            $mixedRet = $oCache->getData($sKey, $this->_aObject['cache_lifetime']);
+            $aRetCssJs = $oCache->getData($sKeyCssJs, $this->_aObject['cache_lifetime']);
+
+            if ($mixedRet !== null && $aRetCssJs !== null) {
+                BxDolTemplate::getInstance()->collectingStart();                
+                BxDolTemplate::getInstance()->collectingInject($aRetCssJs['css'], $aRetCssJs['js']);
+                $sPageCode = BxDolTemplate::getInstance()->collectingEndGetCode();
+                $sPageCode .= $mixedRet;
+            } else {
+
+                BxDolTemplate::getInstance()->collectingStart();
+
+                $sPageCode = $this->_getPageCode();
+
+                $aPageCssJs = BxDolTemplate::getInstance()->collectingEndGetCode(array(), array(), 'array');
+
+                $oCache->setData($sKey, $sPageCode, $this->_aObject['cache_lifetime']);
+                $oCache->setData($sKeyCssJs, $aPageCssJs, $this->_aObject['cache_lifetime']);
+            }
+        }
+        
+        bx_alert('system', 'page_output', 0, false, array(
+            'page_name' => $this->_sObject,
+            'page_object' => $this,
+            'page_query' => $this->_oQuery,
+            'page_code' => &$sPageCode,
+            'sub_page' => $this->_bSubPage,
+        ));
+
+        if (!$this->_bSubPage)
+            $sPageCode .= $this->getJsScript();
+        
+        return $sPageCode;
+    }
+    
+    public function getJsClassName()
+    {
+        return $this->_sJsClassName;
+    }
+
+    public function getJsObjectName()
+    {
+        return $this->_sJsObjectName;
+    }
+    
+    function _wrapInTagJsCode($sCode)
+    {
+        return "<script language=\"javascript\">\n<!--\n" . $sCode . "\n-->\n</script>";
+    }
+
+    public function getJsScript()
+    {
+        $sJsObjName = $this->getJsObjectName();
+        $sJsObjClass = $this->getJsClassName();
+        $sCode = "if(window['" . $sJsObjName . "'] == undefined) var " . $sJsObjName . " = new " . $sJsObjClass . "(" . json_encode(array(
+            'sObjName' => $sJsObjName,
+            'sObject' => $this->_sObject,
+            'sRootUrl' => BX_DOL_URL_ROOT,
+            'aHtmlIds' => $this->_aHtmlIds,
+            'isStickyColumns' => $this->_bStickyColumns
+        )) . ");";
+
+        return $this->_wrapInTagJsCode($sCode);
+    }
+
+    /**
+     * Is page cover enabled.
+     * @return string
+     */
+    public function isPageCover()
+    {
+        $bResult = false;
+        switch((int)$this->_aObject['cover']) {
+            case 1: //--- Enabled for all
+                $bResult = true;
+                break;
+
+            case 2: //--- Enabled for visitors only
+                $bResult = !isLogged();
+                break;
+
+            case 3: //--- Enabled for members only
+                $bResult = isLogged();
+                break;
+        }
+
+    	return $bResult;
+    }
+
+    public function setSubPage($b = true)
+    {
+        $this->_bSubPage = $b;
+    }
+
+    public function setPageCover($bCover = true)
+    {
+        $this->_aObject['cover'] = (bool)$bCover;
+    }
+
+	public function getPageCoverImage($bTranscoder = true)
+    {
+    	$iId = (int)$this->_aObject['cover_image'];
+    	if(empty($iId)) {
+    		$iId = (int)getParam('sys_site_cover_common');
+    		if(empty($iId))
+    			return array();
+    	}
+
+    	$aResult = array(
+    		'id' => $iId
+    	);
+    	if($bTranscoder)
+    		$aResult['transcoder'] = BX_DOL_TRANSCODER_OBJ_COVER;
+    	else 
+    		$aResult['object'] = $this->_sStorage;
+
+    	return $aResult;
+    }
+
+    public function getPageCoverParams()
+    {
+        $aParams = [
+            'title' => $this->_getPageTitle(),
+            'actions' => '',
+            'bx_if:image' => [
+                'condition' => false,
+                'content' => []
+            ],
+            'bx_if:icon' => [
+                'condition' => false,
+                'content' => []
+            ],
+        ];
+
+        if(($oMenuSubmenu = BxDolMenu::getObjectInstance('sys_site_submenu')) !== false) {
+            $aCoverParams = $oMenuSubmenu->getPageCoverParams();
+            if(!empty($aCoverParams) && is_array($aCoverParams))
+                $aParams = $aCoverParams;
+        }
+
+        if(!empty($this->_aObject['cover_title'])) {
+            $sCoverTitle = _t($this->_aObject['cover_title']);
+            if($sCoverTitle && strcmp($sCoverTitle, $this->_aObject['cover_title']) != 0)
+                $aParams['title'] = $sCoverTitle;
+        }
+
+    	return $aParams;
+    }
+
+    /**
+     * Get block title.
+     * @return string
+     */
+    public function getBlockTitle ($aBlock)
+    {
+        return $this->_replaceMarkers(_t($aBlock['title']), array('block_id' => $aBlock['id']));
+    }
+
+    /**
+     * Get help control if help is available for the block.
+     * @return string
+     */
+    public function getBlockHelp ($aBlock)
+    {
+        if(empty($aBlock['help']) || empty(strip_tags(_t($aBlock['help']))))
+            return '';
+
+        return $this->_oTemplate->parseHtmlByName('designbox_help.html', array(
+            'js_object' => $this->_sJsObjectName,
+            'block_id' => $aBlock['id']
+        ));
+    }
+
+    /**
+     * Get code to load block asyncroniously
+     * @param $aBlock block code
+     * @param $iAsync if greater than 0 the it defines loading indicator for the block
+     * @return HTML code string
+     */
+    public function getBlockAsyncCode($aBlock, $iAsync)
+    {
+        $aContentPlaceholder = $this->_oQuery->getPageBlockContentPlaceholder($iAsync);
+        if (!$aContentPlaceholder)
+            return _t('_sys_txt_error_occured');
+        $oTemplate = $this->_oTemplate;
+        if ('system' != $aContentPlaceholder['module']) {
+            $oModule = BxDolModule::getInstance($aContentPlaceholder['module']);
+            if (!$oModule)
+                return _t('_sys_txt_error_occured');
+            $oTemplate = $oModule->_oTemplate;
+        }
+        return $oTemplate->parseHtmlByName($aContentPlaceholder['template'], $aBlock);
+    }
+
+    /**
+     * Get page array with all cells and blocks
+     */
+    public function getPageAPI ($aBlocks = [])
+    {
+        define('BX_API_PAGE', true);
+        $query_string  = '';
+
+        if (isset(bx_get('params')[2]) && bx_get('params')[2] != ''){
+            $array = json_decode(bx_get('params')[2], true);
+            $query_string = http_build_query($array);
+            parse_str($query_string, $_GET);
+        }
+        $sMetaTitle = $this->_getPageMetaTitle();
+        $sName = $this->_getPageTitle();
+        $a = [
+            'id' => $this->_aObject['id'],
+            'title' => $sMetaTitle ? $sMetaTitle : $sName,
+            'name' => $sName,
+            'uri' => $this->_aObject['uri'],
+            'url' =>  ((bx_get('params')[0] ? bx_get('params')[0] : $this->_aObject['uri'] ) . ($query_string != '' ? '?' . $query_string : '')),
+            'author' => $this->_aObject['author'],
+            'added' => $this->_aObject['added'],
+            'module' => $this->getModule(),
+            'type' => $this->getType (),
+            'layout' => str_replace('.html', '', $this->_aObject['template']),
+            'cover_block' => '',
+            'menu_top' => '',
+            'menu' => '',
+            'menu_bottom' => '',
+            'menu_add' => '',
+            'elements' => $this->getPageBlocksAPI($aBlocks),
+        ];
+        
+        
+        if (BxDolCover::getInstance($this)->isCover() && isset($this->_aProfileInfo)) {
+            $oModule = BxDolModule::getInstance($this->getModule());
+            $aContentInfo = bx_srv($this->getModule(), 'get_info', [$this->_aProfileInfo['content_id'], false]);
+            
+            $a['cover_block'] = [
+                'profile' => BxDolProfile::getData($this->_aProfileInfo['id'], [
+                    'get_avatar' => 'getAvatarBig',
+                    'with_info' => true
+                ]),
+                'actions_menu' => '',
+                'meta_menu' => '',
+                'badges' => BxDolProfile::getInstance($this->_aProfileInfo['id'])->getBadges(),
+                'cover' => $oModule->serviceGetCover($this->_aProfileInfo['content_id']),
+                'allow_edit' =>  $oModule->checkAllowedChangeCover($aContentInfo) === CHECK_ACTION_RESULT_ALLOWED
+            ];
+            
+            $CNF = $oModule->_oConfig->CNF;
+            
+            if(!empty($CNF['OBJECT_MENU_VIEW_ENTRY_META'])){
+                $oMetaMenu = BxTemplMenu::getObjectInstance($CNF['OBJECT_MENU_VIEW_ENTRY_META']);
+                $a['cover_block']['meta_menu'] =  $oMetaMenu->getCodeAPI();
+            }
+            
+            $oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS']);
+            if ($oConnection){
+                $a['cover_block']['members_list'] =  $oConnection->getConnectedListAPI($this->_aProfileInfo['id'], false, BX_CONNECTIONS_CONTENT_TYPE_INITIATORS, 10);
+            }
+            
+            if(!empty($CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY'])){
+                $oActionMenu = BxTemplMenu::getObjectInstance($CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY_ALL']);
+                $a['cover_block']['actions_menu'] = $oActionMenu->getCodeAPI();
+            }  
+        }
+        
+        $sMenuTop = getParam('sys_api_menu_top');
+        if (!empty($sMenuTop) && ($oMenuTop = BxDolMenu::getObjectInstance($sMenuTop)) !== false)
+            $a['menu_top'] = $oMenuTop->getCodeAPI();
+
+        if (($oMenuSubmenu = BxDolMenu::getObjectInstance('sys_site_submenu')) !== false) {
+            if(($sSubmenu = $this->getSubMenu()) !== '')
+                $oMenuSubmenu->setObjectSubmenu($sSubmenu, []);
+
+            $a['menu'] = $oMenuSubmenu->getCodeAPI();
+        }
+
+        if (isLogged()) {
+            /* temporatery commented
+                if(($oMenuAddContent = BxDolMenu::getObjectInstance('sys_add_content')) !== false)
+                $a['menu_add'] = $oMenuAddContent->getCodeAPI();
+            */
+            $o = BxDolProfile::getInstance();
+            $oAccount = $o->getAccountObject();
+            $a['user'] = [
+                'id' => $o->id(),
+                'email' => $o->getAccountObject()->getEmail(),
+                'display_name' => $o->getDisplayName(),
+                'url' => bx_api_get_relative_url($o->getUrl()),
+                'avatar' => $o->getAvatar(),
+                'settings' => $o->getSettings(),
+                //'level' => BxDolAcl::getInstance()->getMemberMembershipInfo($o->id()),
+                'moderator' => (bool)BxDolAcl::getInstance()->isMemberLevelInSet([MEMBERSHIP_ID_ADMINISTRATOR, MEMBERSHIP_ID_MODERATOR], $o->id()),
+                'operator' => isAdmin(),
+                //'info' => $o->getInfo(),
+                'confirmed' => $oAccount->isConfirmed(),
+                'notifications' => 0,
+                'cart' => 0,
+                'active' => $o->isActive(),
+                'status' => $o->getStatus(),
+            ];
+            
+            $oInformer = BxDolInformer::getInstance(BxDolTemplate::getInstance());
+            $sRet = $oInformer ? $oInformer->display() : '';
+            if ($sRet){
+                $a['user']['informer'] = $sRet;
+            }
+            
+            $sModuleNotifications = 'bx_notifications';
+            if(BxDolRequest::serviceExists($sModuleNotifications, 'get_unread_notifications_num'))
+                $a['user']['notifications'] = bx_srv($sModuleNotifications, 'get_unread_notifications_num', [$o->id()]);
+
+            $oPayments = BxDolPayments::getInstance();
+            if($oPayments->isActive())
+                $a['user']['cart'] = $oPayments->getCartItemsCount();
+        }
+        
+        $aExtras = [
+            'page' => $this,
+            'blocks' => $aBlocks,
+            'data' => &$a,
+        ];
+
+        if(!$this->_isAvailablePage($this->_aObject))
+            $a['page_status'] = 404;
+        else if(!$this->_isVisiblePage($this->_aObject))
+            $a['page_status'] = 403;
+
+        bx_alert('system', 'get_page_api', 0, 0, $aExtras);
+
+        return $a;
+    }
+
+    public function getPageBlocksAPI($aBlocks = [])
+    {
+        $bBlocks = !empty($aBlocks) && is_array($aBlocks);
+        $aFieldsUnset = ['object', 'cell_id', 'title_system', 'class', 'submenu', 'tabs', 'async', 'visible_for_levels', 'hidden_on', 'type', 'text', 'text_updated', 'help', 'cache_lifetime', 'active', 'active_api', 'copyable', 'deletable', 'order'];
+
+        $aCells = $this->_oQuery->getPageBlocks(true);
+        foreach($aCells as $sKey => &$aCell) {
+            foreach($aCell as $i => $aBlock) {     
+                if(!$this->_isVisibleBlock($aBlock)) {
+                    unset($aCells[$sKey][$i]);
+                    continue;
+                }
+
+                $this->processPageBlock($aCells[$sKey][$i], true);
+                $aBlock = $aCells[$sKey][$i];
+
+                $sSource = '';
+                if($aBlock['type'] == 'service') {
+                    $aContent = @unserialize($aBlock['content']);
+                    if(isset($aContent['module'], $aContent['method']))
+                        $sSource = $aContent['module'] . ':' . $aContent['method'];
+                }
+                else{
+                    $sSource = 'system:block_' . $aBlock['id'];
+                }
+
+                if($bBlocks && !in_array($sSource, $aBlocks)) {
+                    unset($aCells[$sKey][$i]);
+                    continue;
+                }
+
+                $sFunc = '_getBlock' . ucfirst($aBlock['type']);
+                $mBlock = method_exists($this, $sFunc) ? $this->$sFunc($aBlock) : $aBlock['content'];
+
+                $aCells[$sKey][$i] = array_merge($aCells[$sKey][$i], [
+                    'title' => isset($mBlock['title']) ? $mBlock['title'] : $this->getBlockTitle($aBlock),
+                    'content' => isset($mBlock['content']) ? $mBlock['content'] : $mBlock,
+                    'menu' => isset($mBlock['menu']) ? $mBlock['menu'] : '',
+                    'source' => $sSource
+                ]);
+                $aCells[$sKey][$i] = array_diff_key($aCells[$sKey][$i], array_flip($aFieldsUnset));
+            }
+        }
+
+        return array_map('array_values', $aCells);
+    }
+
+    /**
+     * Get page code vars
+     * @return string
+     */
+    protected function _getPageCodeVars ()
+    {
+    	$aHiddenOn = array(
+            pow(2, BX_DB_HIDDEN_PHONE - 1) => 'bx-def-media-phone-hide',
+            pow(2, BX_DB_HIDDEN_TABLET - 1) => 'bx-def-media-tablet-hide',
+            pow(2, BX_DB_HIDDEN_DESKTOP - 1) => 'bx-def-media-desktop-hide',
+            pow(2, BX_DB_HIDDEN_MOBILE - 1) => 'bx-def-mobile-app-hide'
+        );
+
+        $aVars = array (
+            'page_id' => 'bx-page-' . $this->_aObject['uri'],
+            'bx_if:show_layout_row_dump' => array(
+                'condition' => false,
+                'content' => array()
+            )
+        );
+        $aBlocks = $this->_oQuery->getPageBlocks();
+        foreach ($aBlocks as $sKey => $aCell) {
+            $sCell = '';
+            foreach ($aCell as $aBlock) {
+                $this->processPageBlock($aBlock, false);
+
+                $sContentWithBox = $this->_getBlockCodeWithCache($aBlock, $aBlock['async']);
+
+            	$sClassAdd = '';
+                if(!empty($aBlock['class']))
+                    $sClassAdd .= ' ' . $aBlock['class'];
+
+                if(!empty($aBlock['hidden_on']))
+                    foreach($aHiddenOn as $iHiddenOn => $sHiddenOnClass)
+                        if((int)$aBlock['hidden_on'] & $iHiddenOn)
+                            $sClassAdd .= ' ' . $sHiddenOnClass;
+
+                if ($sContentWithBox)
+                    $sCell .= $this->_oTemplate->parseHtmlByName('designbox_container.html', array(
+                        'class_add' => $sClassAdd,
+                        'bx_if:show_html_id' => array(
+                            'condition' => true,
+                            'content' => array(
+                                'html_id' => 'bx-page-block-' . $aBlock['id']
+                            ),
+                        ),
+                        'content' => $sContentWithBox
+                    ));
+            }
+            $aVars[$sKey] = $sCell;
+        }
+
+        return $aVars;
+    }
+
+    /**
+     * Process block values, especially if someting need to be overrided 
+     */
+    protected function processPageBlock(&$aBlock, $bApi = false) 
+    {
+
+    }
+
+    /**
+     * Get page code only.
+     * @return string
+     */
+    protected function _getPageCode ()
+    {
+        $aVars = $this->_getPageCodeVars ();
+        return $this->_oTemplate->parseHtmlByName($this->_aObject['template'], $aVars);
+    }
+
+    /**
+     * Get one block code only.
+     * @return string
+     */
+    protected function _getBlockOnlyCode ($iBlockId)
+    {
+        if (!($aBlock = $this->_oQuery->getPageBlock((int)$iBlockId)))
+            return '';
+        return $this->_getBlockCodeWithCache($aBlock, 0);
+    }
+
+
+    protected function _getBlockCodeWithCache(&$aBlock, $iAsync = 0)
+    {
+        if (bx_is_dbg() || !getParam('sys_pb_cache_enable') || !$aBlock['cache_lifetime'] || $iAsync || (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST')) {
+            $sBlockCode = $this->_getBlockCode($aBlock, $iAsync);
+        }
+        else {
+            $oCache = $this->_getBlockCacheObject();
+            $sKey = $this->_getBlockCacheKey(false, $aBlock);
+            $sKeyCssJs = 'cssjs_' . $sKey;
+
+            $mixedRet = $oCache->getData($sKey, $aBlock['cache_lifetime']);
+            $aRetCssJs = $oCache->getData($sKeyCssJs, $aBlock['cache_lifetime']);
+
+            if ($mixedRet !== null && $aRetCssJs !== null) {
+                BxDolTemplate::getInstance()->collectingStart();
+                BxDolTemplate::getInstance()->collectingInject($aRetCssJs['css'], $aRetCssJs['js']);
+                $sBlockCode = BxDolTemplate::getInstance()->collectingEndGetCode();
+                $sBlockCode .= $mixedRet;
+            } else {
+                BxDolTemplate::getInstance()->collectingStart();
+
+                $sBlockCode = $this->_getBlockCode($aBlock, $iAsync);
+
+                $aBlockCssJs = BxDolTemplate::getInstance()->collectingEndGetCode(array(), array(), 'array');
+
+                $oCache->setData($sKey, $sBlockCode, $aBlock['cache_lifetime']);
+                $oCache->setData($sKeyCssJs, $aBlockCssJs, $aBlock['cache_lifetime']);
+            }
+        }
+
+        return $sBlockCode;
+    }
+
+    /**
+     * Get block code.
+     * @return string
+     */
+    protected function _getBlockCode(&$aBlock, $iAsync = 0)
+    {
+        $sContentWithBox = '';
+        $oFunctions = $this->_oTemplate->getTemplateFunctions();
+
+        if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->beginPageBlock(_t($aBlock['title']), $aBlock['id']);
+
+        $aDbNoTitle = [BX_DB_CONTENT_ONLY, BX_DB_PADDING_CONTENT_ONLY, BX_DB_NO_CAPTION, BX_DB_PADDING_NO_CAPTION];
+
+        $sTitle = $this->getBlockTitle($aBlock);
+        $sHelp = $this->getBlockHelp($aBlock);
+
+        $sFunc = '_getBlock' . ucfirst($aBlock['type']);
+        $bBlockVisible = $this->_isVisibleBlock($aBlock);
+        if ($iAsync && $bBlockVisible) {
+            $iDesignboxId = $aBlock['designbox_id'];
+
+            $sHelpTitle = $sHelpContent = '';
+            if(!in_array($iDesignboxId, $aDbNoTitle))
+                $sHelpTitle = $sHelp;
+            else
+                $sHelpContent = $sHelp;
+
+            $sContent = $this->getBlockAsyncCode($aBlock, $iAsync);
+            $aParams = array(
+                $sTitle . $sHelpTitle,
+                $sContent . $sHelpContent,
+                $iDesignboxId
+            );
+            $sContentWithBox = call_user_func_array(array($oFunctions, 'designBoxContent'), $aParams);
+        } 
+        elseif ($bBlockVisible && method_exists($this, $sFunc)) {
+            $mixedContent = $this->$sFunc($aBlock);
+
+            if(!empty($aBlock['content_empty'])) {
+                $sEmpty = _t($aBlock['content_empty']);
+                $sRegExp = '/(<[A-Za-z0-9]+\b.[^<]*[\pPi\pPf]bx\-msg\-box\b.*[\pPi\pPf][^>]*>)(' . _t('_Empty') . ')(<\/[A-Za-z0-9]+>)/su';
+                if(is_string($mixedContent))
+                    $mixedContent = preg_replace($sRegExp, "\${1}" . $sEmpty . "\${3}", $mixedContent);
+                else if(is_array($mixedContent) && !empty($mixedContent['content']))
+                   $mixedContent['content'] = preg_replace($sRegExp, "\${1}" . $sEmpty . "\${3}", $mixedContent['content']);
+            }
+
+            $this->_oQuery->setReadOnlyMode(true);
+
+            if(is_array($mixedContent) && !empty($mixedContent['content'])) {
+                $iDesignboxId = isset($mixedContent['designbox_id']) ? $mixedContent['designbox_id'] : $aBlock['designbox_id'];
+
+                $sHelpTitle = $sHelpContent = '';
+                if(!in_array($iDesignboxId, $aDbNoTitle))
+                    $sHelpTitle = $sHelp;
+                else
+                    $sHelpContent = $sHelp;
+
+                $aParams = array(
+                    (isset($mixedContent['title']) ? $mixedContent['title'] : $sTitle) . $sHelpTitle,
+                    $mixedContent['content'] . $sHelpContent,
+                    $iDesignboxId
+                );
+
+                $mixedMenu = false;
+                if(isset($mixedContent['menu']))
+                    $mixedMenu = $mixedContent['menu'];
+                else if(!empty($aBlock['submenu']))
+                    $mixedMenu = $aBlock['submenu'];
+                $aParams[] = $mixedMenu;
+
+                if(isset($mixedContent['buttons']))
+                    $aParams[] = $mixedContent['buttons'];
+                else if($mixedMenu)
+                    $aParams[] = (int)$aBlock['tabs'] == 1 ? true : array();
+
+                if(isset($mixedContent['class']))
+                    $aBlock['class'] = $mixedContent['class'];
+            }
+            else if(is_string($mixedContent) && !empty($mixedContent)) {
+                $iDesignboxId = $aBlock['designbox_id'];
+
+                $sHelpTitle = $sHelpContent = '';
+                if(!in_array($iDesignboxId, $aDbNoTitle))
+                    $sHelpTitle = $sHelp;
+                else
+                    $sHelpContent = $sHelp;
+
+                $aParams = array(
+                    $sTitle . $sHelpTitle,
+                    $mixedContent . $sHelpContent,
+                    $iDesignboxId
+                );
+
+                $mixedMenu = !empty($aBlock['submenu']) ? $aBlock['submenu'] : false;
+                $aParams[] = $mixedMenu;
+
+                if($mixedMenu)
+                    $aParams[] = (int)$aBlock['tabs'] == 1 ? true : array();
+            }
+
+            if(!empty($aParams))
+                $sContentWithBox = call_user_func_array(array($oFunctions, 'designBoxContent'), $aParams);
+        }
+
+        if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->endPageBlock($aBlock['id'], $sContentWithBox ? false : true, false );
+
+        return $sContentWithBox;
+    }
+
+    /**
+     * Add necessary js and css files.
+     */
+    protected function _addJsCss()
+    {
+        $this->_oTemplate->addJs(array('BxDolPage.js', 'theia-sticky-sidebar/theia-sticky-sidebar.min.js', 'theia-sticky-sidebar/ResizeSensor.min.js'));
+        $this->_oTemplate->addCss('page_layouts.css');
+    }
+
+    /**
+     * Set system template variables, like page title, meta description, meta keywords and meta robots.
+     */
+    protected function _addSysTemplateVars ()
+    {
+        $oTemplate = BxDolTemplate::getInstance();
+
+        $sPageTitle = $this->_getPageTitle();
+        if ($sPageTitle)
+            $oTemplate->setPageHeader ($sPageTitle);
+
+        $sMetaTitle = $this->_getPageMetaTitle();
+        if ($sMetaTitle)
+            $oTemplate->setPageMetaTitle ($sMetaTitle);
+        
+        $sMetaDesc = $this->_getPageMetaDesc();
+        if ($sMetaDesc)
+            $oTemplate->setPageDescription ($sMetaDesc);
+
+        $sMetaRobots = $this->_getPageMetaRobots();
+        if ($sMetaRobots)
+            $oTemplate->setPageMetaRobots ($sMetaRobots);
+
+        $sMetaImage = $this->_getPageMetaImage();
+        if ($sMetaImage)
+            $oTemplate->addPageMetaImage($sMetaImage);
+
+        $sMetaKeywords = $this->_getPageMetaKeywords();
+        if ($sMetaKeywords)
+            $oTemplate->addPageKeywords ($sMetaKeywords);
+    }
+
+    /**
+     * Select menu from page properties.
+     */
+    protected function _selectMenu ()
+    {
+        BxDolMenu::setSelectedGlobal ($this->_aObject['module'], $this->_aObject['uri']);
+    }
+
+    /**
+     * Set page submenu if it's specified
+     */
+    protected function _setSubmenu ($aParams)
+    {
+        if(empty($this->_aObject['submenu']) || $this->_aObject['submenu'] == 'disabled')
+            return;
+
+        $oMenuSubmenu = BxDolMenu::getObjectInstance('sys_site_submenu');
+        if(!$oMenuSubmenu)
+            return;
+
+        $oMenuSubmenu->setObjectSubmenu($this->_aObject['submenu'], $aParams);
+    }
+
+    /**
+     * Get content for 'raw' block type.
+     * @return string
+     */
+    protected function _getBlockRaw ($aBlock)
+    {
+        if (bx_is_api()){
+            return [bx_api_get_block('raw', ['title' => _t($aBlock['title']), 'content' => BxDolTemplate::getInstance()->parseHtmlByContent($aBlock['content'], array())])];
+        }
+        
+        $s = '<div class="bx-page-raw-container bx-def-vanilla-html max-w-none">' . BxDolTemplate::getInstance()->parseHtmlByContent($aBlock['content'], array()) . '</div>';
+        $s = $this->_replaceMarkers($s, array('block_id' => $aBlock['id']));
+        $s = bx_process_macros($s);
+        return $s;
+    }
+
+    /**
+     * Get content for 'custom' block type.
+     * @return string
+     */
+    protected function _getBlockCustom ($aBlock)
+    {
+        if (bx_is_api()){
+            return [bx_api_get_block('custom', ['title' => _t($aBlock['title']), 'content' => BxDolTemplate::getInstance()->parseHtmlByContent($aBlock['content'], array())])];
+        }
+        
+        $s = '<div class="bx-page-custom-container">' . BxDolTemplate::getInstance()->parseHtmlByContent($aBlock['content'], array()) . '</div>';
+        $s = $this->_replaceMarkers($s, array('block_id' => $aBlock['id']));
+        $s = bx_process_macros($s);
+        return $s;
+    }
+
+    /**
+     * Get content for 'html' block type.
+     * @return string
+     */
+    protected function _getBlockHtml ($aBlock)
+    {
+        if (bx_is_api()){
+            return [bx_api_get_block('html', ['title' => _t($aBlock['title']), 'content' => $aBlock['content']])];
+        }
+        
+        $s = '<div class="bx-page-html-container bx-def-vanilla-html max-w-none">' . $aBlock['content'] . '</div>';
+        $s = $this->_replaceMarkers($s, array('block_id' => $aBlock['id']));
+        $s = bx_process_macros($s);
+        return $s;
+    }
+
+    /**
+     * Get content for 'wiki' block type.
+     * @return string
+     */
+    protected function _getBlockWiki ($aBlock)
+    {
+        $oWiki = BxDolWiki::getObjectInstance($this->_aObject['module']);
+        if (!$oWiki) {
+            $sContent = _t('_sys_wiki_error_missing_wiki_object', $this->_aObject['module']);
+        } 
+        else {
+            $sContent = $oWiki->getBlockContent($aBlock['id'], false, (int)bx_get($aBlock['id'].'rev') ? (int)bx_get($aBlock['id'].'rev') : false);
+        }
+
+        $s = '<div id="bx-page-wiki-container-' . $aBlock['id'] . '" class="bx-page-wiki-container markdown-body bx-def-vanilla-html">' . $sContent . '</div>';
+        $s = $this->_replaceMarkers($s, array('block_id' => $aBlock['id']));
+        $s = bx_process_macros($s);
+        return $s;
+    }
+
+    /**
+     * Get content for 'lang' block type.
+     * @return string
+     */
+    protected function _getBlockLang ($aBlock)
+    {
+        if (bx_is_api()){
+            return [bx_api_get_block('lang', ['title' => _t($aBlock['title']), 'content' => bx_process_macros(_t(trim($aBlock['content'])))])];
+        }
+    
+        $s = '<div class="bx-page-lang-container bx-def-vanilla-html max-w-none">' . _t(trim($aBlock['content'])) . '</div>';
+        $s = $this->_replaceMarkers($s, array('block_id' => $aBlock['id']));
+        $s = bx_process_macros($s);
+        return $s;
+    }
+
+    /**
+     * Get content for 'image' block type.
+     * @return string
+     */
+    protected function _getBlockImage ($aBlock)
+    {
+        if (empty($aBlock['content']))
+            return false;
+
+        list($iFileId, $sAlign ) = explode('#', $aBlock['content']);
+        $iFileId = (int)$iFileId;
+        if (!$iFileId)
+            return false;
+
+        $oStorage = BxDolStorage::getObjectInstance(BX_DOL_STORAGE_OBJ_IMAGES);
+        if (!$oStorage)
+            return false;
+
+        $sUrl = $oStorage->getFileUrlById($iFileId);
+        if (!$sUrl)
+            return false;
+
+        $sStyleAdd = '';
+        if ('center' == $sAlign || 'left' == $sAlign || 'right' == $sAlign)
+            $sStyleAdd = 'style="text-align:' . $sAlign . '"';
+
+        return '<div class="bx-page-image-container" ' . $sStyleAdd . '><img src="' . $sUrl . '" /></div>';
+    }
+
+    /**
+     * Get content for 'rss' block type.
+     * @return string
+     */
+    protected function _getBlockRss ($aBlock)
+    {
+        if (empty($aBlock['content']))
+            return false;
+
+        list( $sUrl, $iNum ) = explode('#', $aBlock['content']);
+        $iNum = (int)$iNum;
+
+        return BxDolRss::getObjectInstance('sys_page_block')->getHolder($aBlock['id'], $iNum);
+    }
+
+    /**
+     * Get content for 'menu' block type.
+     * @return string
+     */
+    protected function _getBlockMenu ($aBlock)
+    {
+        $oMenu = BxTemplMenu::getObjectInstance($aBlock['content']);
+        return $oMenu ? $oMenu->getCode () : '';
+    }
+
+    /**
+     * Get content for 'service' block type.
+     * @return string
+     */
+    protected function _getBlockService ($aBlock)
+    {
+        $aMarkers = array_merge($this->_aMarkers, [
+            'block_id' => $aBlock['id']
+        ]);
+
+        return BxDolService::callSerialized($aBlock['content'], $aMarkers);
+    }
+
+    /**
+     * Get page title.
+     * @return string
+     */
+    protected function _getPageTitle()
+    {
+        return $this->_replaceMarkers(_t($this->_aObject['title']));
+    }
+
+    /**
+     * Get page meta description.
+     * @return string
+     */
+    protected function _getPageMetaDesc()
+    {
+        return $this->_replaceMarkers(_t($this->_aObject['meta_description']));
+    }
+
+    /**
+     * Get page meta title.
+     * @return string
+     */
+    protected function _getPageMetaTitle()
+    {
+        return $this->_replaceMarkers(_t($this->_aObject['meta_title']));
+    }
+    
+    /**
+     * Get page meta image.
+     * @return string
+     */
+    protected function _getPageMetaImage()
+    {
+        return '';
+    }
+    
+    /**
+     * Get page meta keywords.
+     * @return string
+     */
+    protected function _getPageMetaKeywords()
+    {
+        return $this->_replaceMarkers(_t($this->_aObject['meta_keywords']));
+    }
+
+    /**
+     * Get page meta robots.
+     * @return string
+     */
+    protected function _getPageMetaRobots()
+    {
+        return _t($this->_aObject['meta_robots']);
+    }
+
+    /**
+     * Get page not found message.
+     * @return string
+     */
+    protected function _getPageNotFoundMsg ($mixedMsg = false)
+    {
+        $this->_oTemplate->displayPageNotFound();
+        exit;
+    }
+
+    /**
+     * Get access denied message.
+     * @return string
+     */
+    protected function _getPageAccessDeniedMsg ($mixedMsg = false)
+    {
+        return MsgBox($mixedMsg !== false ? $mixedMsg : _t('_Access denied'));
+    }
+
+    /**
+     * Get page cache object.
+     * @return cache object instance
+     */
+    protected function _getPageCacheObject ()
+    {
+        return $this->_getCacheObject ();
+    }
+
+    /**
+     * Get block cache object.
+     * @return cache object instance
+     */
+    protected function _getBlockCacheObject ()
+    {
+        return $this->_getCacheObject ('Block', 'pb');
+    }
+
+    protected function _getCacheObject ($sSuffixObj = 'Page', $sSuffixParam = 'page')
+    {
+        $sObj = '_o' . $sSuffixObj . 'CacheObject';
+        $sParam = 'sys_' . $sSuffixParam . '_cache_engine';
+
+        if ($this->{$sObj} != null) {
+            return $this->{$sObj};
+        } else {
+            $sEngine = getParam($sParam);
+            $this->{$sObj} = bx_instance ('BxDolCache' . $sEngine);
+            if (!$this->{$sObj}->isAvailable())
+                $this->{$sObj} = bx_instance ('BxDolCacheFile');
+            return $this->{$sObj};
+        }
+    }
+
+    /**
+     * Get page cache key.
+     * @param $isPrefixOnly return key prefix only.
+     * @return string
+     */
+    protected function _getPageCacheKey ($isPrefixOnly = false)
+    {
+        $s = 'page_' . $this->_aObject['object'] . '_';
+        if ($isPrefixOnly)
+            return $s;
+        $s .= $this->_getPageCacheParams ();
+        $s .= bx_site_hash() . '.php';
+        return $s;
+    }
+
+    /**
+     * Get block cache key.
+     * @param $isPrefixOnly return key prefix only.
+     * @return string
+     */
+    protected function _getBlockCacheKey ($isPrefixOnly = false, $aBlock = array())
+    {
+        $s = $this->_getPageCacheKey(true) . 'block_';
+        if ($isPrefixOnly)
+            return $s;
+        $s .= ($aBlock ? $aBlock['id'] : 0) . '_' . $this->_getPageCacheParams ();
+        $s .= bx_site_hash() . '.php';
+        return $s;
+    }
+
+    /**
+     * Additional cache key. In the case of dynamic page.
+     * For example - profile page, where each profile has own page.
+     * @return string
+     */
+    protected function _getPageCacheParams ()
+    {
+        return md5(serialize($_GET) . bx_lang_name());
+    }
+
+    /**
+     * Clean page cache.
+     * @param $isDelAllWithPagePrefix delete cache by prefix, it can be used for dynamic pages, like profile view, where for each profile separate cache is generated.
+     * @return string
+     */
+    protected function cleanCache ($isDelAllWithPagePrefix = false)
+    {
+        $a = ['Page', 'Block'];
+        $bRet = false;
+        foreach ($a as $s) {
+            $oCache = $this->{'_get' . $s . 'CacheObject'} ();
+            $sKey = $this->{'_get'. $s . 'CacheKey'} ($isDelAllWithPagePrefix);
+
+            if ($isDelAllWithPagePrefix)
+                $bRet = $bRet && $oCache->removeAllByPrefix($sKey);
+            else
+                $bRet = $bRet && $oCache->delData($sKey);
+        }
+        return $bRet;
+    }
+}
+
+/** @} */

+ 471 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBaseSearchResult.php

@@ -0,0 +1,471 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+bx_import('BxDolSearch');
+
+class BxBaseSearchResult extends BxDolSearchResult
+{
+    public $isError;
+
+    protected $oFunctions;
+
+    protected $sBrowseUrl; ///< currect browse url, used for paginate and other links in browsing
+
+    protected $sUnitTemplate = 'unit.html'; ///< common template to try to use for displaying one item
+    protected $sUnitTemplateLiveSearch = 'unit_live_search.html'; ///< common template to try to use for displaying live search results
+
+    protected $aGetParams = array(); ///< get params to keep in paginate and other browsing links
+
+    protected $iDesignBoxTemplate = 11; ///< design box ID to wrap result in, or false to disable design box wrapping
+
+    protected $aConstants;
+
+    protected $sCenterContentUnitSelector = false;
+
+    protected $aContainerAttrs = array();
+    protected $aContainerClasses = array('bx-search-result-block', 'bx-clearfix');
+
+    protected $aUnitParams = array(); ///< additional params array to pass to unit display function
+
+    function __construct($oFunctions = false)
+    {
+        parent::__construct();
+
+        if ($oFunctions)
+            $this->oFunctions = $oFunctions;
+        else
+            $this->oFunctions = BxTemplFunctions::getInstance();
+    }
+
+    function getMain()
+    {
+        // override this to return main module class
+    }
+
+    function displayResultBlock ()
+    {
+        $sCode = '';
+
+        $aData = $this->getSearchData();
+        if($this->aCurrent['paginate']['num'] > 0) {
+            $sCode .= $this->addCustomParts();
+
+            foreach ($aData as $aValue)
+                $sCode .= $this->displaySearchUnit($aValue);
+
+            $sHtmlId = $this->applyContainerId();
+            $sClasses = $this->applyContainerClass();
+
+            $sAttributes = '';
+            if(is_array($this->aContainerAttrs) && !empty($this->aContainerAttrs))
+            	foreach ($this->aContainerAttrs as $sName => $sValue)
+                    $sAttributes .= ' ' . $sName . '="' . $sValue . '"';
+
+            $sCode = BxDolTemplate::getInstance()->parseHtmlByName('search_result_block.html', [
+                'html_id' => $sHtmlId,
+                'class' => $sClasses,
+                'attrs' => $sAttributes,
+                'content' => $sCode,
+                'bx_if:do_center' => [
+                    'condition' => !$this->_bLiveSearch && $this->sCenterContentUnitSelector,
+                    'content' => [
+                        'html_id' => $sHtmlId,
+                        'selector_content' => $this->sCenterContentUnitSelector
+                    ]
+                ]
+            ]);
+        }
+
+        bx_alert('simple_search', 'show_data', 0, false, [
+            'object' => &$this->aCurrent, 
+            'mode' => $this->_sMode, 
+            'search_results' => &$sCode
+        ]);
+
+        return $sCode;
+    }
+
+    function displayResultBlockEmpty ()
+    {
+        return BxDolTemplate::getInstance()->parseHtmlByName('search_result_block.html', [
+            'html_id' => $this->applyContainerId(),
+            'class' => 'bx-search-result-block bx-search-result-block-empty',
+            'attrs' => '',
+            'content' => parent::displayResultBlockEmpty(),
+            'bx_if:do_center' => [
+                'condition' => false,
+                'content' => []
+            ]
+        ]);
+    }
+
+    function displaySearchBox ($sContent, $sPaginate = '')
+    {
+        $sContent = BxDolTemplate::getInstance()->parseHtmlByName('paginate_block.html', array(
+            'bx_if:paginate_top' => array(
+                'condition' => (isset($this->aCurrent['paginate']['on_top']) && $this->aCurrent['paginate']['on_top'] == true) && $sPaginate != '',
+                'content' => array(
+                    'paginate' => $sPaginate
+                ),
+            ),
+            'bx_if:paginate_bottom' => array(
+                'condition' => (!isset($this->aCurrent['paginate']['on_bottom']) || $this->aCurrent['paginate']['on_bottom'] != false) && $sPaginate != '',
+                'content' => array(
+                    'paginate' => $sPaginate
+                ),
+            ),
+            'content' => $sContent
+        ));
+	
+        $sMenu = $this->getDesignBoxMenu();
+
+        if ($this->id) {
+            $sTitle = _t($this->aCurrent['title']);
+            $sCode = $this->oFunctions->designBoxContent($sTitle, $sContent, $this->iDesignBoxTemplate, $sMenu);
+            return BxDolTemplate::getInstance()->parseHtmlByName('designbox_container.html', array(
+                'class_add' => ' bx-search-results',
+                'bx_if:show_html_id' => array(
+                    'condition' => true,
+                    'content' => array(
+                        'html_id' => 'bx-page-block-' . $this->id
+                    ),
+                ),
+                'content' => $sCode
+            ));
+        }
+
+        $this->addPageRssLink ();
+
+        return array(        	
+            'content' => $sContent,
+            'menu' => $sMenu,
+        );
+    }
+
+    function displaySearchUnit ($aData)
+    {
+        $oMain = $this->getMain();
+        return $oMain->_oTemplate->unit($aData, $this->bProcessPrivateContent, $this->_bLiveSearch ? $this->sUnitTemplateLiveSearch : $this->sUnitTemplate, $this->aUnitParams);
+    }
+
+    function setUnitTemplate ($s)
+    {
+        $this->sUnitTemplate = $s;
+    }
+
+    function getDesignBoxMenu ()
+    {
+        return array();
+    }
+
+    protected function addPageRssLink ()
+    {
+        if (false === ($sLink = $this->getRssPageUrl ()))
+            return;
+
+        if (!($oTemplate = BxDolTemplate::getInstance()))
+            return;
+
+        $oTemplate->addPageRssLink($this->aCurrent['title'], $sLink);
+    }
+
+    function getRssPageUrl ()
+    {
+        if (!isset($this->aCurrent['rss']) || !$this->aCurrent['rss']['link'])
+            return false;
+
+        $oPermalinks = BxDolPermalinks::getInstance();
+        return bx_absolute_url(bx_append_url_params($oPermalinks->permalink($this->aCurrent['rss']['link']), 'rss=1'));
+    }
+
+    function showAdminActionsPanel($sWrapperId, $aButtons, $sCheckboxName = 'entry', $bSelectAll = true, $bSelectAllChecked = false, $sCustomHtml = '')
+    {
+        $aBtns = array();
+        foreach ($aButtons as $k => $v) {
+            if(is_array($v)) {
+                $aBtns[] = $v;
+                continue;
+            }
+
+            $aBtns[] = array(
+                'type' => 'submit',
+                'name' => $k,
+                'value' => '_' == $v[0] ? _t($v) : $v,
+                'onclick' => '',
+            );
+        }
+
+        return BxDolTemplate::getInstance()->parseHtmlByName('adminActionsPanel.html', array(
+            'bx_repeat:buttons' => $aBtns,
+            'bx_if:custom_html' => array(
+                'condition' => strlen($sCustomHtml) > 0,
+                'content' => array(
+                    'custom_html' => $sCustomHtml,
+                )
+            ),
+            'bx_if:selectAll' => array(
+                'condition' => $bSelectAll,
+                'content' => array(
+                    'wrapper_id' => $sWrapperId,
+                    'checkbox_name' => $sCheckboxName,
+                    'checked' => ($bSelectAll && $bSelectAllChecked ? 'checked="checked"' : '')
+                )
+            ),
+        ));
+    }
+
+    function showAdminFilterPanel($sFilterValue, $sInputId = 'filter_input_id', $sCheckboxId = 'filter_checkbox_id', $sFilterName = 'filter', $sOnApply = '')
+    {
+        $sFilterValue = bx_html_attribute($sFilterValue);
+        $isChecked = $sFilterValue ? ' checked="checked" ' : '';
+
+        if(empty($sOnApply))
+            $sOnApply = "on_filter_apply(this, '" . $sInputId . "', '" . $sFilterName . "')";
+
+        return BxDolTemplate::getInstance()->parseHtmlByName('adminFilterPanel.html', array(
+            'input_id' => $sInputId,
+            'filter_value' => $sFilterValue,
+            'checkbox_id' => $sCheckboxId,
+            'is_checked' => $isChecked,
+            'on_apply' => $sOnApply,
+        ));
+    }
+
+    function showPagination($bAdmin = false, $bChangePage = true, $bPageReload = true)
+    {
+        $sOnClick = $this->getCurrentOnclick([], false);
+
+        $aPaginate = [
+            'page_url' => $this->getCurrentUrl([], false),
+            'on_change_page' => $sOnClick,
+            'num' => $this->aCurrent['paginate']['num'],
+            'per_page' => $this->aCurrent['paginate']['perPage'],
+            'start' => $this->aCurrent['paginate']['start'],
+        ];
+
+        if(!empty($this->aCurrent['paginate']['total'])) {
+            if(is_numeric($this->aCurrent['paginate']['total']))
+                $aPaginate['total'] = (int)$this->aCurrent['paginate']['total'];
+            else
+                $aPaginate['total'] = $this->getTotal();
+        }
+
+        $oPaginate = new BxTemplPaginate($aPaginate);
+        return $sOnClick ? $oPaginate->getSimplePaginate() : $oPaginate->getPaginate();
+    }
+
+    /**
+     * Get current browse URL with current page and additional params
+     * @param $aAdditionalParams set custom additional params as key value pair
+     * @param $bReplacePagesParams replace paginate params with current values or leave markers for use in paginate class
+     * @return ready to use URL string with BX_DOL_URL_ROOT added in the beginning
+     */
+    protected function getCurrentUrl($aAdditionalParams = array(), $bReplacePagesParams = true)
+    {
+        if (BX_DOL_SEARCH_KEYWORD_PAGE === $this->sBrowseUrl || $this->bForceAjaxPaginate)
+            return 'javascript:void(0);';
+
+        $oPermalinks = BxDolPermalinks::getInstance();
+
+        $sUrlStart = bx_absolute_url($oPermalinks->permalink($this->sBrowseUrl));
+
+        return $this->addAdditionalUrlParams($sUrlStart, $aAdditionalParams, $bReplacePagesParams);
+    }
+
+    /**
+     * Get current browse URL with current page and additional params
+     * @param $aAdditionalParams set custom additional params as key value pair
+     * @param $bReplacePagesParams replace paginate params with current values or leave markers for use in paginate class
+     * @return ready to use URL string with BX_DOL_URL_ROOT added in the beginning
+     */
+    protected function getCurrentOnclick($aAdditionalParams = array(), $bReplacePagesParams = true)
+    {
+        if (BX_DOL_SEARCH_KEYWORD_PAGE !== $this->sBrowseUrl && !$this->bForceAjaxPaginate)
+            return '';
+
+        if (BX_DOL_SEARCH_KEYWORD_PAGE === $this->sBrowseUrl) {
+
+            $sLoadDynamicUrl = BX_DOL_URL_ROOT . 'searchKeywordContent.php?searchMode=ajax&section[]=' . rawurlencode($this->aCurrent['name']) . ($this->_sCategoryObject ? '&cat=' . rawurlencode($this->_sCategoryObject) : '');
+            $sLoadDynamicUrl = $this->addAdditionalUrlParams($sLoadDynamicUrl, $aAdditionalParams, $bReplacePagesParams);
+
+            $sKeyword = bx_get('keyword');
+            if ($sKeyword !== false && mb_strlen($sKeyword) > 0)
+                $sLoadDynamicUrl = bx_append_url_params($sLoadDynamicUrl, 'keyword=' . rawurlencode($sKeyword));
+
+            return "return !loadDynamicBlockAuto(this, '{$sLoadDynamicUrl}');";
+
+        } else {
+            
+            return "return !loadDynamicBlockAutoPaginate(this, '{start}', '{per_page}', " . (empty($aAdditionalParams) ? 'undefined' : "'" . trim($this->addAdditionalUrlParams('', $aAdditionalParams, false, false), '&?') . "'") . ");"; 
+
+        }
+    }
+
+    protected function addAdditionalUrlParams($sUrl, $aAdditionalParams, $bReplacePagesParams, $bAddPaginateParams = true)
+    {
+        if ($bAddPaginateParams) { // add pages params
+            $sUrl = bx_append_url_params($sUrl, array (
+                'type' => $this->_sMetaType,
+                'start' => $bReplacePagesParams ? (int)$this->aCurrent['paginate']['start'] : '{start}',
+                'per_page' => $bReplacePagesParams ? (int)$this->aCurrent['paginate']['perPage'] : '{per_page}',
+            ), false);
+        }
+
+        foreach ($this->aGetParams as $sGetParam) {
+            $sValue = false;
+            if (isset($aAdditionalParams[$sGetParam]))
+                $sValue = $aAdditionalParams[$sGetParam];
+            elseif (false !== bx_get($sGetParam))
+                $sValue = bx_get($sGetParam);
+            if (false !== $sValue)
+                $sUrl = bx_append_url_params($sUrl, $sGetParam . '=' . rawurlencode($sValue));
+        }
+
+        return $sUrl;
+    }
+
+    function clearFilters ($aPassParams = array(), $aPassJoins = array())
+    {
+        //clear sorting
+        $this->aCurrent['sorting'] = 'last';
+
+        //clear restrictions
+        foreach ($this->aCurrent['restriction'] as $sKey => $aValue) {
+            if (!in_array($sKey, $aPassParams))
+                $this->aCurrent['restriction'][$sKey]['value'] = '';
+        }
+
+        //clear unnecessary joins (remains only profile join)
+        $aPassJoins[] = 'profile';
+        $aTemp = array();
+        foreach ($aPassJoins as $sValue) {
+            if (isset($this->aCurrent['join'][$sValue]) && is_array($this->aCurrent['join'][$sValue]))
+                $aTemp[$sValue] = $this->aCurrent['join'][$sValue];
+        }
+        $this->aCurrent['join'] = $aTemp;
+    }
+
+    function fillFilters ($aParams)
+    {
+        // transform all given values to fields values
+        if (is_array($aParams)) {
+            foreach ($aParams as $sKey => $mixedValue) {
+                if (isset($this->aCurrent['restriction'][$sKey]))
+                    $this->aCurrent['restriction'][$sKey]['value'] = $mixedValue;
+            }
+        }
+    }
+    
+    function fillFiltersByObjects ($aParams)
+    {
+        // transform all given values to fields values
+        if (is_array($aParams)) {
+            foreach ($aParams as $sKey => $mixedValue) {
+                $this->aCurrent['restriction'][$sKey] = $mixedValue;
+            }
+        }
+    }
+
+    /**
+     * Set design box template id to use to wrap search results in
+     */
+    function setDesignBoxTemplateId ($i)
+    {
+        $this->iDesignBoxTemplate = $i;
+    }
+
+    /**
+     * Set unit class selector for content centering 
+     */
+    function setCenterContentUnitSelector ($s)
+    {
+        $this->sCenterContentUnitSelector = $s;
+    }
+
+    /**
+     * Add ID to search result container 
+     * @param $string HTML ID
+     */
+    function applyContainerId()
+    {
+        return 'bx-search-result-block-' . rand(0, PHP_INT_MAX);
+    }
+
+    /**
+     * Add class to search result container 
+     * @param $mixed CSS class name string or array of classes
+     */
+    function addContainerClass ($mixed)
+    {
+        if (!is_array($mixed))
+            $mixed = array($mixed);
+
+        foreach ($mixed as $s)
+            if (false === array_search($s, $this->aContainerClasses))
+                $this->aContainerClasses[] = $s;
+    }
+
+    /**
+     * Remove class from search result container 
+     * @param $mixed CSS class name string or array of classes
+     */
+    function removeContainerClass ($mixed)
+    {
+        if (!is_array($mixed))
+            $mixed = array($mixed);
+
+        foreach ($mixed as $s)
+            if (false !== ($i = array_search($s, $this->aContainerClasses)))
+                unset($this->aContainerClasses[$i]);
+    }
+
+    /**
+     * Apply class(es) for search result container 
+     * @return string with a list of classes.
+     */
+    function applyContainerClass ()
+    {
+        $sModule = $this->getMain() ? $this->getMain()->getName() : 'sys';
+        return implode(' ', $this->aContainerClasses) . ' '. str_replace('_', '-', $sModule) . '-wrapper';
+    }
+
+    /**
+     * Add attribute(s) to search result container 
+     * @param $aAttributes an array of Key -> Value pair(s).
+     */
+    function addContainerAttribute ($aAttributes)
+    {
+    	if(empty($aAttributes) || !is_array($aAttributes))
+    		return;
+
+    	$this->aContainerAttrs = array_merge($this->aContainerAttrs, $aAttributes);
+    }
+
+	/**
+     * Remove attribute(s) from search result container 
+     * @param $mixedName attribute name or an array of names
+     */
+    function removeContainerAttribute ($mixedName)
+    {
+        if(!is_array($mixedName))
+            $mixedName = array($mixedName);
+
+		$this->aContainerAttrs = array_diff_assoc($this->aContainerAttrs, array_flip($mixedName));
+    }
+
+    public function setUnitParams ($aParamsAdd = array(), $aParamsRemove = array())
+    {
+        if ($aParamsAdd)
+            $this->aUnitParams = array_merge($this->aUnitParams, $aParamsAdd);
+        if ($aParamsRemove)
+            $this->aUnitParams = array_diff_key($this->aUnitParams, $aParamsRemove);
+    }
+}
+
+/** @} */

+ 797 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBaseServiceAccount.php

@@ -0,0 +1,797 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * System service for creating system profile functionality.
+ */
+
+class BxBaseServiceAccount extends BxDol
+{
+    protected $_oAccountForms;
+    protected $_oAccountQuery;
+
+    public function __construct()
+    {
+        parent::__construct();
+
+        $this->_oAccountForms = new BxTemplAccountForms();
+        $this->_oAccountQuery = BxDolAccountQuery::getInstance();
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-account Account
+     * @subsubsection bx_system_general-create_account_form create_account_form
+     * 
+     * @code bx_srv('system', 'create_account_form', [], 'TemplServiceAccount'); @endcode
+     * @code {{~system:create_account_form:TemplServiceAccount[{"no_login_text":true, "no_auth_buttons":true}]~}} @endcode
+     * 
+     * Join form.
+     * @param $aParams array of additional params, such as: 
+     *          - no_login_text
+     *          - no_auth_buttons
+     * 
+     * @see BxBaseServiceAccount::serviceGetCreatePostForm
+     */
+    /** 
+     * @ref bx_system_general-create_account_form "create_account_form"
+     */
+    public function serviceCreateAccountForm ($aParams = array())
+    {
+        $bApi = bx_is_api();
+
+        if (isLogged() && 'create-account' == bx_get('i')) {
+            if (!$bApi) {
+                header('Location: ' . BX_DOL_URL_ROOT);
+                exit;
+            }
+            else
+                return [
+                    ['id' => 2, 'type' => 'redirect', 'data' => ['uri' => '/']],
+                ];
+        }
+
+        if($bApi) {
+            $mixedResult = $this->_oAccountForms->createAccountForm($aParams);
+            if(is_array($mixedResult))
+                return [bx_api_get_block('form', $mixedResult, [
+                    'ext' => [
+                        'request' => ['url' => '/api.php?r=system/create_account_form/TemplServiceAccount', 'immutable' => true]
+                    ]
+                ])];
+            else if($mixedResult === true){
+                $sRedirectUrl = BxDolForm::getSubmittedValue('relocate','post');
+                if(empty($sRedirectUrl)){
+                    $sRedirectUrl = '/';
+                }
+                else{
+                    $parts = parse_url($sRedirectUrl);
+                    parse_str($parts['query'], $query);
+                    unset($query['_q']);
+                    $parts['query'] = http_build_query($query);
+                    $sRedirectUrl = $parts['path'] . (!empty($parts['query']) ? '?' . $parts['query'] : '');
+                }
+                return [
+                    bx_api_get_block('redirect', ['uri' => $sRedirectUrl]),
+                    bx_api_get_block('login', ['session' => BxDolSession::getInstance()->getId()], ['id' => 2]),
+                ];
+            }
+            else
+                return [bx_api_get_msg($mixedResult)];
+        }
+
+        $oPemalink = BxDolPermalinks::getInstance();
+
+        if (isset($_SERVER['HTTP_REFERER']) && 0 === mb_stripos($_SERVER['HTTP_REFERER'], BX_DOL_URL_ROOT)) { // remember referrer
+            $sJoinReferrer = $_SERVER['HTTP_REFERER'];
+            $aNoRelocatePages = array('forgot-password', 'login', 'create-account', 'logout');
+            foreach ($aNoRelocatePages as $s) {
+                if (false !== mb_stripos($sJoinReferrer, $s)) {
+                    $sJoinReferrer = '';
+                    break;
+                }
+            }   
+            if ($sJoinReferrer)
+                BxDolSession::getInstance()->setValue('join-referrer', $sJoinReferrer);
+        }
+
+        $aTmplVarsAuth = [];
+        if(!isset($aParams['no_auth_buttons']) || false === (bool)$aParams['no_auth_buttons'])
+            $aTmplVarsAuth['content'] = BxDolService::call('system', 'member_auth_code', array(), 'TemplServiceLogin');
+
+        $aTmplVarsForm = [];
+        if(!(bool)getParam('sys_account_disable_join_form'))
+            $aTmplVarsForm['content'] = $this->_oAccountForms->createAccountForm($aParams);
+
+        $aTmplVarsLogin = [];
+        if(!isset($aParams['no_login_text']) || false === (bool)$aParams['no_login_text'])
+            $aTmplVarsLogin['url'] = bx_absolute_url($oPemalink->permalink('page.php?i=login'));
+        
+        return BxDolTemplate::getInstance()->parseHtmlByName('block_join.html', [
+            'bx_if:show_auth' => [
+                'condition' => !empty($aTmplVarsAuth),
+                'content' => $aTmplVarsAuth
+            ],
+            'bx_if:show_form' => [
+                'condition' => !empty($aTmplVarsForm),
+                'content' => $aTmplVarsForm,
+            ],
+            'bx_if:show_login' => [
+                'condition' => !empty($aTmplVarsLogin),
+                'content' => $aTmplVarsLogin
+            ],
+            'agreement' => _t('_sys_form_account_input_agreement_value', bx_absolute_url($oPemalink->permalink('page.php?i=terms')), bx_absolute_url($oPemalink->permalink('page.php?i=privacy'))),
+        ]);
+    }
+
+    public function serviceAccountSettingsEmail ($iAccountId = false)
+    {
+        if (false === $iAccountId)
+            $iAccountId = getLoggedId();
+        
+        $bApi = bx_is_api();
+        
+        $mixedResult =$this->_oAccountForms->editAccountEmailSettingsForm($iAccountId);
+        
+        if($bApi) {
+            if(is_array($mixedResult)){
+                if (isset($mixedResult['form'] )){
+                    return [
+                        bx_api_get_msg($mixedResult['msg']),
+                        bx_api_get_block('form', $mixedResult['form'], [
+                        'ext' => [
+                            'request' => ['url' => '/api.php?r=system/account_settings_email/TemplServiceAccount', 'immutable' => true]
+                        ]
+                    ])];
+                }
+                else{
+                return [bx_api_get_block('form', $mixedResult, [
+                    'ext' => [
+                        'request' => ['url' => '/api.php?r=system/account_settings_email/TemplServiceAccount', 'immutable' => true]
+                    ]
+                ])];
+                }
+            }
+            else
+                return [bx_api_get_msg($mixedResult)];
+        }    
+        
+        return $mixedResult;
+    }
+
+    public function serviceAccountSettingsPassword ($iAccountId = false)
+    {
+        if (false === $iAccountId)
+            $iAccountId = getLoggedId();
+        
+        $bApi = bx_is_api();
+        
+        $mixedResult =$this->_oAccountForms->editAccountPasswordSettingsForm($iAccountId);
+        if($bApi) {
+            if(is_array($mixedResult)){
+                if (isset($mixedResult['form'] )){
+                    return [
+                        bx_api_get_msg($mixedResult['msg']),
+                        bx_api_get_block('form', $mixedResult['form'], [
+                        'ext' => [
+                            'request' => ['url' => '/api.php?r=system/account_settings_password/TemplServiceAccount', 'immutable' => true]
+                        ]
+                    ])];
+                }
+                else{
+                    return [bx_api_get_block('form', $mixedResult, [
+                        'ext' => [
+                            'request' => ['url' => '/api.php?r=system/account_settings_password/TemplServiceAccount', 'immutable' => true]
+                        ]
+                    ])];
+                }
+            }
+            else
+                return [bx_api_get_msg($mixedResult)];
+        }
+        
+        return $mixedResult;
+    }
+
+    public function serviceAccountSettingsInfo ($iAccountId = false)
+    {
+        if (false === $iAccountId)
+            $iAccountId = getLoggedId();
+        
+        $bApi = bx_is_api();
+        
+        $mixedResult =$this->_oAccountForms->editAccountInfoForm($iAccountId);
+        
+        if($bApi) {
+            if(is_array($mixedResult)){
+                if (isset($mixedResult['form'] )){
+                    return [
+                        bx_api_get_msg($mixedResult['msg']),
+                        bx_api_get_block('form', $mixedResult['form'], [
+                        'ext' => [
+                            'request' => ['url' => '/api.php?r=system/account_settings_info/TemplServiceAccount', 'immutable' => true]
+                        ]
+                    ])];
+                }
+                else{
+                return [bx_api_get_block('form', $mixedResult, [
+                    'ext' => [
+                            'request' => ['url' => '/api.php?r=system/account_settings_info/TemplServiceAccount', 'immutable' => true]
+                    ]
+                ])];
+                }
+            }
+            else
+                return [bx_api_get_msg($mixedResult)];
+        }    
+        return $mixedResult;
+    }
+
+    public function serviceAccountSettingsDelAccount ($iAccountId = false)
+    {
+        if (!$iAccountId)
+        	$iAccountId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iAccountId)
+            $iAccountId = getLoggedId();
+
+        $bApi = bx_is_api();
+        
+        $mixedResult = $this->_oAccountForms->deleteAccountForm($iAccountId);
+        
+        if($bApi) {
+            if(is_array($mixedResult)){
+                if (isset($mixedResult['form'] )){
+                    return [
+                        bx_api_get_msg($mixedResult['msg']),
+                        bx_api_get_block('form', $mixedResult['form'], [
+                        'ext' => [
+                            'request' => ['url' => '/api.php?r=system/account_settings_del_account/TemplServiceAccount', 'immutable' => true]
+                        ]
+                    ])];
+                }
+                else{
+                return [bx_api_get_block('form', $mixedResult, [
+                    'ext' => [
+                        'request' => ['url' => '/api.php?r=system/account_settings_del_account/TemplServiceAccount', 'immutable' => true]
+                    ]
+                ])];
+                }
+            }
+            else if(is_string($mixedResult))
+                return [
+                    bx_api_get_msg($mixedResult)
+                ];
+            else
+                return [];
+        }
+
+        return $mixedResult;
+    }
+
+    /**
+     * Display unsubscribe from newsletters form
+     */
+    public function serviceUnsubscribeNews()
+    {
+        return $this->_unsubscribeForm('sys_unsubscribe_news');
+    }
+
+    /**
+     * Display unsubscribe from notifications form
+     */
+    public function serviceUnsubscribeNotifications()
+    {
+        return $this->_unsubscribeForm('sys_unsubscribe_updates');
+    }
+    
+    /**
+     * Display confirmation statuses list
+     */
+    public function serviceGetConfirmationTypes()
+    {
+        $aResult = array();
+        $aChoices = array(BX_ACCOUNT_CONFIRMATION_NONE, BX_ACCOUNT_CONFIRMATION_EMAIL, BX_ACCOUNT_CONFIRMATION_PHONE, BX_ACCOUNT_CONFIRMATION_EMAIL_PHONE, BX_ACCOUNT_CONFIRMATION_EMAIL_OR_PHONE);
+        foreach($aChoices as $sChoice)
+            $aResult[$sChoice] = _t('_sys_acc_confirmation_type_' . $sChoice);
+        
+        return $aResult;
+    }
+
+    protected function _unsubscribeForm($sDisplay)
+    {
+        $iAccountId = bx_process_input(bx_get('id'));
+        $sCode = bx_process_input(bx_get('code'));
+
+        // validate input params
+
+        $oAccount = BxDolAccount::getInstance($iAccountId);
+        if (!$oAccount)
+            return MsgBox(_t('_sys_txt_unsubscribe_wrong_link'));
+
+        if ($sCode != $oAccount->getEmailHash())
+            return MsgBox(_t('_sys_txt_unsubscribe_wrong_link'));
+
+        $oForm = BxDolForm::getObjectInstance('sys_unsubscribe', $sDisplay);
+        if (!$oForm)
+            return MsgBox(_t('_sys_txt_unsubscribe_error_occured'));
+
+        // show form
+
+        $aAccountInfo = $oAccount->getInfo();
+        $aAccountInfo['code'] = $sCode;
+        $oForm->initChecker($aAccountInfo);
+
+        if ($oForm->isSubmittedAndValid()) {
+
+            if (!$oForm->update($oAccount->id()))
+                return MsgBox(_t('_sys_txt_unsubscribe_error_occured'));
+
+            return MsgBox(_t('_sys_txt_unsubscribe_email_settings_update_success'));
+        }
+
+        return '<div class="bx-def-padding-bottom">' . _t("_sys_txt_unsubscribe_info", $aAccountInfo['email']) . '</div>' . $oForm->getCode();
+    }
+
+    /**
+     * Display email confirmation form, if confirmation code is provided then try to confirm profile
+     */
+    public function serviceEmailConfirmation($sMsg = false)
+    {
+        // if user is logged in and email is confirmed then just display a message
+        if (isLogged() && !bx_get('code')) {
+            $oAccount = BxDolAccount::getInstance();
+            if ($oAccount->isConfirmedEmail())
+                return MsgBox(_t("_sys_txt_confirm_email_already_confirmed"));
+        }
+
+        // if confirmation code is provided in the URL - perform email confirmation right away
+        if (bx_get('code') !== false)
+            return $this->confirmEmail(bx_process_input(bx_get('code')));
+
+        // if user requested to resend verification letter then send letter and display message
+        if (bx_process_input(bx_get('resend')) && isLogged()) {
+            $oAccount = BxDolAccount::getInstance();
+            if ($oAccount->sendConfirmationEmail())
+                $sMsg = _t('_sys_txt_confirm_email_sent');
+            else
+                $sMsg = _t('_sys_txt_confirm_email_sent_failed');
+            return MsgBox($sMsg);
+        }
+
+        // show and process code verification form
+
+        $oForm = BxDolForm::getObjectInstance('sys_confirm_email', 'sys_confirm_email');
+        if (!$oForm)
+            return MsgBox(_t("_sys_txt_confirm_email_error_occured"));
+
+        $oForm->initChecker();
+
+        if ($oForm->isSubmittedAndValid()) {
+
+            $oKey = BxDolKey::getInstance();
+            if (!$oKey)
+                return MsgBox(_t("_sys_txt_confirm_email_error_occured"));
+            else
+                return $this->confirmEmail(trim($oForm->getCleanValue('code')));
+
+        }
+
+        return '<div class="bx-def-padding-sec-bottom">' . _t("_sys_txt_confirm_email_desc") . '</div>' . $oForm->getCode();
+    }
+    
+    /**
+     * Display phone confirmation forms, if confirmation code is provided then try to confirm profile
+     */
+    public function servicePhoneConfirmation($sMsg = false)
+    {
+        // if user is logged in and phone is confirmed then just display a message
+        if (isLogged()) {
+            $oAccount = BxDolAccount::getInstance();
+            if ($oAccount->isConfirmedPhone())
+                return MsgBox(_t("_sys_txt_confirm_phone_already_confirmed"));
+            
+            $iStep = 1;
+            $a = $oAccount->getInfo();
+            $sPhoneNumber = $a['phone'];
+            
+            $oSession = BxDolSession::getInstance();           
+            if ($sPhoneNumber != "" && $oSession->isValue(BX_ACCOUNT_SESSION_KEY_FOR_PHONE_ACTIVATEION_CODE)){
+                 $iStep = 2;
+            }
+        
+            if (bx_get('step'))
+                $iStep = (int)bx_get('step');
+        
+            // show and process phone set form
+            if ($iStep == 1){
+                $oForm = BxDolForm::getObjectInstance('sys_confirm_phone', 'sys_confirm_phone_set_phone');
+                if (!$oForm)
+                    return MsgBox(_t("_sys_txt_confirm_phone_set_phone_error_occured"));
+            
+                $oForm->initChecker(array('phone' => $sPhoneNumber));
+                if ($oForm->isSubmittedAndValid()) {
+                    $oAccount->updatePhone(trim($oForm->getCleanValue('phone')));
+                    $sActivationCode = rand(1000, 9999);
+                    $sActivationText =_t('_sys_txt_confirm_phone_sms_text', $sActivationCode);
+                    $ret = null;
+                    bx_alert('account', 'before_confirm_phone_send_sms', $oAccount->id(), bx_get_logged_profile_id(), array('phone_number' => $sPhoneNumber, 'sms_text' => $sActivationText, 'override_result' => &$ret));
+                    if ($ret === null) 
+                    {
+                        $oTwilio = BxDolTwilio::getInstance();
+                        if(!$oTwilio->sendSms($sPhoneNumber,  $sActivationText)){
+                            return MsgBox(_t("_sys_txt_confirm_phone_send_sms_error_occured"));
+                        }
+                    }
+
+                    $oSession->setValue(BX_ACCOUNT_SESSION_KEY_FOR_PHONE_ACTIVATEION_CODE, $sActivationCode);
+                    header('Location: ' . bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=confirm-phone')));
+                } 
+                return '<div class="bx-def-padding-sec-bottom">' . _t("_sys_txt_confirm_phone_set_phone_desc_set_phone") . '</div>' .$oForm->getCode();
+            }
+        
+            if ($iStep == 2){
+                $oForm = BxDolForm::getObjectInstance('sys_confirm_phone', 'sys_confirm_phone_confirmation');
+                if (!$oForm)
+                    return MsgBox(_t("_sys_txt_confirm_phone_set_phone_confirmation_occured"));
+            
+                $oForm->initChecker();
+            
+                if ($oForm->isSubmittedAndValid()) {
+                    $oAccount->updatePhoneConfirmed(true);
+                    $oSession->unsetValue(BX_ACCOUNT_SESSION_KEY_FOR_PHONE_ACTIVATEION_CODE);
+                    return MsgBox(_t("_sys_txt_confirm_phone_confirm_success"));
+                } 
+                return '<div class="bx-def-padding-sec-bottom">' . _t("_sys_txt_confirm_phone_set_phone_desc_phone_confirmation") . '</div>' .$oForm->getCode();
+            }
+        }
+    }
+
+    /**
+     * Perform email confirmation
+     */
+    public function confirmEmail($sKey)
+    {
+        $mixedAccountId = $this->_confirmEmail($sKey);
+        if(is_string($mixedAccountId) && !is_numeric($mixedAccountId))
+            return MsgBox($mixedAccountId);
+
+        // redirect with success message
+        $sUrl = getParam('sys_redirect_after_email_confirmation');
+        bx_alert('account', 'after_email_confirmation', $mixedAccountId, false, ['override_result' => &$sUrl]);
+
+        $oTemplate = BxDolTemplate::getInstance();
+        $oTemplate->setPageNameIndex (BX_PAGE_TRANSITION);
+        $oTemplate->setPageHeader (_t('_sys_txt_confirm_email_success'));
+        $oTemplate->setPageContent ('page_main_code', MsgBox(_t('_sys_txt_confirm_email_success')));
+        $oTemplate->setPageContent ('url_relocate', bx_absolute_url(BxDolPermalinks::getInstance()->permalink($sUrl)));
+
+        BxDolTemplate::getInstance()->getPageCode();
+        exit;
+    }
+
+    public function serviceConfirmEmail($sKey)
+    {
+        $mixedAccountId = $this->_confirmEmail($sKey);
+        return is_int($mixedAccountId) || is_numeric($mixedAccountId);
+    }
+
+    public function serviceGetOptionsResetPasswordRedirect()
+    {
+        $aResult = array();
+
+        $aChoices = array('home', 'profile', 'profile_edit', 'custom');
+        foreach($aChoices as $sChoice) 
+            $aResult[] = array('key' => $sChoice, 'value' => _t('_adm_stg_cpt_option_value_sys_account_reset_password_redirect_' . $sChoice));
+
+        return $aResult;
+    }
+
+    public function serviceGetOptionsSwitchToProfileRedirect()
+    {
+        $aResult = array();
+
+        $aChoices = array('back', 'home', 'profile', 'custom');
+        foreach($aChoices as $sChoice) 
+            $aResult[] = array('key' => $sChoice, 'value' => _t('_adm_stg_cpt_option_value_sys_account_switch_to_profile_redirect_' . $sChoice));
+
+        return $aResult;
+    }
+
+    public function serviceGetOptionsPruningInterval()
+    {
+        $aResult = array();
+
+        $aChoices = array('no', 'no_login_delete', 'no_login_suspend', 'no_confirm_delete', 'no_profile_delete');
+        foreach($aChoices as $sChoice) 
+            $aResult[] = array('key' => $sChoice, 'value' => _t('_adm_stg_cpt_option_value_sys_account_pruning_' . $sChoice));
+
+        return $aResult;
+    }
+            
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-account Account
+     * @subsubsection bx_system_general-forgot_password forgot_password
+     * 
+     * @code bx_srv('system', 'forgot_password', [], 'TemplServiceAccount'); @endcode
+     * @code {{~system:forgot_password:TemplServiceAccount~}} @endcode
+     * 
+     * Display forgot password form, 
+     * if reset password key is provided then reset password
+     * 
+     * @see BxBaseServiceAccount::serviceForgotPassword
+     */
+    /** 
+     * @ref bx_system_general-forgot_password "forgot_password"
+     */
+    public function serviceForgotPassword()
+    {
+        if(isLogged()){
+            header('Location: ' . BX_DOL_URL_ROOT);
+            exit;
+        }
+        
+        $bApi = bx_is_api();
+
+        if (bx_get('key') !== false)
+            return $this->resetPassword();
+
+        $oForm = BxDolForm::getObjectInstance('sys_forgot_password', 'sys_forgot_password');
+        if (!$oForm)
+            return '';
+
+        $bNeedCheckEmailAndPhone = true;
+        $sCaptionKey = "_sys_txt_forgot_pasword_by_both_desc";
+        if (BxDolAccount::isNeedConfirmPhone()){
+            if (!BxDolAccount::isNeedConfirmEmail()){
+                unset($oForm->aInputs['email']);
+                $sCaptionKey = "_sys_txt_forgot_pasword_by_phone_desc";
+                $oForm->aInputs['phone']['checker']['func'] = 'PhoneExist';
+                $bNeedCheckEmailAndPhone = false;
+            }
+        }
+        else{
+            unset($oForm->aInputs['phone']);
+            $sCaptionKey = "_sys_txt_forgot_pasword_desc";
+            $oForm->aInputs['email']['checker']['func'] = 'EmailExist';
+            $bNeedCheckEmailAndPhone = false;
+        }
+
+        if ((BxDolAccount::isNeedConfirmPhone() && BxDolAccount::isNeedConfirmEmail())){
+            if (isset($oForm->aInputs['phone']))
+                $oForm->aInputs['phone']['required'] = false;
+            if (isset($oForm->aInputs['email']))
+                $oForm->aInputs['email']['required'] = false;
+        }
+
+        $oForm->initChecker();
+
+        $bShowErrorEmptyBoth = false;
+        if ($oForm->isSubmitted() && $bNeedCheckEmailAndPhone && $oForm->getCleanValue('phone') == '' && $oForm->getCleanValue('email') == ''){
+            $bShowErrorEmptyBoth = true;
+            $oForm->setValid(false);
+        }
+
+        if ($oForm->isSubmittedAndValid()) {
+            $sEmail = $oForm->getCleanValue('email');
+            $sPhone = $oForm->getCleanValue('phone');
+
+            if (isset($oForm->aInputs['email']) &&  $sEmail != '') {
+                $oAccount = BxDolAccount::getInstance($this->_oAccountQuery->getIdByEmail($sEmail));
+
+                if($oAccount && $oAccount->sendResetPasswordEmail())
+                    $sResultMsg = MsgBox(_t("_sys_txt_forgot_pasword_check_email"));
+                else
+                    $sResultMsg = MsgBox(_t("_sys_txt_forgot_pasword_email_send_failed"));
+
+                $sForm = '';
+            }
+
+            if (isset($oForm->aInputs['phone']) &&  $sPhone != '') {   
+                $iAccountId = $this->_oAccountQuery->getIdByPhone($sPhone);
+                $aAccountInfo = $this->_oAccountQuery->getInfoById($iAccountId);
+
+                $sKey = bx_get_reset_password_key($aAccountInfo['email']);
+                $sForgotPasswordUrl = bx_get_reset_password_link_by_key($sKey); 
+
+                $sSmsText = _t('_sys_txt_forgot_pasword_sms_text', $sForgotPasswordUrl);
+
+                $mixedOverrideResult = null;
+                bx_alert('account', 'before_forgot_password_send_sms', $aAccountInfo['id'], false, array('phone_number' => &$sPhone, 'sms_text' => &$sSmsText, 'override_result' => &$mixedOverrideResult));
+                if ($mixedOverrideResult === null) {
+                    $oTwilio = BxDolTwilio::getInstance();
+                    if($oTwilio->sendSms($sPhone,  $sSmsText))
+                        $sResultMsg = MsgBox(_t("_sys_txt_forgot_pasword_check_phone"));
+                    else
+                        $sResultMsg = MsgBox(_t("_sys_txt_forgot_pasword_error_occured"));
+                }
+
+                $sForm = '';
+            }
+            
+            if($bApi) {
+                return [
+                    bx_api_get_msg(strip_tags($sResultMsg)),
+                ];
+            }
+        } 
+        else {
+            $sResultMsg = _t($sCaptionKey);
+            $sForm = $oForm->getCode();
+            
+            if($bApi) {
+                return [
+                    bx_api_get_msg($sResultMsg, ['ext' => ['msg_type' => 'caption']]),
+                    bx_api_get_block('form', $oForm->getCodeAPI(), [
+                        'ext' => [
+                            'request' => ['url' => '/api.php?r=system/forgot_password/TemplServiceAccount', 'immutable' => true]
+                        ]
+                ])];
+            }
+        }
+
+        return '<div class="bx-def-padding-sec-bottom">' . $sResultMsg . '</div>' . $sForm . ($bShowErrorEmptyBoth ? '<div class="bx-form-warn">' . _t("_sys_form_forgot_password_phone_and_email_empty_error") . '</div>' : '');
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-account Account
+     * @subsubsection bx_system_general-switch_profile switch_profile
+     * 
+     * @code bx_srv('system', 'switch_profile', [123], 'TemplServiceAccount'); @endcode
+     * @code {{~system:switch_profile:TemplServiceAccount:[123]~}} @endcode
+     * 
+     * Switch current user profile. 
+     * User can switch between profiles if multiple profiles are created.
+     * Also it's possible to switch to Organization profile if user is admin of Organization, 
+     * or other module which can `actAsProfile`.
+     * Admin can switch to any prifile by default
+     *
+     * @param $iSwitchToProfileId profile ID to switch to
+     * @return true on success or error message otherwise, so make sue to make strict (===) comparision
+     * 
+     * @see BxBaseServiceAccount::serviceSwitchProfile
+     */
+    /** 
+     * @ref bx_system_general-switch_profile "switch_profile"
+     */
+    public function serviceSwitchProfile($iSwitchToProfileId)
+    {
+        $oProfile = BxDolProfile::getInstance($iSwitchToProfileId);
+        if (!$oProfile) {
+            return _t('_sys_txt_error_occured');
+        }
+
+        $iViewerAccountId = getLoggedId();
+        $iSwitchToAccountId = $oProfile->getAccountId();
+        
+        $iViewerProfileId = bx_get_logged_profile_id();
+        $aCheck = checkActionModule($iViewerProfileId, 'switch to any profile', 'system', false);
+        $bAllowSwitchToAnyProfile = $aCheck[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED;
+
+        $bCanSwitch = ($iSwitchToAccountId == $iViewerAccountId || $bAllowSwitchToAnyProfile);
+        bx_alert('account', 'check_switch_context', $iSwitchToAccountId, $iViewerProfileId, array('switch_to_profile' => $iSwitchToProfileId, 'viewer_account' => $iViewerAccountId, 'override_result' => &$bCanSwitch));
+
+        if (!$bCanSwitch) {
+            return isset($aCheck[CHECK_ACTION_MESSAGE]) ? $aCheck[CHECK_ACTION_MESSAGE] : _t('_sys_txt_error_occured'); 
+        } 
+
+        $oAccount = BxDolAccount::getInstance();
+        if (!$oAccount->updateProfileContext($iSwitchToProfileId)) {
+            return _t('_sys_txt_error_occured');
+        }
+
+        checkActionModule($iViewerProfileId, 'switch to any profile', 'system', true);
+        return true;
+    }
+
+    /**
+     * Reset password procedure
+     */
+    public function resetPassword()
+    {
+        $sKey = bx_process_input(bx_get('key'));
+
+        $oForm = BxDolForm::getObjectInstance('sys_forgot_password', 'sys_forgot_password_reset');
+        if(!empty($sKey) && isset($oForm->aInputs['key']))
+            $oForm->aInputs['key']['value'] = $sKey;
+
+        $oForm->initChecker();
+        if($oForm->isSubmittedAndValid()) {
+            $sErrorUrl = bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=forgot-password'));
+
+            // check if key exists
+            $oKey = BxDolKey::getInstance();
+            $sKey = $oForm->getCleanValue('key');
+            if(!$oKey || !$oKey->isKeyExists($sKey))
+                return MsgBox(_t('_sys_txt_reset_pasword_error_invalid_key', $sErrorUrl));
+
+            // check if key data exists
+            $aKeyData = $oKey->getKeyData($sKey);
+            if(empty($aKeyData['email']))
+                return MsgBox(_t('_sys_txt_reset_pasword_error_invalid_key', $sErrorUrl));
+
+            // check if account with such email exists
+            $iAccountId = $this->_oAccountQuery->getIdByEmail($aKeyData['email']);
+            if(empty($iAccountId))
+                return MsgBox(_t('_sys_txt_reset_pasword_error_not_found', $sErrorUrl));;
+
+            $sPassword = $oForm->getCleanValue('password');
+            $oAccount = BxDolAccount::getInstance($iAccountId);
+            if (!$oAccount || !$oAccount->updatePassword($sPassword))
+                return MsgBox(_t('_sys_txt_reset_pasword_error_occured', $sErrorUrl));
+
+            $this->_oAccountQuery->unlockAccount($iAccountId);
+            $oKey->removeKey($sKey);
+
+            return MsgBox(_t('_sys_txt_reset_pasword_success'), 3) . bx_srv('system', 'login_form_only', array('', bx_get_reset_password_redirect($iAccountId)), 'TemplServiceLogin');
+        }
+
+        $sTitleReset = _t('_sys_page_title_forgot_password_reset');
+        BxDolTemplate::getInstance()->setPageHeader($sTitleReset);
+        if(($oPage = BxDolPage::getObjectInstanceByURI()) !== false)
+            $oPage->setTitle($sTitleReset);
+
+        return $oForm->getCode();
+    }
+
+    /**
+     * Generate new password.
+     */
+    public function generateUserNewPwd($iAccountId)
+    {
+        $sPwd = genRndPwd(8, false);
+        $sSalt = genRndSalt();
+        $sPasswordHash = encryptUserPwd($sPwd, $sSalt);
+        
+        $oAccount = BxDolAccount::getInstance($iAccountId);
+        $iPasswordExpired = $oAccount->getPasswordExpiredDateByAccount($iAccountId);
+        
+        $this->_oAccountQuery->updatePassword($sPasswordHash, $sSalt, $iAccountId, $iPasswordExpired);
+
+        bx_alert('account', 'edited', $iAccountId, $iAccountId, array('action' => 'forgot_password'));
+
+        return $sPwd;
+    }
+
+    protected function _confirmEmail($sKey)
+    {
+        // check if key exists
+        $oKey = BxDolKey::getInstance();
+        if (!$oKey || !$oKey->isKeyExists($sKey))
+            return _t("_sys_txt_confirm_email_error_occured");
+
+        // check if key data exists
+        $aData = $oKey->getKeyData($sKey);
+        if (!isset($aData['account_id']))
+            return _t("_sys_txt_confirm_email_error_occured");
+
+        // check if account exists
+        $oAccount = BxDolAccount::getInstance($aData['account_id']);
+        if (!$oAccount)
+            return _t("_sys_txt_confirm_email_error_occured");
+
+        // remove key
+        $oKey->removeKey($sKey);
+
+        // confirm email
+        if (!$oAccount->updateEmailConfirmed(true))
+            return _t("_sys_txt_confirm_email_error_occured");
+
+        // login to user's account automatically
+        bx_login($aData['account_id']);
+
+        return (int)$aData['account_id'];
+    }
+}
+
+/** @} */

+ 466 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBaseServiceConnections.php

@@ -0,0 +1,466 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * System services for connections.
+ */
+class BxBaseServiceConnections extends BxDol
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-connections Connections
+     * @subsubsection bx_system_general-perform perform
+     * 
+     * @code bx_srv('system', 'perform', [[...]], 'TemplVoteServices'); @endcode
+     * @code {{~system:perform:TemplVoteServices[[...]]~}} @endcode
+     * 
+     * Performs Perform an action (add, remove, etc) with connection object.
+     * @param $aParams an array with necessary parameters 
+     * 
+     * @see BxBaseServiceConnections::servicePerform
+     */
+    /** 
+     * @ref bx_system_general-perform "perform"
+     * @api @ref bx_system_general-perform "perform"
+     */
+    public function servicePerform($aParams)
+    {
+        if(is_string($aParams))
+            $aParams = json_decode($aParams, true);
+
+        if(!$aParams['o'] || !$aParams['a'] || !$aParams['iid'] || !$aParams['cid'])
+            return ['code' => 1];
+
+        $oConnection = BxDolConnection::getObjectInstance($aParams['o']);
+        if(!$oConnection)
+            return ['code' => 2];
+
+        $sMethodHas = 'hasQuestionnaire';
+        $sMethodIsAnswered = 'isQuestionnaireAnswered';
+        if($aParams['a'] == 'add' && method_exists($oConnection, $sMethodHas) && $oConnection->$sMethodHas($aParams['cid']) && method_exists($oConnection, $sMethodIsAnswered) && !$oConnection->$sMethodIsAnswered($aParams['cid'], $aParams['iid'])) {
+            $sModule = $oConnection->getModule();
+
+            return [
+                'a' => 'questionnaire',
+                'data' => [
+                    bx_api_get_block('form', $oConnection->getQuestionnaireForm('add', $aParams['cid'])->getCodeAPI(), ['ext' => [
+                        'name' => $sModule, 
+                        'request' => ['url' => '/api.php?r=' . $sModule . '/get_questionnaire&params[]=main&params[]=' . $aParams['o'] . '&params[]=add&params[]=' . $aParams['cid'], 'immutable' => true]
+                    ]])
+                ]
+            ];
+        }
+
+        $sMethod = 'action' . bx_gen_method_name($aParams['a']);
+        if(!method_exists($oConnection, $sMethod))
+            return ['code' => 2];
+
+        $aResult = $oConnection->$sMethod($aParams['cid'], $aParams['iid']);
+        if($aResult['err'] !== false)
+            return ['code' => 3, 'message' => $aResult['msg']];
+
+        $aFlip = ['add' => 'remove', 'remove' => 'add'];
+
+        return [
+            'a' => $aFlip[$aParams['a']],
+            'title' => $this->serviceGetActionTitle($aParams['o'], $aParams['a'], $aParams['iid'], $aParams['cid'], true),
+        ];
+    }
+
+    public function serviceGetActionTitle($sObject, $sAction, $iInitiatorId, $iContentId, $bFlip = false)
+    {
+        if(empty($sAction))
+            return '';
+
+        $oConnection = BxDolConnection::getObjectInstance($sObject);
+        if(!$oConnection || !$oConnection->isProfileContent())
+            return '';
+
+        $sMethod = 'get_menu_item_title_by_connection';
+        $aContentProfileInfo = BxDolProfileQuery::getInstance()->getInfoById($iContentId);
+        if(empty($aContentProfileInfo) || !is_array($aContentProfileInfo) || !BxDolRequest::serviceExists($aContentProfileInfo['type'], $sMethod))
+            return '';
+
+        $aTitles = bx_srv($aContentProfileInfo['type'], $sMethod, [$sObject, '', $iContentId, $iInitiatorId]);
+        if(empty($aTitles) || !is_array($aTitles))
+            return '';
+
+        $aFlip = ['add' => 'remove', 'remove' => 'add'];
+        if($bFlip)
+            $sAction = $aFlip[$sAction];
+
+        return !empty($aTitles[$sAction]) ? _t($aTitles[$sAction]) : '';
+    }
+
+    /**
+     * get grid with friends connections
+     */
+    public function serviceConnectionsTable ($iProfileId = 0)
+    {
+        if(!$iProfileId && bx_get('profile_id') !== false)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if(!$iProfileId)
+            return false;
+
+        if(bx_is_api())
+            return bx_srv('system', 'browse_friends', [
+                'profile_id' => $iProfileId
+            ], 'TemplServiceProfiles');
+
+        $oGrid = BxDolGrid::getObjectInstance('sys_grid_connections');
+        if(!$oGrid)
+            return false;
+
+        $oGrid->setProfile($iProfileId);
+        return $oGrid->getCode();
+    }
+
+	/**
+     * get grid with friends connection requests
+     */
+    public function serviceConnectionsRequestTable ()
+    {
+        $oGrid = BxDolGrid::getObjectInstance('sys_grid_connections_requests');
+        if (!$oGrid)
+            return false;
+
+        return $oGrid->getCode();
+    }
+
+    /**
+     * get number of received unconfirmed connections (friend requests)
+     * @param $sConnectionsObject connections object to get unconfirmed connections from
+     * @param $mixedId id to get connections for, if omitted then logged-in profile id is used
+     * @return number
+     */
+    public function serviceGetUnconfirmedConnectionsNum ($sConnectionsObject, $mixedId = 0)
+    {
+        $oConnection = BxDolConnection::getObjectInstance($sConnectionsObject);
+        if (!$oConnection)
+            return 0;
+
+        if (!$mixedId)
+            $mixedId = bx_get_logged_profile_id();
+
+        $i = 0;
+        $a = $oConnection->getConnectedInitiators($mixedId, 0); // get received friend requests
+        foreach ($a as $iId)
+            if (BxDolProfile::getInstance($iId))
+                ++$i;
+
+        return $i;
+    }
+
+    public function serviceGetLiveUpdatesUnconfirmedConnections($sModule, $sConnectionsObject, $aMenuItemParent, $aMenuItemChild, $iCount = 0)
+    {
+        $iProfile = bx_get_logged_profile_id();
+        $oProfile = BxDolProfile::getInstance($iProfile);
+        if(!$oProfile || $oProfile->getModule() != $sModule)
+            return false;
+
+        $iCountNew = (int)$this->serviceGetUnconfirmedConnectionsNum($sConnectionsObject, $iProfile);
+        if($iCountNew == (int)$iCount)
+			return false;
+
+        return array(
+    		'count' => $iCountNew, // required
+    		'method' => 'bx_menu_show_live_update(oData)', // required
+    		'data' => array(
+    			'code' => BxDolTemplate::getInstance()->parseHtmlByTemplateName('menu_item_addon', array(
+    				'content' => '{count}'
+                )),
+                'mi_parent' => $aMenuItemParent,
+                'mi_child' => $aMenuItemChild
+    		),  // optional, may have some additional data to be passed in JS method provided using 'method' param above.
+    	);
+    }
+
+    /**
+     * get number of initiated connections (like "my subscriptions")
+     * @param $sConnectionsObject connections object to get connections from
+     * @param $mixedId id to get connections for, if omitted then logged-in profile id is used
+     * @return number
+     */
+    public function serviceGetConnectedContentNum ($sConnectionsObject, $mixedId = 0, $isMutual = false)
+    {
+        $oConnection = BxDolConnection::getObjectInstance($sConnectionsObject);
+        if (!$oConnection)
+            return 0;
+
+        if (!$mixedId)
+            $mixedId = bx_get_logged_profile_id();
+
+        $i = 0;
+        $a = $oConnection->getConnectedContent($mixedId, $isMutual, 0, BX_CONNECTIONS_LIST_NO_LIMIT); // get received friend requests
+        foreach ($a as $iId)
+            if (BxDolProfile::getInstance($iId))
+                ++$i;
+
+        return $i;
+    }
+
+    /**
+     * get number of received connections (like "subscribed me")
+     * @param $sConnectionsObject connections object to get connections from
+     * @param $mixedId id to get connections for, if omitted then logged-in profile id is used
+     * @return number
+     */
+    public function serviceGetConnectedInitiatorsNum ($sConnectionsObject, $mixedId = 0, $isMutual = false)
+    {
+        $oConnection = BxDolConnection::getObjectInstance($sConnectionsObject);
+        if (!$oConnection)
+            return 0;
+
+        if (!$mixedId)
+            $mixedId = bx_get_logged_profile_id();
+
+        $i = 0;
+        $a = $oConnection->getConnectedInitiators($mixedId, $isMutual, 0, BX_CONNECTIONS_LIST_NO_LIMIT); // get received friend requests
+        foreach ($a as $iId)
+            if (BxDolProfile::getInstance($iId))
+                ++$i;
+
+        return $i;
+    }
+
+    /**
+     * get grid with subscriptions connections
+     */
+    public function serviceSubscriptionsTable ($iProfileId = 0)
+    {
+        if(!$iProfileId && bx_get('profile_id') !== false)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+
+        $oProfile = false;
+        if(!$iProfileId || !($oProfile = BxDolProfile::getInstance($iProfileId)))
+            return false;
+
+        $aProfile = $oProfile->getInfo();
+        if(empty($aProfile) || !is_array($aProfile))
+            return false;
+
+        if(bx_is_api())
+            return bx_srv('system', 'browse_subscriptions', [
+                'profile_id' => $iProfileId,
+            ], 'TemplServiceProfiles');
+
+        $CNF = &BxDolModule::getInstance($aProfile['type'])->_oConfig->CNF;
+        if(getParam($CNF['PARAM_PUBLIC_SBSN']) != 'on' && $aProfile['id'] != bx_get_logged_profile_id())
+            return false;
+
+        $oGrid = BxDolGrid::getObjectInstance('sys_grid_subscriptions');
+        if (!$oGrid)
+            return false;
+
+        $oGrid->setProfileId($iProfileId);
+        $sContent = $oGrid->getCode();
+        if(empty($sContent))
+            return false;
+
+        return BxDolTemplate::getInstance()->parseHtmlByName('connections_list.html', array(
+            'name' => 'subscriptions',
+            'content' => $sContent
+        ));
+    }
+
+    /**
+     * get grid with subscribed me connections
+     */
+    public function serviceSubscribedMeTable ($iProfileId = 0)
+    {
+        if(!$iProfileId && bx_get('profile_id') !== false)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+
+        $oProfile = false;
+        if(!$iProfileId || !($oProfile = BxDolProfile::getInstance($iProfileId)))
+            return false;
+
+        $aProfile = $oProfile->getInfo();
+        if(empty($aProfile) || !is_array($aProfile))
+            return false;
+
+        if(bx_is_api())
+            return bx_srv('system', 'browse_subscribed_me', [
+                'profile_id' => $iProfileId,
+            ], 'TemplServiceProfiles');
+
+        $CNF = &BxDolModule::getInstance($aProfile['type'])->_oConfig->CNF;
+        if(getParam($CNF['PARAM_PUBLIC_SBSD']) != 'on' && $aProfile['id'] != bx_get_logged_profile_id())
+            return false;
+
+        $oGrid = BxDolGrid::getObjectInstance('sys_grid_subscribed_me');
+        if(!$oGrid)
+            return false;
+
+        $oGrid->setProfileId($iProfileId);
+        $sContent = $oGrid->getCode();
+        if(empty($sContent))
+            return false;
+
+        return BxDolTemplate::getInstance()->parseHtmlByName('connections_list.html', array(
+            'name' => 'subscribers',
+            'content' => $sContent
+        ));
+    }
+
+    /**
+     * get grid with 'relations' connections
+     */
+    public function serviceRelationsTable ($iProfileId = 0)
+    {
+        if(!BxDolRelation::isEnabled())
+            return false;
+
+        if(!$iProfileId && bx_get('profile_id') !== false)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+
+        $aProfile = BxDolProfile::getInstance($iProfileId)->getInfo();
+        if(empty($aProfile) || !is_array($aProfile))
+            return false;
+
+        if(!BxDolConnection::getObjectInstance('sys_profiles_relations')->isRelationAvailableFromProfile($aProfile['type']))
+            return false;
+
+        $oGrid = BxDolGrid::getObjectInstance('sys_grid_relations');
+        if (!$oGrid)
+            return false;
+
+        $oGrid->setProfileId($iProfileId);
+        $sContent = $oGrid->getCode();
+        if(empty($sContent))
+            return false;
+
+        return BxDolTemplate::getInstance()->parseHtmlByName('connections_list.html', array(
+            'name' => 'relations',
+            'content' => $sContent
+        ));
+    }
+
+    /**
+     * get grid with 'related me' connections
+     */
+    public function serviceRelatedMeTable ($iProfileId = 0)
+    {
+        if(!BxDolRelation::isEnabled())
+            return false;
+
+        if(!$iProfileId && bx_get('profile_id') !== false)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+
+        $aProfile = BxDolProfile::getInstance($iProfileId)->getInfo();
+        if(empty($aProfile) || !is_array($aProfile))
+            return false;
+
+        if(!BxDolConnection::getObjectInstance('sys_profiles_relations')->isRelationAvailableWithProfile($aProfile['type']))
+            return false;
+
+        $oGrid = BxDolGrid::getObjectInstance('sys_grid_related_me');
+        if(!$oGrid)
+            return false;
+
+        $oGrid->setProfileId($iProfileId);
+        $sContent = $oGrid->getCode();
+        if(empty($sContent))
+            return false;
+
+        return BxDolTemplate::getInstance()->parseHtmlByName('connections_list.html', array(
+            'name' => 'related-me',
+            'content' => $sContent
+        ));
+    }
+
+    /*
+     * Get notification data for Notifications module - action Subscribe. 
+     */
+    public function serviceGetNotificationsPost($aEvent)
+    {
+        $iOwner = (int)$aEvent['owner_id'];
+        $oOwner = BxDolProfile::getInstance($iOwner);
+        if(!$oOwner)
+            return array();
+
+        $iProfile = (int)$aEvent['object_id'];
+        $oProfile = BxDolProfile::getInstance($iProfile);
+        if(!$oProfile)
+            return array();
+
+        return array(
+            'entry_sample' => '_sys_profile_sample_single',
+            'entry_url' => str_replace(BX_DOL_URL_ROOT, '{bx_url_root}', $oOwner->getUrl()),
+            'entry_caption' => $oProfile->getDisplayName(),
+            'entry_author' => $oProfile->id(),
+            'lang_key' => '_sys_profile_subscription_added',
+        );
+    }
+
+    /*
+     * Get notification data for Notifications module - action Friend. 
+     */
+    public function serviceGetNotificationsPostFriendship($aEvent)
+    {
+        $iOwner = (int)$aEvent['owner_id'];
+        $oOwner = BxDolProfile::getInstance($iOwner);
+        if(!$oOwner)
+            return array();
+
+        $iProfile = (int)$aEvent['object_id'];
+        $oProfile = BxDolProfile::getInstance($iProfile);
+        if(!$oProfile)
+            return array();
+
+        $sLangKey = '_sys_profile_friendship_added';
+        if(isset($aEvent['content']['request']) && (int)$aEvent['content']['request'] == 1)
+            $sLangKey = '_sys_profile_friend_request_added';
+
+        return array(
+            'entry_sample' => '_sys_profile_sample_single',
+            'entry_url' => str_replace(BX_DOL_URL_ROOT, '{bx_url_root}', $oOwner->getUrl()),
+            'entry_caption' => $oProfile->getDisplayName(),
+            'entry_author' => $oProfile->id(),
+            'lang_key' => $sLangKey,
+        );
+    }
+
+    public function serviceAlertResponseConnections($oAlert)
+    {
+        $sMethod = 'process' . bx_gen_method_name($oAlert->sUnit . '_' . $oAlert->sAction);
+        if(!method_exists($this, $sMethod))
+            return;
+            
+        $this->$sMethod($oAlert);
+    }
+
+    protected function processSysProfilesFriendsConnectionAdded(&$oAlert)
+    {
+        if((int)$oAlert->aExtras['mutual'] != 1)
+            return;
+
+        $oConnection = BxDolConnection::getObjectInstance('sys_profiles_subscriptions');
+        $oConnection->addConnection((int)$oAlert->aExtras['initiator'], (int)$oAlert->aExtras['content'], array('alert_extras' => array('silent_mode' => 1)));
+        $oConnection->addConnection((int)$oAlert->aExtras['content'], (int)$oAlert->aExtras['initiator'], array('alert_extras' => array('silent_mode' => 1)));
+    }
+
+    protected function processSysProfilesFriendsConnectionRemoved(&$oAlert)
+    {
+        if((int)$oAlert->aExtras['mutual'] != 1)
+            return;
+
+        $oConnection = BxDolConnection::getObjectInstance('sys_profiles_subscriptions');
+        $oConnection->removeConnection((int)$oAlert->aExtras['initiator'], (int)$oAlert->aExtras['content']);
+        $oConnection->removeConnection((int)$oAlert->aExtras['content'], (int)$oAlert->aExtras['initiator']);
+    }
+}
+
+/** @} */

+ 1185 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBaseServiceProfiles.php

@@ -0,0 +1,1185 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * System service for profiles handling functionality.
+ */
+class BxBaseServiceProfiles extends BxDol
+{
+    protected $_bIsApi;
+    public function __construct()
+    {
+        parent::__construct();
+
+        $this->_bIsApi = bx_is_api();
+    }
+
+    public function serviceGetProfileTypes()
+    {
+        $aTypes = array(
+			'' => _t('_Select_one')
+        );
+
+        $aModules = $this->_getProfilesModules();
+        foreach($aModules as $aModule) {
+			if(empty($aModule) || !is_array($aModule))
+				continue;
+
+            $sTitleKey = '_' . $aModule['name'];
+            $sTitle = _t($sTitleKey);
+
+            $aTypes[$aModule['name']] = !empty($sTitle) && $sTitle != $sTitleKey ? $sTitle : $aModule['title'];
+		}
+
+        return $aTypes;
+    }
+
+    public function serviceProfileAvatar ($iProfileId = 0)
+    {
+        if (!$iProfileId && !($iProfileId = bx_get_logged_profile_id()))
+            return '';
+
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if(!$oProfile)
+            return '';
+
+        if(bx_is_api())
+            return [
+                bx_api_get_block ('profile_avatar', $oProfile->getData())
+            ];
+
+        $oTemplate = BxDolTemplate::getInstance();
+
+        $sSwitcher = '';
+        $aSwitcher = bx_srv('system', 'account_profile_switcher', array(false, null, '', true), 'TemplServiceProfiles');
+        if($aSwitcher !== false)
+            $sSwitcher = BxTemplFunctions::getInstance()->popupBox('bx-profile-switcher', _t('_sys_txt_switch_profiles'), $oTemplate->parseHtmlByName('profile_avatar_switcher.html', array(
+                'profile_switcher' => $aSwitcher['content'],
+                'bx_if:multiple_profiles_mode' => array(
+                    'condition' => empty($aSwitcher['content']) || BxDolAccount::isAllowedCreateMultiple($oProfile->id()),
+                    'content' => array(
+                        'url_switch_profile' => BxDolPermalinks::getInstance()->permalink('page.php?i=account-profile-switcher')
+                    )
+                )
+            )), true);
+        $bSwitcher = !empty($sSwitcher);        
+        $sSwitcherUrl = $bSwitcher ? 'javascript:void(0)' : $sUrl;
+        $sSwitcherOnclick = $bSwitcher ? "javascript:$('#bx-profile-switcher').dolPopup({});" : "";
+
+        $sDisplayName = $oProfile->getDisplayName();
+        $sUrl = $oProfile->getUrl();
+
+        $oAcl = BxDolAcl::getInstance();
+        $aAcl = $oAcl->getMemberMembershipInfo($iProfileId);
+        $aAclInfo = $oAcl->getMembershipInfo($aAcl['id']);
+        list($sIcon, $sIconUrl, $sIconA, $sIconHtml) = $this->_getIcon($aAclInfo['icon']);
+
+        $aVars = array(
+            'profile_id' => $oProfile->id(),
+            'profile_url' => $sUrl,
+            'profile_edit_url' => $oProfile->getEditUrl(),
+            'profile_title' => $sDisplayName,
+            'profile_title_attr' => bx_html_attribute($sDisplayName),
+            'profile_ava_url' => $oProfile->getAvatar(),
+            'profile_unit' => $oProfile->getUnit(0, array('template' => array(
+                'name' => 'unit_wo_info_links',
+                'size' => 'thumb'
+            ))),
+            'profile_acl_title' => _t($aAclInfo['name']),
+            'bx_if:image' => array (
+                'condition' => (bool)$sIconUrl,
+                'content' => array('icon_url' => $sIconUrl),
+            ),
+            'bx_if:image_inline' => array (
+                'condition' => false,
+                'content' => array('image' => ''),
+            ),
+            'bx_if:icon' => array (
+                'condition' => (bool)$sIcon,
+                'content' => array('icon' => $sIcon),
+            ),
+            'bx_if:icon-a' => array (
+                'condition' => (bool)$sIconA,
+                'content' => array('icon-a' => $sIconA),
+            ),
+            'bx_if:icon-html' => array (
+                'condition' => (bool)$sIconHtml,
+                'content' => array('icon' => $sIconHtml),
+            ),
+            'switcher_url' => $sSwitcherUrl,
+            'switcher_onclick' => $sSwitcherOnclick,
+            'bx_if:show_switcher_icon' => array(
+                'condition' => $bSwitcher,
+                'content' => array(
+                    'switcher_url' => $sSwitcherUrl,
+                    'switcher_onclick' => $bSwitcher ? "javascript:$('#bx-profile-switcher').dolPopup({});" : "",
+                )
+            ),
+            'switcher' => $sSwitcher
+        );
+
+        return $oTemplate->parseHtmlByName('profile_avatar.html', $aVars);
+    }
+
+    public function serviceProfileMenu ($iProfileId = 0)
+    {
+        if(($oMenu = BxDolMenu::getObjectInstance('sys_profile_stats')) !== false) {
+            if(bx_is_api())
+                return [
+                    bx_api_get_block ('profile_menu', $oMenu->getCodeAPI())
+                ];
+
+            return $oMenu->getCode();
+        }
+        else
+            return '';
+    }
+
+    public function serviceProfileFollowings ($iProfileId = 0)
+    {
+        if(($oMenu = BxDolMenu::getObjectInstance('sys_profile_followings')) !== false)
+            return $oMenu->getCode();
+        else
+            return '';
+    }
+    
+    public function serviceBefriend ($sParams)
+    {
+        $aParams = json_decode($sParams, true);
+        $iProfileId = $aParams['profile_id'];
+        $oConnection = BxDolConnection::getObjectInstance('sys_profiles_friends'); 
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if ($aParams['action'] == 'info'){
+            return ['profile' => BxDolProfile::getData($iProfileId), 'result' => $oConnection->isConnected((int)bx_get_logged_profile_id(), $iProfileId, true)];
+        }
+       
+        if ($oProfile && $oConnection){
+            if ($oConnection->isConnected((int)bx_get_logged_profile_id(), $iProfileId, true)){
+                 return ['profile' => BxDolProfile::getData($iProfileId), 'result' => false];
+            }
+            else{
+                $oConnection->addConnection((int)bx_get_logged_profile_id(), $iProfileId);
+                $oConnection->addConnection($iProfileId, (int)bx_get_logged_profile_id());
+                $oProfile = BxDolProfile::getInstance($iProfileId);
+                return ['profile' => BxDolProfile::getData($iProfileId), 'result' => true];
+            }
+        }
+    }
+
+    public function serviceProfileStats ($iProfileId = 0)
+    {
+        if (!$iProfileId && !($iProfileId = bx_get_logged_profile_id()))
+            return '';
+
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if(!$oProfile)
+            return '';
+
+        $oMenu = BxDolMenu::getObjectInstance('sys_profile_stats');
+        if(!$oMenu)
+            return '';
+
+        if(bx_is_api())
+            return [
+                bx_api_get_block ('profile_avatar', $oProfile->getData(), ['id' => 1]),
+                bx_api_get_block ('profile_menu', $oMenu->getCodeAPI(), ['id' => 2])
+            ];
+
+        $oAcl = BxDolAcl::getInstance();
+        $aAcl = $oAcl->getMemberMembershipInfo($iProfileId);
+        $aAclInfo = $oAcl->getMembershipInfo($aAcl['id']);
+
+        list ($sIcon, $sIconUrl, $sIconA, $sIconHtml) = $this->_getIcon($aAclInfo['icon']);
+
+        $aVars = array(
+            'profile_id' => $oProfile->id(),
+            'profile_url' => $oProfile->getUrl(),
+            'profile_edit_url' => $oProfile->getEditUrl(),
+            'profile_title' => $oProfile->getDisplayName(),
+            'profile_title_attr' => bx_html_attribute($oProfile->getDisplayName()),
+            'profile_ava_url' => $oProfile->getAvatar(),
+            'profile_unit' => $oProfile->getUnit(0, array('template' => array(
+                'name' => 'unit_wo_info',
+                'size' => 'ava'
+            ))),
+            'profile_acl_title' => _t($aAclInfo['name']),
+            'menu' => $oMenu->getCode(),
+        );
+		
+        $aVars['bx_if:image'] = array (
+            'condition' => (bool)$sIconUrl,
+            'content' => array('icon_url' => $sIconUrl),
+        );
+        $aVars['bx_if:image_inline'] = array (
+            'condition' => false,
+            'content' => array('image' => ''),
+        );
+        $aVars['bx_if:icon'] = array (
+            'condition' => (bool)$sIcon,
+            'content' => array('icon' => $sIcon),
+        );
+        $aVars['bx_if:icon-html'] = array (
+            'condition' => (bool)$sIconHtml,
+            'content' => array('icon' => $sIconHtml),
+        );
+        $aVars['bx_if:icon-a'] = array (
+            'condition' => (bool)$sIconA,
+            'content' => array('icon-a' => $sIconA),
+        );
+
+        return BxDolTemplate::getInstance()->parseHtmlByName('profile_stats.html', $aVars);
+    }
+
+    public function serviceGetMenuAddonProfileEdit($iProfileId = 0, $sCaption = '')
+    {
+        /**
+         * Disabled.
+         */
+        return '';
+
+    	if(empty($sCaption))
+            $sCaption = _t('_Edit');
+
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if(!$oProfile)
+            return '';
+
+        $sModule = $oProfile->getModule();
+        $sMethod = 'profile_edit_url';
+        if(!BxDolRequest::serviceExists($sModule, $sMethod))
+            return '';
+
+        return BxDolTemplate::getInstance()->parseLink(BxDolService::call($sModule, $sMethod, array($oProfile->getContentId())), $sCaption);
+    }
+    
+    public function serviceGetMenuAddonProfileConnections($iProfileId = 0)
+    {
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if(!$oProfile)
+            return '';
+
+        $iCount = 0;
+
+        //--- Add friend requests.
+        $iCount += (int)bx_srv('system', 'get_unconfirmed_connections_num', ['sys_profiles_friends'], 'TemplServiceConnections');
+
+        //--- Add subscriptions.
+        $iCount += (int)bx_srv('system', 'get_connected_content_num', ['sys_profiles_subscriptions'], 'TemplServiceConnections');
+
+        //--- Add subscribed me.
+        $iCount += (int)bx_srv('system', 'get_connected_initiators_num', ['sys_profiles_subscriptions'], 'TemplServiceConnections');
+
+        return $iCount > 0 ? $iCount : '';
+    }   
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-profiles Profiles
+     * @subsubsection bx_system_general-profile_membership profile_membership
+     * 
+     * @code bx_srv('system', 'profile_membership', [2], 'TemplServiceProfiles'); @endcode
+     * @code {{~system:profile_membership:TemplServiceProfiles[2]~}} @endcode
+     * 
+     * Get membership level for specified profile.
+     * @param $iProfileId profile ID
+     * 
+     * @see BxBaseServiceProfiles::serviceProfileMembership
+     */
+    /** 
+     * @ref bx_system_general-profile_membership "profile_membership"
+     */
+    public function serviceProfileMembership ($iProfileId = 0)
+    {
+        if (!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+
+		return BxDolAcl::getInstance()->getProfileMembership($iProfileId);
+    }
+
+    public function serviceProfileMembershipStats ($iProfileId = 0)
+    {
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+
+        $sTxtUnlimited = _t('_unlimited');
+        $sTxtFiles = _t('_sys_storage_files');
+        $aQuota = BxDolProfileQuery::getInstance()->getProfileQuota($iProfileId);
+
+        $aTmplVarsStats = array();
+        if(!empty($aQuota['quota_size'])) {
+            $iWidth = (int)round(100 * $aQuota['current_size']/$aQuota['quota_size']);
+            $sPercent = $iWidth . '%';
+        }
+        else {
+            $iWidth = 0;
+            $sPercent = $sTxtUnlimited;
+        }
+        $aTmplVarsStats[] = array('title' => _t('_sys_profile_storage_quota_size'), 'width' => $iWidth, 'value' => _t_format_size($aQuota['current_size']), 'percent' => $sPercent);
+
+        if(!empty($aQuota['quota_number'])) {
+            $iWidth = (int)round(100 * $aQuota['current_number']/$aQuota['quota_number']);
+            $sPercent = $iWidth . '%';
+        }
+        else {
+            $iWidth = 0;
+            $sPercent = $sTxtUnlimited;
+        } 
+        $aTmplVarsStats[] = array('title' => _t('_sys_profile_storage_quota_number'), 'width' => $iWidth, 'value' => $aQuota['current_number'] . ' ' . $sTxtFiles, 'percent' => $sPercent);
+
+        return BxDolTemplate::getInstance()->parseHtmlByName('profile_membership_stats.html', array(
+            'membership' => BxDolAcl::getInstance()->getProfileMembership($iProfileId),
+            'bx_repeat:stat_items' => $aTmplVarsStats
+        ));
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-profiles Profiles
+     * @subsubsection bx_system_general-profile_notifications profile_notifications
+     * 
+     * @code bx_srv('system', 'profile_notifications', [], 'TemplServiceProfiles'); @endcode
+     * @code {{~system:profile_notifications:TemplServiceProfiles~}} @endcode
+     * 
+     * Get number of unread notifications for logged in profile
+     * 
+     * @see BxBaseServiceProfiles::serviceProfileNotifications
+     */
+    /** 
+     * @ref bx_system_general-profile_notifications "profile_notifications"
+     */
+    public function serviceProfileNotifications ($iProfileId = 0)
+    {
+        if (!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+
+        $oMenu = BxDolMenu::getObjectInstance('sys_account_notifications');
+        if(!$oMenu)
+            return 0;
+
+        $iNum = 0;
+        $aMenuItems = $oMenu->getMenuItems ();
+        foreach ($aMenuItems as $r) {
+            if (isset($r['bx_if:addon']) && $r['bx_if:addon']['condition'])
+                $iNum += $r['bx_if:addon']['content']['addon'];
+        }
+
+        return $iNum;
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-profiles Profiles
+     * @subsubsection bx_system_general-get_count_online_profiles get_count_online_profiles
+     * 
+     * @code bx_srv('system', 'get_count_online_profiles', [], 'TemplServiceProfiles'); @endcode
+     * @code {{~system:get_count_online_profiles:TemplServiceProfiles~}} @endcode
+     * 
+     * Get number of online profiles
+     * 
+     * @see BxBaseServiceProfiles::serviceGetCountOnlineProfiles
+     */
+    /** 
+     * @ref bx_system_general-get_count_online_profiles "get_count_online_profiles"
+     */
+    public function serviceGetCountOnlineProfiles ()
+    {
+        $oProfilesQuery = BxDolProfileQuery::getInstance();
+        return $oProfilesQuery->getOnlineCount();
+    }
+    
+    public function serviceGetProfilesModules ($bForceActAsProfile = true)
+    {
+        if (getParam('sys_db_cache_enable')) { // get list of profiles  modules from db cache, cache is invalidated when new module is installed
+
+            $oDb = BxDolDb::getInstance();
+
+            $oCache = $oDb->getDbCacheObject ();
+
+            $sKey = $oDb->genDbCacheKey('profiles_' . (!$bForceActAsProfile ? 'all_' : '') . 'modules_array');
+            $sKeyTs = $oDb->genDbCacheKey('profiles_modules_ts');
+
+            $mixedRetTs = $oCache->getData($sKeyTs);
+            $mixedRet = $oCache->getData($sKey);
+
+            $iNewestModuleTs = $this->_getLatestModuleTimestamp ();
+
+            if ($mixedRetTs != null && $mixedRet !== null && $mixedRetTs == $iNewestModuleTs) {
+
+                $aModulesArray = $mixedRet;
+
+            } else {
+
+                $aModulesArray = $this->_getProfilesModules ($bForceActAsProfile);
+
+                $oCache->setData($sKey, $aModulesArray);
+                $oCache->setData($sKeyTs, $iNewestModuleTs);
+            }
+
+        } else {
+
+            $aModulesArray = $this->_getProfilesModules ($bForceActAsProfile);
+
+        }
+
+        return $aModulesArray;
+    }
+
+    public function serviceProfilesFriends ($iLimit = 20)
+    {
+        if (!($iProfileId = bx_get_logged_profile_id()))
+            return array();
+        
+        $oConnection = BxDolConnection::getObjectInstance('sys_profiles_friends');
+        if (!$oConnection)
+            return array();
+
+        if (!($a = $oConnection->getConnectedContent ($iProfileId, true, 0, $iLimit)))
+            return array();
+
+        $aRet = array();
+        foreach ($a as $iId) {
+            $oProfile = BxDolProfile::getInstance($iId);
+
+            $aRet[] = array (
+            	'label' => $oProfile->getDisplayName(), 
+                'value' => $iId, 
+                'url' => $oProfile->getUrl(),
+            	'thumb' => $oProfile->getThumb(),
+                'unit' => $oProfile->getUnit(0, array('template' => 'unit_wo_info'))
+            );
+        }
+        return $aRet;
+    }
+
+    public function serviceProfilesSearch ($sTerm, $mixedParems = [])
+    {
+        $iLimit = (int)getParam('sys_profiles_search_limit');
+        if(is_int($mixedParems)) 
+            $iLimit = (int)$mixedParems;
+        else if(is_array($mixedParems) && isset($mixedParems['limit']))
+            $iLimit = (int)$mixedParems['limit'];
+
+        // display friends by default
+        if (!$sTerm)
+            return $this->serviceProfilesFriends($iLimit);
+
+        // get list of "profiles" modules
+        $aModules = $this->serviceGetProfilesModules();
+
+        // search in each module
+        $a = array();
+        foreach ($aModules as $aModule) {
+            if (!BxDolService::call($aModule['name'], 'act_as_profile'))
+                continue;
+            $a = array_merge($a, BxDolService::call($aModule['name'], 'profiles_search', array($sTerm, getParam('sys_per_page_search_keyword_single'))));
+        }
+
+        // sort result
+        usort($a, function($r1, $r2) {
+            return strcmp($r1['label'], $r2['label']);
+        });
+
+        bx_alert('system', 'profiles_search', 0, 0, array(
+            'module' => is_array($mixedParems) && isset($mixedParems['module']) ? $mixedParems['module'] : '',
+            'term' => $sTerm,
+            'result' => &$a
+        ));
+
+        // return as array
+        return array_slice($a, 0, $iLimit);
+    }
+
+    public function serviceProfilesSearchByLocation ($aLocation, $iRadius, $mixedParems = [])
+    {
+        $sModule = !empty($mixedParems['module']) ? $mixedParems['module'] : '';
+        $aModules = !empty($sModule) ? [BxDolModuleDb::getInstance()->getModuleByName($sModule)] : $this->serviceGetProfilesModules();
+
+        $iStart = 0;
+        $iLimit = 20;
+        if(is_int($mixedParems)) 
+            $iLimit = (int)$mixedParems;
+        else if(is_array($mixedParems) && isset($mixedParems['limit']))
+            $iLimit = (int)$mixedParems['limit'];
+
+        $aLocation[] = $iRadius;
+        $oProfileQuery = BxDolProfileQuery::getInstance();
+
+        $aResult = [];
+        foreach($aModules as $aModule) {
+            if(!bx_srv($aModule['name'], 'act_as_profile'))
+                continue;
+
+            $aContentIds = bx_srv($aModule['name'], 'get_search_result_extended', [[
+                'location' => ['type' => 'location_radius', 'operator' => 'locate', 'value' => [
+                    'array' => $aLocation
+                ]]
+            ], $iStart, $iLimit]);
+
+            $aResult = array_merge($aResult, $oProfileQuery->getProfiles(['type' => 'id_by_module_content_id', 'module' => $aModule['name'], 'content_id' => $aContentIds]));
+        }
+
+        bx_alert('system', 'profiles_search_by_location', 0, 0, [
+            'module' => $sModule,
+            'location' => $aLocation,
+            'radius' => $iRadius,
+            'result' => &$aResult
+        ]);
+
+        return array_slice($aResult, 0, $iLimit);
+    }
+
+    public function serviceProfilesList ($iAccountId = 0)
+    {
+        $oProfilesQuery = BxDolProfileQuery::getInstance();
+
+        $aProfiles = $oProfilesQuery->getProfilesByAccount($iAccountId ? $iAccountId : getLoggedId());
+        if (!$aProfiles)
+            return false;
+
+        $s = '';
+        foreach ($aProfiles as $aProfile)
+            if ($aProfile['type'] != 'system')
+                $s .= BxDolService::call($aProfile['type'], 'profile_unit', array($aProfile['content_id']));
+
+        if (!$s)
+            $s = MsgBox(_t('_sys_txt_empty'));
+
+        return $s . '<div class="bx-clear"></div>';
+    }
+
+    public function serviceBrowseFriends ($iProfileId = 0, $aParams = [])
+    {
+        if($this->_bIsApi)
+            $aParams = bx_api_get_browse_params($aParams);
+
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return '';
+
+        $aParams = array_merge([
+            'params' => false,
+            'design_box' => BX_DB_PADDING_DEF,
+            'empty_message' => false,
+            'ajax_paginate' => true,
+        ], $aParams);
+
+        $aParamsBrowse = ['profile' => $iProfileId];
+        if(!empty($aParams['params']) && is_array($aParams['params']))
+            $aParamsBrowse = array_merge($aParamsBrowse, $aParams['params']);
+
+        return $this->_serviceBrowseConnections('friends', $aParamsBrowse, $aParams['design_box'], $aParams['empty_message'], $aParams['ajax_paginate']);
+    }
+    
+    public function serviceProfileFriendsRequestsCount ($iProfileId = 0)
+    {
+        
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return '';
+        
+        $oConnection = BxDolConnection::getObjectInstance('sys_profiles_friends');
+        if(!$oConnection)
+            return false;
+
+        return $oConnection->getConnectedInitiatorsCount($iProfileId, false) - $oConnection->getConnectedInitiatorsCount($iProfileId, true);
+    }
+    
+    public function serviceProfileSentFriendsRequestsCount ($iProfileId = 0)
+    {
+        
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return '';
+        
+        $oConnection = BxDolConnection::getObjectInstance('sys_profiles_friends');
+        if(!$oConnection)
+            return false;
+
+        return $oConnection->getConnectedContentCount($iProfileId, false) - $oConnection->getConnectedContentCount($iProfileId, true);
+    }
+    
+    
+    public function serviceProfileFriendsCount ($iProfileId = 0)
+    {
+        
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return '';
+        
+        $oConnection = BxDolConnection::getObjectInstance('sys_profiles_friends');
+        if(!$oConnection)
+            return false;
+
+        return $oConnection->getConnectedInitiatorsCount($iProfileId, true);
+    }
+    
+    public function serviceProfileFollowersCount ($iProfileId = 0)
+    {
+        
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return '';
+        
+        $oConnection = BxDolConnection::getObjectInstance('sys_profiles_subscriptions');
+        if(!$oConnection)
+            return false;
+
+        return $oConnection->getConnectedInitiatorsCount($iProfileId);
+    }
+
+    public function serviceProfileFollowingCount ($iProfileId = 0)
+    {
+        
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return '';
+        
+        $oConnection = BxDolConnection::getObjectInstance('sys_profiles_subscriptions');
+        if(!$oConnection)
+            return false;
+
+        return $oConnection->getConnectedContentCount($iProfileId);
+    }
+
+    public function serviceProfileRecommendationFollowingCount ($iProfileId = 0, $aParams = [])
+    {
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return '';
+        
+        $oRecommendation = BxDolRecommendation::getObjectInstance('sys_subscriptions');
+        if(!$oRecommendation)
+            return false;
+
+        $aModules = bx_srv('system', 'get_modules_by_type', ['profile', ['name_as_key' => true]]);
+
+        return $oRecommendation->getCount($iProfileId, array_merge([
+            'type' => !empty($aModules) && is_array($aModules) ? array_keys($aModules) : '',
+        ], $aParams));
+    }
+    
+    public function serviceProfileRecommendationFriendsCount ($iProfileId = 0, $aParams = [])
+    {
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return '';
+
+        $oRecommendation = BxDolRecommendation::getObjectInstance('sys_friends');
+        if(!$oRecommendation)
+            return false;
+
+        return $oRecommendation->getCount($iProfileId, $aParams);
+    }
+
+    public function serviceBrowseFriendRequests ($iProfileId = 0, $aParams = [])
+    {
+        if($this->_bIsApi)
+            $aParams = bx_api_get_browse_params($aParams);
+
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return '';
+
+        $aParams = array_merge([
+            'params' => false,
+            'design_box' => BX_DB_PADDING_DEF,
+            'empty_message' => false,
+            'ajax_paginate' => true,
+        ], $aParams);
+
+        $aParamsBrowse = ['profile' => $iProfileId];
+        if(!empty($aParams['params']) && is_array($aParams['params']))
+            $aParamsBrowse = array_merge($aParamsBrowse, $aParams['params']);
+
+        return $this->_serviceBrowseConnections('friend_requests', $aParamsBrowse, $aParams['design_box'], $aParams['empty_message'], $aParams['ajax_paginate']);
+    }
+
+    public function serviceBrowseFriendRequested ($iProfileId = 0, $aParams = [])
+    {
+        if($this->_bIsApi)
+            $aParams = bx_api_get_browse_params($aParams);
+
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return '';
+
+        $aParams = array_merge([
+            'params' => false,
+            'design_box' => BX_DB_PADDING_DEF,
+            'empty_message' => false,
+            'ajax_paginate' => true,
+        ], $aParams);
+
+        $aParamsBrowse = ['profile' => $iProfileId];
+        if(!empty($aParams['params']) && is_array($aParams['params']))
+            $aParamsBrowse = array_merge($aParamsBrowse, $aParams['params']);
+
+        return $this->_serviceBrowseConnections('friend_requested', $aParamsBrowse, $aParams['design_box'], $aParams['empty_message'], $aParams['ajax_paginate']);
+    }
+
+    public function serviceBrowseSubscriptions ($iProfileId = 0, $aParams = [])
+    {
+        if($this->_bIsApi)
+            $aParams = bx_api_get_browse_params($aParams);
+
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return '';
+
+        $aParams = array_merge([
+            'params' => false,
+            'design_box' => BX_DB_PADDING_DEF,
+            'empty_message' => false,
+            'ajax_paginate' => true,
+        ], $aParams);
+
+        $aParamsBrowse = ['profile' => $iProfileId];
+        if(!empty($aParams['params']) && is_array($aParams['params']))
+            $aParamsBrowse = array_merge($aParamsBrowse, $aParams['params']);
+
+        return $this->_serviceBrowseConnections('subscriptions', $aParamsBrowse, $aParams['design_box'], $aParams['empty_message'], $aParams['ajax_paginate']);
+    }
+
+    public function serviceBrowseSubscribedMe ($iProfileId = 0, $aParams = [])
+    {
+        if($this->_bIsApi)
+            $aParams = bx_api_get_browse_params($aParams);
+
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return '';
+
+        $aParams = array_merge([
+            'params' => false,
+            'design_box' => BX_DB_PADDING_DEF,
+            'empty_message' => false,
+            'ajax_paginate' => true,
+        ], $aParams);
+
+        $aParamsBrowse = ['profile' => $iProfileId];
+        if(!empty($aParams['params']) && is_array($aParams['params']))
+            $aParamsBrowse = array_merge($aParamsBrowse, $aParams['params']);
+
+        return $this->_serviceBrowseConnections('subscribed_me', $aParamsBrowse, $aParams['design_box'], $aParams['empty_message'], $aParams['ajax_paginate']);
+    }
+
+    public function serviceBrowseMembers($iProfileId, $sObject, $aParams = [])
+    {
+        if($this->_bIsApi)
+            $aParams = bx_api_get_browse_params($aParams);
+
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return '';
+
+        $aParams = array_merge([
+            'params' => false,
+            'design_box' => BX_DB_PADDING_DEF,
+            'empty_message' => false,
+            'ajax_paginate' => true,
+        ], $aParams);
+
+        $aParamsBrowse = [
+            'profile' => $iProfileId, 
+            'object' => $sObject
+        ];
+        if(!empty($aParams['params']) && is_array($aParams['params']))
+            $aParamsBrowse = array_merge($aParamsBrowse, $aParams['params']);
+
+        return $this->_serviceBrowseConnections('members', $aParamsBrowse, $aParams['design_box'], $aParams['empty_message'], $aParams['ajax_paginate']);
+    }
+
+    public function serviceBrowseConnections ($iProfileId = 0, $aParams = [])
+    {
+        if($this->_bIsApi)
+            $aParams = bx_api_get_browse_params($aParams);
+
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return '';
+
+        $aParams = array_merge([
+            'params' => false,
+            'design_box' => BX_DB_PADDING_DEF,
+            'empty_message' => false,
+            'ajax_paginate' => true,
+        ], $aParams);
+
+        $aParamsBrowse = [
+            'profile' => $iProfileId, 
+            'object' => 'sys_profiles_friends',
+            'type' => 'content',
+            'mutual' => false,
+            'profile2' => 0
+        ];
+        if(!empty($aParams['params']) && is_array($aParams['params']))
+            $aParamsBrowse = array_merge($aParamsBrowse, $aParams['params']);
+
+        return $this->_serviceBrowseConnections('connections', $aParamsBrowse, $aParams['design_box'], $aParams['empty_message'], $aParams['ajax_paginate']);
+    }
+
+    public function serviceBrowseRecommendationsFriends ($iProfileId = 0, $aParams = [])
+    {
+        if($this->_bIsApi)
+            $aParams = bx_api_get_browse_params($aParams, true);
+
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return '';
+
+        $aParams = array_merge([
+            'empty_message' => false,
+            'start' => 0,
+            'per_page' => 0
+        ], $aParams);
+
+        if(($iStartGet = bx_get('start')) !== false)
+            $aParams['start'] = (int)$iStartGet;
+
+        if(($iPerPageGet = bx_get('per_page')) !== false)
+            $aParams['per_page'] = (int)$iPerPageGet;
+
+        $oRecommendation = BxDolRecommendation::getObjectInstance('sys_friends');
+        if(!$oRecommendation)
+            return false;
+
+        if(bx_is_api()) {
+            $aData = $oRecommendation->getCodeAPI($iProfileId, $aParams);
+            $aData = array_merge($aData, [
+                'module' => 'system',
+                'unit' => 'mixed', 
+                'request_url' => '/api.php?r=system/browse_recommendations_friends/TemplServiceProfiles&params[]=' . $iProfileId . '&params[]='
+            ]);
+
+            return [bx_api_get_block('browse', $aData)];
+        }
+
+        $sCode = $oRecommendation->getCode($iProfileId, $aParams);
+        if(!$sCode && $aParams['empty_message'])
+            $sCode = MsgBox(_t('_Empty'));
+
+        return $sCode;
+    }
+
+    public function serviceBrowseRecommendationsSubscriptions ($iProfileId = 0, $aParams = [])
+    {
+        if($this->_bIsApi)
+            $aParams = bx_api_get_browse_params($aParams, true);
+
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return '';
+
+        $aModules = bx_srv('system', 'get_modules_by_type', ['profile', ['name_as_key' => true]]);
+
+        $aParams = array_merge([
+            'empty_message' => false,
+            'type' => !empty($aModules) && is_array($aModules) ? array_keys($aModules) : '',
+            'start' => 0,
+            'per_page' => 0
+        ], $aParams);
+
+        if(($iStartGet = bx_get('start')) !== false)
+            $aParams['start'] = (int)$iStartGet;
+
+        if(($iPerPageGet = bx_get('per_page')) !== false)
+            $aParams['per_page'] = (int)$iPerPageGet;
+
+        $oRecommendation = BxDolRecommendation::getObjectInstance('sys_subscriptions');
+        if(!$oRecommendation)
+            return false;
+
+        if(bx_is_api()) {
+            $aData = $oRecommendation->getCodeAPI($iProfileId, $aParams);
+            $aData = array_merge($aData, [
+                'module' => 'system',
+                'unit' => 'mixed',
+                'request_url' => '/api.php?r=system/browse_recommendations_subscriptions/TemplServiceProfiles&params[]=' . $iProfileId . '&params[]='
+            ]);
+
+            return [bx_api_get_block('browse', $aData)];
+        }
+
+        $sCode = $oRecommendation->getCode($iProfileId, $aParams);
+        if(!$sCode && $aParams['empty_message'])
+            $sCode = MsgBox(_t('_Empty'));
+
+        return $sCode;
+    }
+
+    public function serviceAccountProfileSwitcher ($iAccountId = false, $iActiveProfileId = null, $sUrlProfileAction = '', $bShowAll = 0, $sButtonTitle = '', $sProfileTemplate = '')
+    {
+    	$oTemplate = BxDolTemplate::getInstance();
+
+        $oProfilesQuery = BxDolProfileQuery::getInstance();
+
+        $aProfiles = $oProfilesQuery->getProfilesByAccount($iAccountId ? $iAccountId : getLoggedId());
+        if (!$aProfiles)
+            return false;
+
+        if (null === $iActiveProfileId)
+            $iActiveProfileId = bx_get_logged_profile_id();
+
+        $oModuleDb = BxDolModuleQuery::getInstance();
+
+        $aVars = array (
+            'bx_repeat:row' => array(),
+        );
+        
+        bx_alert('system', 'account_profile_switcher', 0, false, array(
+            'account_id' => $iAccountId,
+            'active_profile_id' => &$iActiveProfileId,
+            'url_profile_action' => &$sUrlProfileAction,
+            'show_all' => &$bShowAll,
+            'button_title' => &$sButtonTitle,
+            'profile_template' => &$sProfileTemplate,
+            'profiles' => &$aProfiles
+        ));
+        
+        foreach ($aProfiles as $aProfile) {
+            if (!$bShowAll && $iActiveProfileId == $aProfile['id'])
+                continue;
+
+            if(!$oModuleDb->isEnabledByName($aProfile['type']))
+                continue;
+
+            if (!BxDolService::call($aProfile['type'], 'act_as_profile'))
+                continue;
+            
+            if($this->_bIsApi){
+                $o = BxDolProfile::getInstance($aProfile['id']);
+                $aProfilesData[] = [
+                    'id' => $o->id(),
+                    'display_name' => $o->getDisplayName(),
+                    'url' => bx_api_get_relative_url($o->getUrl()),
+                    'avatar' => $o->getAvatar()
+                ];
+            }
+
+            $aVars['bx_repeat:row'][] = array (
+                'class' => $iActiveProfileId == $aProfile['id'] ? '' : 'bx-def-color-bg-box',
+                'bx_if:active' => array (
+                    'condition' => $iActiveProfileId == $aProfile['id'],
+                    'content' => array('id' => $aProfile['id']),
+                ),
+                'bx_if:inactive' => array (
+                    'condition' => $iActiveProfileId != $aProfile['id'],
+                    'content' => array(
+                        'id' => $aProfile['id'], 
+                        'button_title' => $sButtonTitle ? $sButtonTitle : _t('_sys_txt_switch_profile_context'),
+                        'url_switch' => $sUrlProfileAction ? str_replace('{profile_id}', $aProfile['id'], $sUrlProfileAction) : BxDolProfile::getSwitchToProfileRedirectUrl($aProfile['id'])
+                    ),
+                ),
+                'unit' => BxDolService::call($aProfile['type'], 'profile_unit', array($aProfile['content_id'], array('template' => $sProfileTemplate))),
+            );
+        }
+
+        if($this->_bIsApi){
+            return [bx_api_get_block ('profile_switcher', ['active_profile_id' => $iActiveProfileId, 'profiles' => $aProfilesData])];
+        }
+        
+        $oTemplate->addCss('account.css');
+        return array(
+            'content' => $oTemplate->parseHtmlByName('profile_switch_row.html', $aVars),
+        );
+    }
+
+    public function serviceAccountProfileSwitcherAll ($iAccountId = false, $iActiveProfileId = null, $sUrlProfileAction = '', $bShowAll = true, $sButtonTitle = '', $sProfileTemplate = '')
+    {
+        return $this->serviceAccountProfileSwitcher($iAccountId, $iActiveProfileId, $sUrlProfileAction, $bShowAll, $sButtonTitle, $sProfileTemplate);
+    }
+
+    public function serviceProfileSettingsCfilter($iProfileId = false)
+    {
+        if(!BxDolContentFilter::getInstance()->isEnabled()) {
+            BxDolTemplate::getInstance()->displayPageNotFound();
+            exit;
+        }
+
+        // set settings submenu
+        $oMenuSubmenu = BxDolMenu::getObjectInstance('sys_site_submenu');
+        if ($oMenuSubmenu) {
+            $oMenuSubmenu->setObjectSubmenu('sys_account_settings_submenu', array (
+                'title' => _t('_sys_menu_item_title_account_settings'),
+                'link' => BX_DOL_URL_ROOT . 'member.php',
+                'icon' => '',
+            ));
+        }
+
+        if($iProfileId === false)
+            $iProfileId = bx_get_logged_profile_id();
+
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        $aProfileInfo = $oProfile ? $oProfile->getInfo() : false;
+        if(empty($aProfileInfo) || !is_array($aProfileInfo))
+            return MsgBox(_t('_sys_txt_error_profile_is_not_defined'));
+
+        $sForm = 'sys_profile';
+        $sFormDisplay = 'sys_profile_cf_set';
+        $oForm = BxDolForm::getObjectInstance($sForm, $sFormDisplay);
+        if(!$oForm)
+            return MsgBox(_t('_sys_txt_error_occured'));
+
+        $oForm->initChecker($aProfileInfo);
+        if(!$oForm->isSubmittedAndValid())
+            return $sCode = $oForm->getCode();
+
+        if(!$oForm->update($iProfileId)) {
+            if (!$oForm->isValid())
+                return $oForm->getCode();
+            else
+                return MsgBox(_t('_sys_txt_error_profile_update'));
+        }
+
+        // display result message
+        $sMsg = MsgBox(_t('_' . $sFormDisplay . '_successfully_submitted'));
+        return $sMsg . $oForm->getCode();
+    }
+
+    public function serviceIsAllowedCfilter($sAction, $iValue, $aProfileInfo)
+    {
+        if(!in_array($sAction, ['watch', 'use']) || !isset($aProfileInfo['birthday']))
+            return false;
+
+        $iAge = 0;
+        if(!empty($aProfileInfo['birthday']) && !in_array($aProfileInfo['birthday'], array('0000-00-00', '0000-00-00 00:00:00'))) 
+            $iAge = bx_birthday2age($aProfileInfo['birthday']);
+
+        $iResult = 0;
+        switch($iValue) {
+            case 1:
+                $iResult = $iValue;
+                break;
+
+            case 2: 
+                if($iAge >= 6)
+                   $iResult = $iValue;
+                break;
+
+            case 3:
+                if($iAge >= 13)
+                   $iResult = $iValue;
+                break;
+
+            case 4:
+                if($iAge >= 17)
+                   $iResult = $iValue;
+                break;
+
+            case 5:
+                if($iAge >= 21)
+                   $iResult = $iValue;
+                break;
+        }
+
+        if($iResult != 0)
+            $iResult = 1 << ($iResult - 1);
+
+        return $iResult;
+    }
+
+    public function serviceIsEnabledCfilter()
+    {
+        return BxDolContentFilter::getInstance()->isEnabled();
+    }
+
+    public function serviceUpdateSettings($iProfileId = false, $sSettings = '')
+    {
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if(!$oProfile)
+            return '';
+        
+        $sModule = $oProfile->getModule();
+        
+        $sMethod = 'profile_settings_set';
+        if(!BxDolRequest::serviceExists($sModule, $sMethod))
+            return '';
+
+        BxDolService::call($sModule, $sMethod, [$oProfile->getContentId(), json_decode($sSettings)]);
+    }
+
+    
+    protected function _getIcon ($sIcon)
+    {
+        return BxTemplFunctions::getInstance()->getIcon($sIcon);
+    }
+	
+    protected function _getLatestModuleTimestamp ()
+    {
+        $aModules = BxDolModuleQuery::getInstance()->getModulesBy(array('type' => 'modules', 'active' => 1, 'order_by' => '`date` ASC'));
+        if (empty($aModules))
+            return 0;
+        $aModuleNewest = array_pop($aModules);
+        return $aModuleNewest['date'];
+    }
+
+    protected function _getProfilesModules($bForceActAsProfile = true)
+    {
+        $aRet = array();
+
+        $aModules = BxDolModuleQuery::getInstance()->getModulesBy(array('type' => 'modules', 'active' => 1));
+        foreach($aModules as $aModule) {
+            $oModule = BxDolModule::getInstance($aModule['name']);
+            if(!($oModule instanceof iBxDolProfileService)) 
+                continue;
+
+            if($bForceActAsProfile && !$oModule->serviceActAsProfile())
+                continue;
+
+            $aRet[] = $aModule;
+        }
+
+        return $aRet;
+    }
+
+    protected function _serviceBrowseConnections ($sMode, $aParams = [], $iDesignBox = BX_DB_PADDING_DEF, $bDisplayEmptyMsg = false, $bAjaxPaginate = true)
+    {
+        $sClass = 'BxTemplProfileSearchResult';
+        bx_import($sClass);
+        $o = new $sClass($sMode, $aParams);
+
+        $o->setDesignBoxTemplateId($iDesignBox);
+        $o->setDisplayEmptyMsg($bDisplayEmptyMsg);
+        $o->setAjaxPaginate($bAjaxPaginate);
+        $o->setUnitParams(['context' => $sMode]);
+
+        if ($o->isError)
+            return '';
+
+        if ($s = $o->processing()) {
+            if(!bx_is_api())
+                return $s;
+
+            return [bx_api_get_block('browse', $s)];
+        }
+        else
+            return '';
+    }
+}
+
+/** @} */

+ 1450 - 0
upgrade/files/14.0.0.A2-14.0.0.A3/files/template/scripts/BxBaseServices.php

@@ -0,0 +1,1450 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * System services.
+ */
+class BxBaseServices extends BxDol implements iBxDolProfileService
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    public function serviceIsPublicService($s)
+    {
+        $sService = bx_gen_method_name($s);
+        $aServices = $this->serviceGetPublicServices();
+        return isset($aServices[$sService]);
+    }
+
+    public function serviceGetPublicServices()
+    {
+        return array (
+            'GetProductsNames' => 'BxBaseServices',
+            'GetPageByRequest' => 'BxBaseServicePages',
+        );
+    }
+
+    public function serviceIsSafeService($s)
+    {
+        $sService = bx_gen_method_name($s);
+        $aSafeServices = $this->serviceGetSafeServices();
+        return isset($aSafeServices[$sService]);
+    }
+
+    public function serviceGetSafeServices()
+    {
+        return array(
+            'GetCreatePostForm' => 'BxBaseServices',
+            'GetProductsNames' => 'BxBaseServices',
+            'KeywordSearch' => 'BxBaseServices',
+            'GetDataSearchApi' => 'BxBaseServices',
+            'Cmts' => 'BxBaseServices',
+            'GetFooter' => 'BxBaseServices',
+
+            'CreateAccountForm' => 'BxBaseServiceAccount',
+            'AccountSettingsEmail' => 'BxBaseServiceAccount',
+            'AccountSettingsPassword' => 'BxBaseServiceAccount',
+            'AccountSettingsDelAccount' => 'BxBaseServiceAccount',
+            'AccountSettingsInfo' => 'BxBaseServiceAccount',
+            'ForgotPassword' => 'BxBaseServiceAccount',
+            'SwitchProfile' => 'BxBaseServiceAccount',
+            'AccountProfileSwitcher' => 'BxBaseServiceAccount',
+            'EmailConfirmation' => 'BxBaseServiceAccount',
+            'ConfirmEmail' => 'BxBaseServiceAccount',
+            
+            'CategoriesList' => 'BxBaseServiceCategory',
+
+            'Test' => 'BxBaseServiceLogin',
+            'MemberAuthCode' => 'BxBaseServiceLogin',
+            'LoginForm' => 'BxBaseServiceLogin',
+            'LoginFormOnly' => 'BxBaseServiceLogin',
+            'Logout' => 'BxBaseServiceLogin',
+        
+            'KeywordsCloud' => 'BxBaseServiceMetatags',
+
+            'AccountProfileSwitcher' => 'BxBaseServiceProfiles',
+            'ProfileMembership' => 'BxBaseServiceProfiles',
+            'ProfileNotifications' => 'BxBaseServiceProfiles',
+            'GetCountOnlineProfiles' => 'BxBaseServiceProfiles',
+            'BrowseRecommendationsFriends' => 'BxBaseServiceProfiles',
+            'BrowseRecommendationsSubscriptions' => 'BxBaseServiceProfiles',
+            'BrowseFriends' => 'BxBaseServiceProfiles',
+            'BrowseFriendRequests' => 'BxBaseServiceProfiles',
+            'BrowseFriendRequested' => 'BxBaseServiceProfiles',
+            'BrowseSubscribedMe' => 'BxBaseServiceProfiles',
+            'BrowseSubscriptions' => 'BxBaseServiceProfiles',
+            'BrowseMembers' => 'BxBaseServiceProfiles',
+            'UpdateSettings' => 'BxBaseServiceProfiles',
+            'Befriend' => 'BxBaseServiceProfiles',
+            
+            'GetChartGrowth' => 'BxBaseChartServices',
+            'GetChartStats' => 'BxBaseChartServices',
+            'GetDataByInterval' => 'BxBaseChartServices',
+            
+            'GetCartItemsCount' => 'BxBasePaymentsServices',
+            'GetOrdersCount' => 'BxBasePaymentsServices',
+
+            'Do' => 'BxBaseReportServices',
+            'GetPerformedBy' => 'BxBaseReportServices',
+
+            'Do' => 'BxBaseVoteServices',
+            'GetPerformedBy' => 'BxBaseVoteServices',
+
+            'Perform' => 'BxBaseServiceConnections',
+            'Perform' => 'BxBaseServiceRecommendations',
+            'GetDataApi' => 'BxBaseCmtsServices',
+
+            'GetDataApi' => 'BxBaseUploaderServices',
+            
+            'GetStatBlock' => 'BxBaseDashboardServices',
+            
+            'PerfomActionApi' => 'BxBaseServiceGrid',
+
+            'GetLabels' => 'BxBaseLabelsServices',
+            'GetForm' => 'BxBaseSearchExtendedServices',
+            'GetResults' => 'BxBaseSearchExtendedServices'
+        );
+    }
+
+    public function serviceGetPreloaderContent($sName) {
+        $sResult = '';
+
+        switch($sName) {
+            case 'icons':
+                $sResult = getParam('sys_css_icons_default');
+                break;
+
+            case 'tailwind':
+                $sFile = getParam('sys_css_tailwind_default');
+                if(empty($sFile))
+                    $sFile = 'tailwind.min.css';
+
+                $sResult = '{dir_plugins_public}tailwind/css/|' . $sFile;
+                break;
+        }
+
+        return $sResult;
+    }
+            
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-general General
+     * @subsubsection bx_system_general-module_check_for_update module_check_for_update
+     * 
+     * @code bx_srv('system', 'module_check_for_update', ["bx_posts"]); @endcode
+     * @code bx_srv('system', 'module_check_for_update', ["bx_posts"], 'TemplServices'); @endcode
+     * @code {{~system:module_check_for_update:TemplServices["bx_posts"]~}} @endcode
+     * 
+     * Checks whether an update is available for requested module or not.
+     * @param $mixedModule - module ID/name to check the updates for.
+     * @return array with information about the update, false if nothing was found
+     * 
+     * @see BxBaseServices::serviceModuleCheckForUpdate
+     */
+    /** 
+     * @ref bx_system_general-module_check_for_update "module_check_for_update"
+     */
+    public function serviceModuleCheckForUpdate($mixedModule)
+    {
+        $aModule = BxDolModuleQuery::getInstance()->{'getModuleBy' . (is_numeric($mixedModule) ? 'Id' : 'Name')}($mixedModule, false);
+        if(empty($aModule) || !is_array($aModule))
+            return false;
+
+        $aUpdates = BxDolStudioInstallerUtils::getInstance()->checkUpdatesByModule($aModule['name']);
+        if(empty($aUpdates) || !is_array($aUpdates)) 
+            return false;
+
+        $aUpdate = array_shift($aUpdates);
+        return array(
+            'version_from' => $aUpdate['file_version'],
+            'version_to' => $aUpdate['file_version_to'],
+            'file_id' => $aUpdate['file_id']
+        );
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-general General
+     * @subsubsection bx_system_general-module_update module_update
+     * 
+     * @code bx_srv('system', 'module_update', ["bx_posts"]); @endcode
+     * @code bx_srv('system', 'module_update', ["bx_posts"], 'TemplServices'); @endcode
+     * @code {{~system:module_update:TemplServices["bx_posts"]~}} @endcode
+     * 
+     * Download and install an update for requested module. The operation may be performed 
+     * immediately or by Transient Cron task. The second way is more common.
+     * @param $mixedModule - module ID/name to check the updates for.
+     * @return integer value determining the result of the operation. It can be one of the following values:
+     * BX_DOL_STUDIO_IU_RC_SUCCESS, BX_DOL_STUDIO_IU_RC_FAILED or BX_DOL_STUDIO_IU_RC_SCHEDULED.
+     * 
+     * @see BxBaseServices::serviceModuleUpdate
+     */
+    /** 
+     * @ref bx_system_general-module_update "module_update"
+     */
+    public function serviceModuleUpdate($mixedModule)
+    {
+        $aModule = BxDolModuleQuery::getInstance()->{'getModuleBy' . (is_numeric($mixedModule) ? 'Id' : 'Name')}($mixedModule, false);
+        if(empty($aModule) || !is_array($aModule))
+            return BX_DOL_STUDIO_IU_RC_FAILED;
+
+        $aResult = BxDolStudioInstallerUtils::getInstance()->downloadUpdatePublic($aModule['name'], true);
+        return $aResult['code'];
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-general General
+     * @subsubsection bx_system_general-module_delete module_delete
+     * 
+     * @code bx_srv('system', 'module_delete', ["bx_posts"]); @endcode
+     * @code bx_srv('system', 'module_delete', ["bx_posts", true], 'TemplServices'); @endcode
+     * @code {{~system:module_delete:TemplServices["bx_posts"]~}} @endcode
+     * 
+     * Delete the requested module. By default only uninstalled module can be deleted.
+     * The operation may be performed immediately or by Transient Cron task. The second way is more common.
+     * @param $sModule - module name to delete.
+     * @param $bForceUninstall - if 'true' is passed the module will be uninstalled before deletion.
+     * @return integer value determining the result of the operation. It can be one of the following values:
+     * BX_DOL_STUDIO_IU_RC_SUCCESS, BX_DOL_STUDIO_IU_RC_FAILED or BX_DOL_STUDIO_IU_RC_SCHEDULED.
+     * 
+     * @see BxBaseServices::serviceModuleDelete
+     */
+    /** 
+     * @ref bx_system_general-module_delete "module_delete"
+     */
+    public function serviceModuleDelete($sModule, $bForceUninstall = false)
+    {
+        $sModulePath = '';
+        $oInstallerUtils = BxDolStudioInstallerUtils::getInstance();
+
+        $aModule = BxDolModuleQuery::getInstance()->getModuleByName($sModule, false);
+        if(!empty($aModule) && is_array($aModule)) {
+            if(!$bForceUninstall)
+                return BX_DOL_STUDIO_IU_RC_FAILED;
+
+            $sModulePath = $aModule['path'];
+
+            $aResult = $oInstallerUtils->perform($sModulePath, 'uninstall', array('auto_disable' => $bForceUninstall));
+            if($aResult['code'] != BX_DOL_STUDIO_IU_RC_SUCCESS)
+                return BX_DOL_STUDIO_IU_RC_FAILED;
+        }
+        else {
+            $aModules = $oInstallerUtils->getModules(false);
+            if(!isset($aModules[$sModule]))
+                return BX_DOL_STUDIO_IU_RC_FAILED;
+            
+            $sModulePath = $aModules[$sModule]['dir'];
+        }
+
+        $aResult = $oInstallerUtils->perform($sModulePath, 'delete');
+        return $aResult['code'];
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-general General
+     * @subsubsection bx_system_general-module_install module_install
+     * 
+     * @code bx_srv('system', 'module_install', ["bx_posts"]); @endcode
+     * @code bx_srv('system', 'module_install', ["bx_posts", true], 'TemplServices'); @endcode
+     * @code {{~system:module_install:TemplServices["bx_posts"]~}} @endcode
+     * 
+     * Install the requested module. By default the module will be installed only.
+     * @param $sModule - module name to install.
+     * @param $bForceEnable - if 'true' is passed the module will be automatically enabled after installation.
+     * @return boolean value determining the result of the operation.
+     * 
+     * @see BxBaseServices::serviceModuleInstall
+     */
+    /** 
+     * @ref bx_system_general-module_install "module_install"
+     */
+    public function serviceModuleInstall($sModule, $bForceEnable = false)
+    {
+        $oInstallerUtils = BxDolStudioInstallerUtils::getInstance();
+
+        $aModules = $oInstallerUtils->getModules(false);
+        if(!isset($aModules[$sModule]))
+            return false;
+
+        $aResult = $oInstallerUtils->perform($aModules[$sModule]['dir'], 'install', array('auto_enable' => $bForceEnable));
+        if($aResult['code'] != BX_DOL_STUDIO_IU_RC_SUCCESS)
+            return false;
+
+        return true;
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-general General
+     * @subsubsection bx_system_general-module_uninstall module_uninstall
+     * 
+     * @code bx_srv('system', 'module_uninstall', ["bx_posts"]); @endcode
+     * @code bx_srv('system', 'module_uninstall', ["bx_posts", true], 'TemplServices'); @endcode
+     * @code {{~system:module_uninstall:TemplServices["bx_posts"]~}} @endcode
+     * 
+     * Uninstall the requested module. By default only disabled module can be uninstalled.
+     * @param $mixedModule - module ID/name to uninstall.
+     * @param $bForceDisable - if 'true' is passed the module will be automatically disabled before uninstallation.
+     * @return boolean value determining the result of the operation.
+     * 
+     * @see BxBaseServices::serviceModuleUninstall
+     */
+    /** 
+     * @ref bx_system_general-module_uninstall "module_uninstall"
+     */
+    public function serviceModuleUninstall($mixedModule, $bForceDisable = false)
+    {
+        $aModule = BxDolModuleQuery::getInstance()->{'getModuleBy' . (is_numeric($mixedModule) ? 'Id' : 'Name')}($mixedModule, false);
+        if(empty($aModule) || !is_array($aModule))
+            return false;
+
+        if((int)$aModule['enabled'] != 0 && !$bForceDisable)
+            return false;
+
+        $aResult = BxDolStudioInstallerUtils::getInstance()->perform($aModule['path'], 'uninstall', array('auto_disable' => $bForceDisable));
+        if($aResult['code'] != BX_DOL_STUDIO_IU_RC_SUCCESS)
+            return false;
+
+        return true;
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-general General
+     * @subsubsection bx_system_general-module_enable module_enable
+     * 
+     * @code bx_srv('system', 'module_enable', ["bx_posts"]); @endcode
+     * @code bx_srv('system', 'module_enable', ["bx_posts"], 'TemplServices'); @endcode
+     * @code {{~system:module_enable:TemplServices["bx_posts"]~}} @endcode
+     * 
+     * Enable the requested module.
+     * @param $mixedModule - module ID/name to enable.
+     * @return boolean value determining the result of the operation.
+     * 
+     * @see BxBaseServices::serviceModuleEnable
+     */
+    /** 
+     * @ref bx_system_general-module_enable "module_enable"
+     */
+    public function serviceModuleEnable($mixedModule)
+    {
+        $aModule = BxDolModuleQuery::getInstance()->{'getModuleBy' . (is_numeric($mixedModule) ? 'Id' : 'Name')}($mixedModule, false);
+        if(empty($aModule) || !is_array($aModule))
+            return false;
+
+        if((int)$aModule['enabled'] != 0)
+            return false;
+
+        $aResult = BxDolStudioInstallerUtils::getInstance()->perform($aModule['path'], 'enable');
+        if($aResult['code'] != BX_DOL_STUDIO_IU_RC_SUCCESS)
+            return false;
+
+        return true;
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-general General
+     * @subsubsection bx_system_general-module_disable module_disable
+     * 
+     * @code bx_srv('system', 'module_disable', ["bx_posts"]); @endcode
+     * @code bx_srv('system', 'module_disable', ["bx_posts"], 'TemplServices'); @endcode
+     * @code {{~system:module_disable:TemplServices["bx_posts"]~}} @endcode
+     * 
+     * Disable the requested module.
+     * @param $mixedModule - module ID/name to disable.
+     * @return boolean value determining the result of the operation.
+     * 
+     * @see BxBaseServices::serviceModuleDisable
+     */
+    /** 
+     * @ref bx_system_general-module_disable "module_disable"
+     */
+    public function serviceModuleDisable($mixedModule)
+    {
+        $aModule = BxDolModuleQuery::getInstance()->{'getModuleBy' . (is_numeric($mixedModule) ? 'Id' : 'Name')}($mixedModule, false);
+        if(empty($aModule) || !is_array($aModule))
+            return false;
+
+        if((int)$aModule['enabled'] == 0)
+            return false;
+
+        $aResult = BxDolStudioInstallerUtils::getInstance()->perform($aModule['path'], 'disable');
+        if($aResult['code'] != BX_DOL_STUDIO_IU_RC_SUCCESS)
+            return false;
+
+        return true;
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-general General
+     * @subsubsection bx_system_general-is_module_installed is_module_installed
+     * 
+     * @code bx_srv('system', 'is_module_installed', ["bx_posts"]); @endcode
+     * @code bx_srv('system', 'is_module_installed', ["bx_posts"], 'TemplServices'); @endcode
+     * @code {{~system:is_module_installed:TemplServices["bx_posts"]~}} @endcode
+     * 
+     * Checks whether the requested module is installed or not.
+     * @param $mixedModule - module ID/name to check.
+     * @return boolean value determining the result of the operation.
+     * 
+     * @see BxBaseServices::serviceIsModuleInstalled
+     */
+    /** 
+     * @ref bx_system_general-is_module_installed "is_module_installed"
+     */
+    public function serviceIsModuleInstalled($mixedModule)
+    {
+        $aModule = BxDolModuleQuery::getInstance()->{'getModuleBy' . (is_numeric($mixedModule) ? 'Id' : 'Name')}($mixedModule, false);
+        return !empty($aModule) && is_array($aModule);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-general General
+     * @subsubsection bx_system_general-is_module_enabled is_module_enabled
+     * 
+     * @code bx_srv('system', 'is_module_enabled', ["bx_posts"]); @endcode
+     * @code bx_srv('system', 'is_module_enabled', ["bx_posts"], 'TemplServices'); @endcode
+     * @code {{~system:is_module_enabled:TemplServices["bx_posts"]~}} @endcode
+     * 
+     * Checks whether the requested module is enabled or not.
+     * @param $mixedModule - module ID/name to check.
+     * @return boolean value determining the result of the operation.
+     * 
+     * @see BxBaseServices::serviceIsModuleEnabled
+     */
+    /** 
+     * @ref bx_system_general-is_module_enabled "is_module_enabled"
+     */
+    public function serviceIsModuleEnabled($mixedModule)
+    {
+        $aModule = BxDolModuleQuery::getInstance()->{'getModuleBy' . (is_numeric($mixedModule) ? 'Id' : 'Name')}($mixedModule, false);
+        return !empty($aModule) && is_array($aModule) && (int)$aModule['enabled'] != 0;
+    }
+
+    /**
+     * Checks whether a module is a 'Content' module or not.
+     * 
+     * @param string $mixedModule - module name or an instance of Module class.
+     * @return boolean result of operation.
+     */
+    public function serviceIsModuleContent($mixedModule)
+    {
+        /*
+         * TODO: Update after UNA 14.0.0-A3 was released.
+         *  Check for 'subtypes' should be removed. Use only DB call, because it's more accurate.
+         */
+        if(is_string($mixedModule)) {
+            $oModuleQuery = BxDolModuleQuery::getInstance();
+            if($oModuleQuery->isFieldExists('sys_modules', 'subtypes'))
+                return $oModuleQuery->isModuleContent($mixedModule);
+
+            $mixedModule = BxDolModule::getInstance($mixedModule);
+        }
+
+        return
+            $mixedModule instanceof BxDolModule &&
+            $mixedModule instanceof iBxDolContentInfoService &&
+            !($mixedModule instanceof iBxDolProfileService) &&
+            BxDolRequest::serviceExists($mixedModule->_aModule, 'is_allowed_post_in_context') &&
+            bx_srv($mixedModule->getName(), 'is_allowed_post_in_context');
+    }
+
+    /**
+     * Checks whether a module is a 'Context' module or not.
+     * 
+     * @param string $mixedModule - module name or an instance of Module class.
+     * @return boolean result of operation.
+     */
+    public function serviceIsModuleContext($mixedModule)
+    {
+        /*
+         * TODO: Update after UNA 14.0.0-A3 was released.
+         *  Check for 'subtypes' should be removed. Use only DB call, because it's more accurate.
+         */
+        if(is_string($mixedModule)) {
+            $oModuleQuery = BxDolModuleQuery::getInstance();
+            if($oModuleQuery->isFieldExists('sys_modules', 'subtypes'))
+                return $oModuleQuery->isModuleContext($mixedModule);
+
+            $mixedModule = BxDolModule::getInstance($mixedModule);
+        }
+
+        return $mixedModule instanceof BxDolModule && $mixedModule instanceof iBxDolProfileService;
+    }
+
+    /**
+     * Checks whether a module is a 'Profile' module or not.
+     * 
+     * @param string $mixedModule - module name or an instance of Module class.
+     * @return boolean result of operation.
+     */
+    public function serviceIsModuleProfile($mixedModule)
+    {
+        /*
+         * TODO: Update after UNA 14.0.0-A3 was released.
+         *  Check for 'subtypes' should be removed. Use only DB call, because it's more accurate.
+         */
+        if(is_string($mixedModule)) {
+            $oModuleQuery = BxDolModuleQuery::getInstance();
+            if($oModuleQuery->isFieldExists('sys_modules', 'subtypes'))
+                return $oModuleQuery->isModuleProfile($mixedModule);
+
+            $mixedModule = BxDolModule::getInstance($mixedModule);
+        }
+
+        return $mixedModule instanceof BxDolModule && $mixedModule instanceof iBxDolProfileService && $mixedModule->serviceActAsProfile();
+    }
+
+    /**
+     * Get modules by type. Available types are 'content', 'context', 'profile'.
+     * 
+     * @param type $sType - string with type.
+     * @return array of modules.
+     */
+    public function serviceGetModulesByType($sType, $aParams = [])
+    {
+        $aResults = [];
+
+        //TODO: Remove old variant (from ELSE) after UNA 14.0.0-A3 was released.
+        $oModuleQuery = BxDolModuleQuery::getInstance();
+        if($oModuleQuery->isFieldExists('sys_modules', 'subtypes')) {
+            if(!$oModuleQuery->checkModulesSubtypes()) {
+                $oInstallerUtils = new BxDolInstallerUtils();
+                $aModules = $oModuleQuery->getModulesBy(['type' => 'modules']);
+                foreach($aModules as $aModule)
+                    if(($iModuleSubtypes = $oInstallerUtils->getSubtypes($aModule['name'])) != 0)
+                        $oModuleQuery->updateModule(['subtypes' => $iModuleSubtypes], ['id' => $aModule['id']]);
+            }
+
+            $iSubtypes = 0;
+            switch($sType) {
+                case 'content';
+                    $iSubtypes = BX_DOL_MODULE_SUBTYPE_TEXT;
+                    break;
+
+                case 'context';
+                    $iSubtypes = BX_DOL_MODULE_SUBTYPE_CONTEXT;
+                    break;
+
+                case 'profile';
+                    $iSubtypes = BX_DOL_MODULE_SUBTYPE_PROFILE;
+                    break;
+            }
+
+            $aResults = $oModuleQuery->getModulesBy(array_merge([
+                'type' => 'modules_subtypes', 
+                'value' => $iSubtypes, 
+                'active' => 1
+            ], array_intersect_key($aParams, array_flip(['id_as_key', 'name_as_key']))));
+        }
+        else {
+            $bIdAsKey = isset($aParams['id_as_key']) && $aParams['id_as_key'] === true;
+            $bNameAsKey = !$bIdAsKey && isset($aParams['name_as_key']) && $aParams['name_as_key'] === true;
+
+            $aModules = $oModuleQuery->getModulesBy(['type' => 'modules', 'active' => 1]);
+            foreach($aModules as $aModule) {
+                $oModule = BxDolModule::getInstance($aModule['name']);
+                if(!$oModule)
+                    continue;
+
+                if($sType == 'content' && !$this->serviceIsModuleContent($oModule))
+                    continue;
+
+                if($sType == 'context' && !$this->serviceIsModuleContext($oModule))
+                    continue;
+
+                if($sType == 'profile' && !$this->serviceIsModuleProfile($oModule))
+                    continue;
+
+                if($bIdAsKey)
+                    $aResults[$aModule['id']] = $aModule;
+                else if($bNameAsKey)
+                    $aResults[$aModule['name']] = $aModule;
+                else
+                    $aResults[] = $aModule;
+            }
+        }
+
+        return $aResults;
+    }
+
+    public function serviceProfileUnit ($iContentId, $aParams = array())
+    {
+        return $this->_serviceProfileFunc('getUnit', $iContentId, $aParams);
+    }
+
+    public function serviceHasImage ($iContentId)
+    {
+        return false;
+    }
+
+    public function serviceProfilePicture ($iContentId)
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account.svg');
+    }
+
+    public function serviceProfileAvatar ($iContentId)
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account.svg');
+    }
+
+    public function serviceProfileCover ($iContentId)
+    {
+        return '';
+    }
+
+    public function serviceProfileEditUrl ($iContentId)
+    {
+        return bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=account-settings-info'));
+    }
+
+    public function serviceProfileThumb ($iContentId)
+    {
+        return $this->_serviceProfileFunc('getThumb', $iContentId);
+    }
+
+    public function serviceProfileIcon ($iContentId)
+    {
+        return $this->_serviceProfileFunc('getIcon', $iContentId);
+    }
+
+    public function serviceProfileName ($iContentId)
+    {
+        return $this->_serviceProfileFunc('getDisplayName', $iContentId);
+    }
+
+    public function serviceProfileUrl ($iContentId)
+    {
+        return $this->_serviceProfileFunc('getUrl', $iContentId);
+    }
+
+    /**
+     * @see iBxDolProfileService::serviceCheckAllowedProfileView
+     */ 
+    public function serviceCheckAllowedProfileView($iContentId)
+    {
+        return _t('_Access denied');
+    }
+
+    /**
+     * @see iBxDolProfileService::serviceCheckAllowedProfileContact
+     */ 
+    public function serviceCheckAllowedProfileContact($iContentId)
+    {
+        return _t('_Access denied');
+    }
+
+    /**
+     * @see iBxDolProfileService::serviceCheckAllowedPostInProfile
+     */ 
+    public function serviceCheckAllowedPostInProfile($iContentId, $sPostModule = '')
+    {
+        return _t('_Access denied');
+    }
+
+    /**
+     * @see iBxDolProfileService::serviceCheckAllowedModuleActionInProfile
+     */
+    public function serviceCheckAllowedModuleActionInProfile($iContentId, $sPostModule, $sActionName)
+    {
+        return _t('_Access denied');
+    }
+
+    /**
+     * @see iBxDolProfileService::serviceGetSpaceTitle
+     */ 
+    public function serviceGetSpaceTitle()
+    {
+        return '';
+    }
+
+    /**
+     * @see iBxDolProfileService::serviceGetParticipatingProfiles
+     */ 
+    public function serviceGetParticipatingProfiles($iProfileId, $aConnectionObject = false)
+    {
+        return array();
+    }
+    
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-general General
+     * @subsubsection bx_system_general-get_create_post_form get_create_post_form
+     * 
+     * @code bx_srv('system', 'get_create_post_form', [false, "bx_posts"], 'TemplServices'); @endcode
+     * @code {{~system:get_create_post_form:TemplServices[false,"bx_posts"]~}} @endcode
+     * 
+     * Get United Create Post form.
+     * @param $mixedContextId - context which the post will be created in:
+     *      - false = 'Public' post form;
+     *      - 0 = 'Profile' post form, which allows to post in your own profile and connections;
+     *      - n = 'Context' post form, which allows to post in context (3d party profile, group, event, etc).
+     * @param $sDefault - tab selected by default.
+     * @param $aCustom - an array with custom paramaters.
+     * @return string with form content
+     * 
+     * @see BxBaseServices::serviceGetCreatePostForm
+     */
+    /** 
+     * @ref bx_system_general-get_create_post_form "get_create_post_form"
+     */
+    public function serviceGetCreatePostForm($mixedContextId = false, $sDefault = '', $aCustom = array())
+    {
+    	if(!isLogged() || ($mixedContextId !== false && !is_numeric($mixedContextId)))
+            return '';
+
+        $oProfile = BxDolProfile::getInstance();
+        if(!$oProfile)
+            return '';
+
+        if($mixedContextId !== false)
+            $mixedContextId = (int)(!empty($mixedContextId) ? -$mixedContextId : $oProfile->id());
+
+    	$oMenu = BxDolMenu::getObjectInstance('sys_create_post');
+        if(!$oMenu)
+            return '';
+
+        $oMenu->setContextId($mixedContextId);
+    	$aMenuItems = $oMenu->getMenuItems();
+    	if(empty($aMenuItems) || !is_array($aMenuItems))
+            return '';
+
+    	if(empty($sDefault)) {
+            $aDefault = array_shift($aMenuItems);
+            $sDefault = !empty($aDefault['module']) ? $aDefault['module'] : $aDefault['name'];
+    	}
+    	$oMenu->setSelected($sDefault, $sDefault);
+
+        $bContext = $mixedContextId !== false;
+        if($bContext && ($aContextInfo = BxDolProfileQuery::getInstance()->getInfoById(abs($mixedContextId))))
+            if(bx_srv($aContextInfo['type'], 'check_allowed_post_in_profile', [$aContextInfo['content_id'], $sDefault]) !== CHECK_ACTION_RESULT_ALLOWED)
+                return '';
+
+        $sTitle = _t('_sys_page_block_title_create_post' . (!$bContext ? '_public' : ($mixedContextId < 0 ? '_context' : '')));
+        $sPlaceholder = _t('_sys_txt_create_post_placeholder', $oProfile->getDisplayName());
+
+        $oDbModules = BxDolModuleQuery::getInstance();
+    	$oTemplate = BxDolTemplate::getInstance();
+    	$oTemplate->addJs(['BxDolCreatePost.js']);
+
+        $aPreloadingList = [];
+        $aModules = explode(',', getParam('sys_create_post_form_preloading_list'));
+        if(!empty($aModules))
+            foreach($aModules as $sModule) {
+                $aModule = $oDbModules->getModuleByName($sModule);
+                if(empty($aModule) || !is_array($aModule))
+                    continue;
+
+                $aPreloadingList[$sModule] = $aModule['uri'];
+            }
+
+        if(bx_is_api()) {
+            return [
+                'title' => $sTitle,
+                'content' => '',
+                'menu' => $oMenu->getCodeAPI()
+            ];
+        }
+
+    	$sJsObject = 'oBxDolCreatePost';
+        $sJsContent = $oTemplate->_wrapInTagJsCode("var " . $sJsObject . " = new BxDolCreatePost(" . json_encode([
+            'sObjName' => $sJsObject,
+            'sRootUrl' => BX_DOL_URL_ROOT,
+            'sDefault' => $sDefault,
+            'iContextId' => $bContext ? $mixedContextId : 0,
+            'oPreloadingList' => $aPreloadingList,
+            'oCustom' => $aCustom
+        ]) . ");");
+
+    	return [
+            'title' => $sTitle,
+            'content' => $oTemplate->parseHtmlByName('create_post_form.html', [
+                'default' => $sDefault,
+                'placeholder' => $sPlaceholder,
+                'user_thumb' => $oProfile->getUnit(0, ['template' => 'unit_wo_info_links']),
+                'form' => bx_srv($sDefault, 'get_create_post_form', [[
+                    'context_id' => $mixedContextId, 
+                    'ajax_mode' => true, 
+                    'absolute_action_url' => true, 
+                    'custom' => $aCustom
+                ]]),
+                'js_object' => $sJsObject,
+                'js_content' => $sJsContent
+            ]),
+            'menu' => $oMenu
+        ];
+    }
+
+    public function serviceGetOptionsCreatePostFormPreloadingList()
+    {
+        $oMenu = BxDolMenu::getObjectInstance('sys_create_post');
+        if(!$oMenu)
+            return [];
+
+        $aMenuItems = $oMenu->getMenuItems();
+
+        $aResult = [];
+        foreach($aMenuItems as $aMenuItem) {
+            $sModule = !empty($aMenuItem['module']) ? $aMenuItem['module'] : $aMenuItem['name'];
+            if(!bx_srv('system', 'is_module_enabled', [$sModule]))
+                continue;
+
+            $aResult[] = [
+                'key' => $sModule, 
+                'value' => _t('_' . $sModule)
+            ];
+        }
+
+        return $aResult;
+    }
+
+    public function serviceGetBlockAuthor($sModule, $iContentId = 0)
+    {
+        if(!$iContentId && bx_get('id') !== false)
+            $iContentId = (int)bx_get('id');
+
+        $sMethodGetAuthor = 'get_author';
+        if(!$sModule || !$iContentId || !BxDolRequest::serviceExists($sModule, $sMethodGetAuthor))
+            return '';
+
+        $iAuthor = bx_srv($sModule, $sMethodGetAuthor, [$iContentId]);
+        if(!$iAuthor)
+            return '';
+
+        $oAuthor = BxDolProfile::getInstance($iAuthor);
+        if(!$oAuthor)
+            return '';
+
+        $sModuleAuthor = $oAuthor->getModule();
+        $sMethodGetCover = 'entity_cover';
+        if(!BxDolRequest::serviceExists($sModuleAuthor, $sMethodGetCover))
+            return '';
+
+        return bx_srv($sModuleAuthor, $sMethodGetCover, [$oAuthor->getContentId(), [
+            'use_as_author' => true,
+            'show_text' => false
+        ]]);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-general General
+     * @subsubsection bx_system_general-get_site_submenu get_site_submenu
+     * 
+     * @code bx_srv('system', 'get_site_submenu', [], 'TemplServices'); @endcode
+     * @code {{~system:get_site_submenu:TemplServices[]~}} @endcode
+     * 
+     * Get Site Submenu code.
+     * @see BxBaseServices::serviceGetSiteSubmenu
+     */
+    /** 
+     * @ref bx_system_general-get_site_submenu "get_site_submenu"
+     */
+    public function serviceGetSiteSubmenu()
+    {
+        $oMenu = BxDolMenu::getObjectInstance('sys_site_submenu');
+        return $oMenu ? $oMenu->getCode() : '';
+    }
+    
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-general General
+     * @subsubsection bx_system_general-get_products_names get_products_names
+     * 
+     * @code bx_srv('system', 'get_products_names', [], 'TemplServices'); 
+     *
+     * @code http://example.com/m/oauth2/com/get_products_names?module=system&class=BaseServices @endcode
+     * @code http://hihi.una.io/modules/?r=oauth2/com/get_products_names&module=system&class=BaseServices @endcode
+     * 
+     * Get an array of products names from all modules with payments functionality.
+     * @param $iVendorId filter products by vendor ID.
+     * @param $iLimit limit number of records from one module.
+     * @return array of products where key id product name and value is module name
+     * 
+     * @see BxBaseServices::serviceGetProductsNames
+     */
+    /** 
+     * @ref bx_system_general-get_products_names "get_products_names"
+     */
+    public function serviceGetProductsNames($iVendorId = 0, $iLimit = 1000)
+    {
+        $o = BxDolPayments::getInstance();
+        if (!$o)
+            return array();
+        return $o->getProductsNames($iVendorId, $iLimit);
+    }
+
+    /**
+     * @see iBxDolProfileService::serviceCheckSpacePrivacy
+     */ 
+    public function serviceCheckSpacePrivacy($iContentId)
+    {
+        return _t('_Access denied');
+    }
+    
+    public function serviceFormsHelper ()
+    {
+        return new BxTemplAccountForms();
+    }
+
+    public function serviceActAsProfile ()
+    {
+        return false;
+    }
+
+    public function servicePrepareFields ($aFieldsProfile)
+    {
+        return $aFieldsProfile;
+    }
+
+    public function serviceProfilesSearch ($sTerm, $iLimit)
+    {
+        $oDb = BxDolAccountQuery::getInstance();
+        $aRet = array();
+        $a = $oDb->searchByTerm($sTerm, $iLimit);
+        foreach ($a as $r)
+            $aRet[] = array ('label' => $this->serviceProfileName($r['content_id']), 'value' => $r['profile_id']);
+        return $aRet;
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-general General
+     * @subsubsection bx_system_general-keyword_search keyword_search
+     * 
+     * @code bx_srv('system', 'keyword_search', ["bx_posts", ["keyword" => "test"}], 'TemplServices'); @endcode
+     * 
+     * @code {{~system:keyword_search:TemplServices["bx_posts", {"keyword":"test"}]~}} @endcode
+     * @code {{~system:keyword_search:TemplServices["bx_albums", {"meta_type": "location_country", "keyword": "AU"}, "unit.html"]~}} @endcode
+     * @code {{~system:keyword_search:TemplServices["bx_albums", {"meta_type": "location_country_state", "state":"NSW", "keyword": "AU"}, "unit.html"]~}} @endcode
+     * @code {{~system:keyword_search:TemplServices["bx_albums", {"meta_type": "location_country_city", "state":"NSW", "city":"Manly", "keyword": "AU"}, "unit.html"]~}} @endcode
+     * @code {{~system:keyword_search:TemplServices["bx_posts", {"meta_type": "mention", "keyword": 2}, "unit_gallery.html"]~}} @endcode
+     * @code {{~system:keyword_search:TemplServices["bx_posts", {"cat": "bx_posts_cats", "keyword": 3}, "unit_gallery.html"]~}} @endcode
+     * 
+     * Search by keyword
+     * @param $sSection - search object to search in, usually module name, for example: bx_posts
+     * @param $aCondition - condition for search, supported conditions: 
+     *          - search by keyword: ["keyword" => "test"]
+     *          - search by country: ["meta_type" => "location_country", "keyword" => "AU"]
+     *          - search by country and state: ["meta_type": "location_country_state", "state":"NSW", "keyword": "AU"]
+     *          - search by country, state and city: ["meta_type": "location_country_city", "state":"NSW", "city":"Manly", "keyword": "AU"]
+     *          - search for mentions: ["meta_type" => "mention", "keyword" => 2]
+     *          - search in category: ["cat": "bx_posts_cats", "keyword": 3]
+     * @param $sTemplate - template for displaying search results, for example: unit.html
+     * @param $iStart - paginate, display records starting from this number
+     * @param $iPerPage - paginate, display this number of records per page
+     * @param $bLiveSearch - search results like in live search
+     * 
+     * @see BxBaseServices::serviceKeywordSearch
+     */
+    /** 
+     * @ref bx_system_general-keyword_search "keyword_search"
+     */
+    public function serviceKeywordSearch ($sSection, $aCondition, $sTemplate = '', $iStart = 0, $iPerPage = 0, $bLiveSearch = 0, $bPaginate = false)
+    {
+        if (!$sSection || !isset($aCondition['keyword']))
+            return '';
+
+        $sClass = 'BxTemplSearch';
+
+        $sElsName = 'bx_elasticsearch';
+        $sElsMethod = 'is_configured';
+        if(BxDolRequest::serviceExists($sElsName, $sElsMethod) && BxDolService::call($sElsName, $sElsMethod)) {
+             $oModule = BxDolModule::getInstance($sElsName);
+
+             bx_import('Search', $oModule->_aModule);
+             $sClass = 'BxElsSearch';
+        }
+
+        $oSearch = new $sClass(array($sSection));
+        $oSearch->setLiveSearch($bLiveSearch);
+        $oSearch->setMetaType(isset($aCondition['meta_type']) ? $aCondition['meta_type'] : '');
+        $oSearch->setCategoryObject(isset($aCondition['cat']) ? $aCondition['cat'] : '');
+        $oSearch->setCustomSearchCondition($aCondition);
+        if (!$bPaginate)
+            $oSearch->setRawProcessing(true);
+        $oSearch->setCustomCurrentCondition(array(
+            'paginate' => array (
+                'start' => $iStart,
+                'perPage' => $iPerPage ? $iPerPage : BX_DOL_SEARCH_RESULTS_PER_PAGE_DEFAULT,
+            )));
+        if ($sTemplate)
+            $oSearch->setUnitTemplate($sTemplate);
+        
+        return $oSearch->response();
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-general General
+     * @subsubsection bx_system_general-keyword_search keyword_search
+     * 
+     * @code bx_srv('system', 'search_keyword_form', 'TemplServices'); @endcode
+     * 
+     * Block with Search by Keywords Form
+     *  
+     * @see BxBaseServices::serviceSearchKeywordForm
+     */
+    /** 
+     * @ref bx_system_general-keyword_search "keyword_search"
+     */
+    public function serviceSearchKeywordForm ()
+    {
+        return $this->_getSearchObject()->getForm(BX_DB_PADDING_DEF, false, true);
+    }
+    
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-general General
+     * @subsubsection bx_system_general-keyword_search keyword_search
+     * 
+     * @code bx_srv('system', 'search_keyword_result', 'TemplServices'); @endcode
+     * 
+     * Block with Search by Keywords Results
+     *  
+     * @see BxBaseServices::serviceSearchKeywordResult
+     */
+    /** 
+     * @ref bx_system_general-keyword_search "keyword_search"
+     */
+    public function serviceSearchKeywordResult ()
+    {
+        $sKeyword = bx_process_input(bx_get('keyword'));
+        $bKeyword = $sKeyword !== false;
+
+        if(bx_is_api())
+            return $this->serviceGetDataSearchApi(['params' => 
+                [
+                    'keyword' => $sKeyword,
+                    'section' => bx_process_input(bx_get('section')),
+                    'cat' => bx_process_input(bx_get('cat'))
+                ]
+            ]);
+
+        $sCode = '';
+        if($bKeyword) {
+            $oSearch = $this->_getSearchObject();
+
+            $sCode = $oSearch->response();
+            if(!$sCode)
+                $sCode = $oSearch->getEmptyResult();
+        }
+
+        return $sCode;
+    }
+
+    /** 
+     * @ref bx_system_general-get_date_search_api "get_date_search_api"
+     * @api @ref bx_system_general-get_date_search_api "get_date_search_api"
+     */
+    public function serviceGetDataSearchApi ($aParams)
+    {
+        if(!bx_is_api())
+            return false;
+
+        if(is_string($aParams))
+            $aParams = json_decode($aParams, true);
+
+        $aSections = [];
+        $aSectionsAll = BxDolDb::getInstance()->fromCache(
+            'sys_global_search_pairs', 
+            'getPairs', 
+            'SELECT `ObjectName` AS `name`, `Title` AS `title` FROM `sys_objects_search` WHERE `GlobalSearch`=\'1\' ORDER BY `Order` ASC',
+            'name', 'title'
+        );
+
+        $aSectionNames = explode(',', getParam('sys_api_search_sections'));
+        foreach($aSectionNames as $sSectionName) {
+            if(!isset($aSectionsAll[$sSectionName]))
+                continue;
+
+            $aSections[$sSectionName] = [
+                'name' => $sSectionName,
+                'title' => _t($aSectionsAll[$sSectionName])
+            ];
+        }
+
+        $aParamsBrowse = array_merge([
+            'keyword' => '',
+            'section' => '',
+            'sections' => array_values($aSections),
+            'start' => 0,
+            'per_page' => 12
+        ], !empty($aParams['params']) && is_array($aParams['params']) ? $aParams['params'] : []);
+
+        if(empty($aParamsBrowse['section']))
+            $aParamsBrowse['section'] = array_keys($aSections);
+        else if(is_string($aParamsBrowse['section']))
+            $aParamsBrowse['section'] = explode(',', $aParamsBrowse['section']);
+
+        $sClass = 'BxTemplSearch';
+        $oSearch = new $sClass($aParamsBrowse['section']);
+        $oSearch->setLiveSearch(true);
+        $oSearch->setDataProcessing(true);
+        $oSearch->setCustomSearchCondition(['keyword' => $aParamsBrowse['keyword']]);
+        $oSearch->setCustomCurrentCondition([
+            'paginate' => [
+                'forceStart' => $aParamsBrowse['start'],
+                'perPage' => $aParamsBrowse['per_page'],
+            ]
+        ]);
+
+        $aData = $oSearch->response();
+        if(count($aData) > $aParamsBrowse['per_page'])
+            $aData = array_slice($aData, $aParamsBrowse['start'], $aParamsBrowse['per_page']);
+
+        return [
+            bx_api_get_block('browse', [
+                'unit' => 'search-results',  
+                'request_url' => '/api.php?r=system/get_data_search_api/TemplServices&params[]=',
+                'params' => $aParamsBrowse,
+                'data' => $aData
+            ])
+        ];
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-general General
+     * @subsubsection bx_system_general-cmts cmts
+     * 
+     * @code bx_srv('system', 'cmts', ["sys_blocks", 1], 'TemplServices'); @endcode
+     * 
+     * @code {{~system:cmts:TemplServices["sys_blocks", 1]~}} @endcode
+     * 
+     * Comments block
+     * @param $sObject - comments object name
+     * @param $sId - content id assiciated tith the comments
+     * 
+     * @see BxBaseServices::serviceCmts
+     */
+    /** 
+     * @ref bx_system_general-cmts "cmts"
+     */
+    public function serviceCmts ($sObject, $sId)
+    {
+        $o = BxDolCmts::getObjectInstance($sObject, $sId);
+        if (!$o || !$o->isEnabled())
+            return '';
+        return $o->getCommentsBlock(array(), array('in_designbox' => false, 'show_empty' => true));
+    }
+
+    public function _serviceProfileFunc ($sFunc, $iContentId, $aParams = array())
+    {
+        if (!$iContentId)
+            return false;
+        if (!($oAccount = BxDolAccount::getInstance($iContentId)))
+            return false;
+
+        return $oAccount->$sFunc(false, $aParams);
+    }
+
+    public function serviceAlertResponseProcessInstalled()
+    {
+        BxDolTranscoderImage::registerHandlersSystem();
+    }
+
+    public function serviceAlertResponseProcessStorageChange ($oAlert)
+    {
+        if ('sys_storage_default' != $oAlert->aExtras['option'])
+            return;
+
+        $aStorages = BxDolStorageQuery::getStorageObjects();
+        foreach ($aStorages as $r) {
+            if (0 == $r['current_size'] && 0 == $r['current_number'] && ($oStorage = BxDolStorage::getObjectInstance($r['object'])))
+                $oStorage->changeStorageEngine($oAlert->aExtras['value']);
+        }
+
+    }
+
+    public function serviceGetOptionsProfileBot()
+    {
+        $aResult = array(
+            array('key' => '', 'value' => _t('_Select_one'))
+        );
+
+        $aAccountsIds = BxDolAccountQuery::getInstance()->getOperators();
+        foreach($aAccountsIds as $iAccountId) {
+            $aProfilesIds = BxDolAccount::getInstance($iAccountId)->getProfilesIds(true, false);
+            foreach($aProfilesIds as $iProfileId) {
+                $oProfile = BxDolProfile::getInstance($iProfileId);
+                $aResult[] = array(
+                    'key' => $iProfileId,
+                    'value' => _t('_sys_profile_with_type', $oProfile->getDisplayName(), $oProfile->getModule()),
+                );
+            }
+        }
+
+        return $aResult;
+    }
+    
+    public function serviceGetOptionsModuleListForPrivacySelector()
+    {
+        $aResult = array();
+        $BxDolModuleQuery = BxDolModuleQuery::getInstance();
+        $aModules = $BxDolModuleQuery->getModulesBy(array('type' => 'modules', 'active' => 1));
+        foreach($aModules as $aModule){
+            $oModule = BxDolModule::getInstance($aModule['name']);
+            if($oModule instanceof iBxDolContentInfoService){
+                if (!BxDolRequest::serviceExists($aModule['name'], 'act_as_profile'))
+                    continue;
+                $aResult[$aModule['name']] = $aModule['title'];
+            }
+        }
+        return $aResult;
+    }
+    
+
+    public function serviceGetOptionsCaptchaDefault()
+    {
+        $aResults = [];
+        $aObjects = BxDolCaptchaQuery::getObjects();
+        foreach($aObjects as $aObject)
+            $aResults[$aObject['object']] = $aObject['title'];
+
+        return $aResults;
+    }
+
+    public function serviceGetOptionsEmbedDefault()
+    {
+        $aResults = array(
+            '' => _t('_None')
+        );
+
+        $aObjects = BxDolEmbedQuery::getObjects();
+        foreach($aObjects as $aObject)
+            $aResults[$aObject['object']] = $aObject['title'];
+
+        return $aResults;
+    }
+
+    public function serviceGetOptionsLocationFieldDefault()
+    {
+        $aResults = array();
+
+        $aObjects = BxDolLocationFieldQuery::getObjects();
+        foreach($aObjects as $aObject)
+            $aResults[$aObject['object']] = _t($aObject['title']);
+
+        return $aResults;
+    }
+
+    public function serviceGetOptionsLocationMapDefault()
+    {
+        $aResults = array();
+
+        $aObjects = BxDolLocationMapQuery::getObjects();
+        foreach($aObjects as $aObject)
+            $aResults[$aObject['object']] = _t($aObject['title']);
+
+        return $aResults;
+    }
+
+    public function serviceGetOptionsSocketsFieldDefault()
+    {
+        $aResults = [];
+        $aObjects = ['sys_sockets_disabled', 'sys_sockets_soketi'];
+        foreach($aObjects as $sObject)
+            $aResults[$sObject] = _t('_adm_stg_cpt_option_sys_sockets_type_' . $sObject);
+        return $aResults;
+    }
+
+    public function serviceGetOptionsLocationLeafletGetProviders()
+    {
+        return BxDolLocationMapLeaflet::getProviders();
+    }
+
+    public function serviceGetOptionsRelations()
+    {
+        $aModules = BxDolModuleQuery::getInstance()->getModulesBy(array('type' => 'modules', 'active' => 1));
+
+        $aProfiles = array();
+        foreach($aModules as $aModule) {
+            $sMethod = 'act_as_profile';
+            if(!BxDolRequest::serviceExists($aModule['name'], $sMethod) || !BxDolService::call($aModule['name'], $sMethod))
+                continue;
+
+            $aProfiles[$aModule['name']] = _t('_' . $aModule['name']);
+        }
+
+        $aResults = array();
+        foreach($aProfiles as $sName1 => $sTitle1)
+            foreach($aProfiles as $sName2 => $sTitle2)
+                $aResults[$sName1 . '_' . $sName2] = $sTitle1 . ' - ' . $sTitle2;
+
+        return $aResults;
+    }
+
+    public function serviceGetOptionsCfProhibited()
+    {
+        return BxDolFormQuery::getDataItems('sys_content_filter');
+    }
+
+    public function serviceGetOptionsCfUnauthenticated()
+    {
+        return BxDolFormQuery::getDataItems('sys_content_filter');
+    }
+
+    public function serviceGetOptionsTaiwindDefault()
+    {
+        $sPath = BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'tailwind/css';
+        $aExcludes = ['.', '..', 'tailwind.css'];
+
+        $aResults = [];
+        if(($oHandle = opendir($sPath)) !== false) {
+            while(($sFile = readdir($oHandle)) !== false) {
+                if(in_array($sFile, $aExcludes))
+                    continue;
+
+                if(($oHandlerFile = fopen($sPath . '/' . $sFile, 'r')) === false)
+                    continue;
+
+                if(($sContent = fread($oHandlerFile, 1024)) !== false && strpos($sContent, '@tailwind base') === false)
+                    $aResults[$sFile] = $sFile;
+
+                fclose($oHandlerFile);
+            }
+
+            closedir($oHandle);
+        }
+
+        return $aResults;
+    }
+
+    public function serviceGetOptionsApiMenuTop()
+    {
+        $aMenus = BxDolMenuQuery::getMenuObjects();
+
+        $aResults = ['' => _t('_sys_please_select')];
+        foreach($aMenus as $aMenu)
+            if($aMenu['module'] == 'system')
+                $aResults[$aMenu['object']] = _t($aMenu['title']);
+
+        return $aResults;
+    }
+
+    public function serviceRedirect($sUrl = false)
+    {
+        if (!getParam('sys_confirmation_before_redirect')) {
+            BxDolTemplate::getInstance()->displayPageNotFound();
+            exit;
+        }
+
+        if (false === $sUrl)
+            $sUrl = bx_get('url');
+
+        if (!$sUrl || !preg_match('@^https?://@', $sUrl)) {
+            http_response_code(500);
+            return MsgBox(_t('_error occured'));
+        }
+        else {
+
+            $bSpam = null;
+            bx_alert('system', 'check_spam_url', 0, getLoggedId(), array('is_spam' => &$bSpam, 'content' => &$sUrl, 'where' => 'redirect'));
+
+            $sLangKey = $bSpam ? '_sys_redirect_confirmation_harmful' : '_sys_redirect_confirmation';
+            return BxDolTemplate::getInstance()->parseHtmlByName('redirect.html', [
+                'text' => _t($sLangKey, bx_process_output($sUrl), getParam('site_title')),
+                'url' => bx_js_string($sUrl, BX_ESCAPE_STR_APOS),
+            ]);
+        }
+    }
+
+    public function serviceGetBadge($aBadge, $bIsCompact = false)
+    {
+        $sClass = '';
+		$sStyleFont = '';
+		$sStyleBg = '';
+		if ($aBadge['color'] == '')
+			$aBadge['color'] = 'purple-600';
+		
+        if ($bIsCompact && $aBadge['icon'] != ''){
+            $aBadge['is_icon_only'] = 1;
+        }
+        if ($aBadge['is_icon_only'] == 1){
+            $sClass .= ' bx-badge-compact';
+            if (substr_count($aBadge['color'], 'rgb('))
+                $sStyleFont = 'color: ' . $aBadge['color'];
+            else
+                $sClass .= ' text-' . $aBadge['color'];
+        }
+		else{
+            if (substr_count($aBadge['color'], 'rgb('))
+                $sStyleBg = 'background-color: ' . $aBadge['color'];
+            else
+                $sClass .= ' bg-' . $aBadge['color'];
+		}
+        
+        return BxDolTemplate::getInstance()->parseHtmlByName('badge.html', array(
+            'bx_if:content' => array(
+                'condition' => $aBadge['is_icon_only'] != '1',
+                'content' => array('content' => _t($aBadge['text'])),
+            ),
+            'bx_if:icon' => array(
+                'condition' => $aBadge['icon'] != '',
+                'content' => array('content' => BxDolTemplate::getInstance()->getIcon($aBadge['icon'], array('class' => 'bx-badge-icon sys-colored', 'style' => $sStyleFont))),
+            ),
+            'title' => $aBadge['text'],
+			'style_font' => $sStyleFont,
+			'style_bg' => $sStyleBg,
+            'class' => $sClass,
+            )
+    	);
+    }
+    
+    public function serviceGetFooter()
+    {
+        return BxDolTemplate::getInstance()->parseHtmlByName('footer.html', []);
+    }
+
+    private function _getSearchObject()
+    {
+        $sClass = 'BxTemplSearch';
+        $sElsName = 'bx_elasticsearch';
+        $sElsMethod = 'is_configured';
+        if(BxDolRequest::serviceExists($sElsName, $sElsMethod) && BxDolService::call($sElsName, $sElsMethod) && !bx_get('cat') && !bx_get('type')) {
+            $oModule = BxDolModule::getInstance($sElsName);
+            bx_import('Search', $oModule->_aModule);
+            $sClass = 'BxElsSearch';
+        }
+        bx_alert('system', 'search_keyword', 0, 0, array('class' => &$sClass));
+
+        $oSearch = new $sClass(bx_get('section'));
+        $oSearch->setLiveSearch(bx_get('live_search') ? 1 : 0);
+        $oSearch->setMetaType(bx_process_input(bx_get('type')));
+        $oSearch->setCategoryObject(bx_process_input(bx_get('cat')));
+
+        return $oSearch;
+    }
+}
+
+/** @} */

Некоторые файлы не были показаны из-за большого количества измененных файлов