Преглед изворни кода

Merge branch 'master' of https://github.com/unaio/una

AntonLV пре 3 година
родитељ
комит
f2d391c952
87 измењених фајлова са 42638 додато и 4 уклоњено
  1. 1 1
      inc/classes/BxDolDb.php
  2. 1 1
      inc/version.inc.php
  3. 1 1
      install/index.php
  4. 1 1
      install/sql/system.sql
  5. 14 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/check.php
  6. 95 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/api.php
  7. 913 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolAccount.php
  8. 55 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolApiQuery.php
  9. 2237 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolCmts.php
  10. 895 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolConnection.php
  11. 329 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolContentInfo.php
  12. 1346 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolDb.php
  13. 467 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolMenu.php
  14. 451 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolObject.php
  15. 643 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolProfile.php
  16. 180 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolPush.php
  17. 308 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolRelation.php
  18. 1140 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolSearch.php
  19. 216 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolSession.php
  20. 418 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolStorageQuery.php
  21. 3548 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolTemplate.php
  22. 305 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolVoteReactions.php
  23. 500 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/js/editor.quill.js
  24. 372 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/profiles.inc.php
  25. 2433 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/utils.inc.php
  26. 12 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/version.inc.php
  27. 36 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/logout.php
  28. 73 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/manifest.json.php
  29. 688 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/general/classes/BxBaseModGeneralFormsEntryHelper.php
  30. 263 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/general/classes/BxBaseModGeneralMenuSnippetMeta.php
  31. 3667 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/general/classes/BxBaseModGeneralModule.php
  32. 228 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/general/classes/BxBaseModGeneralSearchResult.php
  33. 783 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/general/classes/BxBaseModGeneralTemplate.php
  34. 40 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/groups/classes/BxBaseModGroupsConnectionFans.php
  35. 39 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/groups/classes/BxBaseModGroupsMenuViewMeta.php
  36. 1949 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/groups/classes/BxBaseModGroupsModule.php
  37. 31 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/groups/template/unit.html
  38. 260 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/profile/classes/BxBaseModProfileFormEntry.php
  39. 272 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/profile/classes/BxBaseModProfileMenuSnippetMeta.php
  40. 217 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/profile/classes/BxBaseModProfileMenuViewMeta.php
  41. 2138 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/profile/classes/BxBaseModProfileModule.php
  42. 287 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/profile/classes/BxBaseModProfileSearchResult.php
  43. 757 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/profile/classes/BxBaseModProfileTemplate.php
  44. 1099 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/text/classes/BxBaseModTextModule.php
  45. 279 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/text/classes/BxBaseModTextSearchResult.php
  46. 1102 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/text/classes/BxBaseModTextTemplate.php
  47. 263 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/text/js/links.js
  48. 27 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/text/template/unit_gallery.html
  49. 7 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/plugins/autoload.php
  50. 75 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/plugins/composer/autoload_real.php
  51. 224 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/plugins/composer/autoload_static.php
  52. 348 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/plugins/composer/installed.php
  53. 0 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/plugins_public/tailwind/css/tailwind.min.css
  54. 206 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/plugins_public/tailwind/js/tailwind.config.js
  55. 38 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/api.php
  56. 30 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/classes/BxDolStudioAPI.php
  57. 148 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/classes/BxDolStudioApiKeys.php
  58. 65 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/classes/BxDolStudioApiOrigins.php
  59. 50 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/classes/BxDolStudioJson.php
  60. 229 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/classes/BxDolStudioOAuthOAuth2.php
  61. 439 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/classes/BxDolStudioOptions.php
  62. 65 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/template/images/icons/wi-api.svg
  63. 93 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/template/scripts/BxBaseStudioAPI.php
  64. 19 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/template/scripts_templ/BxTemplStudioAPI.php
  65. 32 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/calendar.html
  66. 12 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/comments_uploader_nfw.html
  67. 177 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/css/uploaders.css
  68. 1596 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseCmts.php
  69. 589 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseCmtsServices.php
  70. 897 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseFunctions.php
  71. 398 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseMenu.php
  72. 155 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseMenuCustom.php
  73. 126 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseMenuProfileStats.php
  74. 221 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseMenuSubmenu.php
  75. 167 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseMenuTagsCloud.php
  76. 1059 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBasePage.php
  77. 349 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseSearchExtended.php
  78. 457 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseServiceConnections.php
  79. 345 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseServiceLogin.php
  80. 84 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseServicePages.php
  81. 666 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseServiceProfiles.php
  82. 1228 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseServices.php
  83. 368 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseVote.php
  84. 168 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseVoteServices.php
  85. 12 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/files_delete.php
  86. 4 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/script.php
  87. 113 0
      upgrade/files/13.0.0.RC3-13.0.0.RC4/sql.sql

+ 1 - 1
inc/classes/BxDolDb.php

@@ -689,7 +689,7 @@ class BxDolDb extends BxDolFactory implements iBxDolSingleton
         bx_log('sys_db', "$sErrorType\n" . 
             (empty($aError['message']) ? '' : "  Error: {$aError['message']}\n") . 
             (empty($aError['query']) ? '' : "  Query: {$aError['query']}\n") . 
-            (getLoggedId() ? '' : "  Account ID: " . getLoggedId() . "\n")
+            (!function_exists('getLoggedId') || !getLoggedId() ? '' : "  Account ID: " . getLoggedId() . "\n")
         );
 
         bx_show_service_unavailable_error_and_exit($sOutput);

+ 1 - 1
inc/version.inc.php

@@ -7,6 +7,6 @@
  * @{
  */
 
-define('BX_DOL_VERSION', '13.0.0-RC3'); ///< system files version, for DB version @see bx_get_ver, DB and files version must match
+define('BX_DOL_VERSION', '13.0.0-RC4'); ///< system files version, for DB version @see bx_get_ver, DB and files version must match
 
 /** @} */

+ 1 - 1
install/index.php

@@ -10,7 +10,7 @@
 define ('BX_DOL_INSTALL',  true);
 define ('BX_SKIP_INSTALL_CHECK', true);
 
-define ('BX_DOL_VER', '13.0.0-RC3');
+define ('BX_DOL_VER', '13.0.0-RC4');
 
 define ('BX_INSTALL_DEFAULT_LANGUAGE', 'en');
 

+ 1 - 1
install/sql/system.sql

@@ -6424,5 +6424,5 @@ INSERT INTO `sys_std_pages_widgets`(`page_id`, `widget_id`, `order`) VALUES(@iId
 -- API Page
 --
 INSERT INTO `sys_std_widgets`(`page_id`, `module`, `type`, `url`, `click`, `icon`, `caption`, `cnt_notices`, `cnt_actions`) VALUES
-(@iIdAPI, 'system', 'appearance', '{url_studio}api.php', '', 'wi-api.svg', '_adm_wgt_cpt_api', '', 'a:4:{s:6:"module";s:6:"system";s:6:"method";s:11:"get_actions";s:6:"params";a:0:{}s:5:"class";s:18:"TemplStudioModules";}');
+(@iIdAPI, 'system', 'configuration', '{url_studio}api.php', '', 'wi-api.svg', '_adm_wgt_cpt_api', '', 'a:4:{s:6:"module";s:6:"system";s:6:"method";s:11:"get_actions";s:6:"params";a:0:{}s:5:"class";s:18:"TemplStudioModules";}');
 INSERT INTO `sys_std_pages_widgets`(`page_id`, `widget_id`, `order`) VALUES(@iIdHome, LAST_INSERT_ID(), 14);

+ 14 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/check.php

@@ -0,0 +1,14 @@
+<?php
+
+$mixCheckResult = 'Update can not be applied';
+
+if (!(PHP_VERSION_ID >= 70400)) {
+    $mixCheckResult = 'This update requires a PHP version ">= 7.4.0". You are running ' . PHP_VERSION;
+}
+else {
+    $sVer = $this->oDb->getOne("SELECT `version` FROM `sys_modules` WHERE `name` = 'system'");
+    if ('13.0.0.RC3' == $sVer || '13.0.0-RC3' == $sVer)
+        $mixCheckResult = true;
+}
+
+return $mixCheckResult;

+ 95 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/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' : $sClass);
+    $bSafe = $b ? BxDolService::call($sModule, 'is_safe_service', array($sMethod), 'system' == $sModule ? 'TemplServices' : $sClass) : false;
+
+    $bPublic = false;
+    if (!$bSafe) {
+        $b = BxDolRequest::serviceExists($sModule, 'is_public_service', 'system' == $sModule ? 'TemplServices' : $sClass);
+        $bPublic = $b ? BxDolService::call($sModule, 'is_public_service', array($sMethod), 'system' == $sModule ? 'TemplServices' : $sClass) : 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,
+]);

+ 913 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolAccount.php

@@ -0,0 +1,913 @@
+<?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
+ * @{
+ */
+
+bx_import('BxDolAcl');
+
+class BxDolAccount extends BxDolFactory implements iBxDolSingleton
+{
+    protected $_iAccountID;
+    protected $_aInfo;
+    protected $_oQuery;
+
+    /**
+     * Constructor
+     */
+    protected function __construct ($iAccountId)
+    {
+        $iAccountId = (int)$iAccountId;
+        $sClass = get_class($this) . '_' . $iAccountId;
+        if (isset($GLOBALS['bxDolClasses'][$sClass]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+
+        $this->_iAccountID = $iAccountId; // since constructor is protected $iAccountId is always valid
+        $this->_oQuery = BxDolAccountQuery::getInstance();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        $sClass = get_class($this) . '_' . $this->_iProfileID;
+        if (isset($GLOBALS['bxDolClasses'][$sClass]))
+            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($mixedAccountId = false,  $bClearCache = false)
+    {
+        if (!$mixedAccountId)
+            $mixedAccountId = getLoggedId();
+
+        $iAccountId = self::getID($mixedAccountId);
+        if (!$iAccountId)
+            return false;
+
+        $sClass = __CLASS__ . '_' . $iAccountId;
+        
+        if ($bClearCache && isset($GLOBALS['bxDolClasses'][$sClass]))
+            unset($GLOBALS['bxDolClasses'][$sClass]);
+        
+        if(!isset($GLOBALS['bxDolClasses'][$sClass]))
+            $GLOBALS['bxDolClasses'][$sClass] = new BxDolAccount($iAccountId);
+
+        return $GLOBALS['bxDolClasses'][$sClass];
+    }
+
+    /**
+     * Get studio operator account singleton instance on the class
+     */
+    public static function getInstanceStudioOperator()
+    {
+        $oQuery = BxDolAccountQuery::getInstance();
+        if (!($iId = $oQuery->getStudioOperatorId()))
+            return false;
+
+        return self::getInstance($iId);
+    }
+
+    /**
+     * Get account id
+     */
+    public function id()
+    {
+        $a = $this->getInfo($this->_iAccountID);
+        return isset($a['id']) ? $a['id'] : false;
+    }
+
+    /**
+     * Check if account is confirmed, it is checked by email confirmation
+     */
+    public function isConfirmed($iAccountId = false)
+    {
+        $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+
+        $a = $this->getInfo($iId);
+
+        $bResult = false;
+        $sConfirmationType = getParam('sys_account_confirmation_type');
+        switch($sConfirmationType) {
+            case BX_ACCOUNT_CONFIRMATION_NONE:
+                $bResult = true;
+                break;
+
+            case BX_ACCOUNT_CONFIRMATION_EMAIL:
+                if($a['email_confirmed'])
+                    $bResult = true;
+                break;
+
+            case BX_ACCOUNT_CONFIRMATION_PHONE:
+                if($a['phone_confirmed'])
+                    $bResult = true;
+                break;
+
+            case BX_ACCOUNT_CONFIRMATION_EMAIL_PHONE:
+                if($a['email_confirmed'] && $a['phone_confirmed'])
+                    $bResult = true;
+                break;
+                
+            case BX_ACCOUNT_CONFIRMATION_EMAIL_OR_PHONE:
+                if($a['email_confirmed'] || $a['phone_confirmed'])
+                    $bResult = true;
+                break;  
+                
+        }
+
+        bx_alert('account', 'is_confirmed', $iId, false, array('type' => $sConfirmationType, 'override_result' => &$bResult));
+
+        return $bResult;
+    }
+    
+    public function getCurrentConfirmationStatusValue($iAccountId = false)
+    {
+        $a = $this->getInfo((int)$iAccountId);
+        $sTmp = $a['email_confirmed'] . $a['phone_confirmed'];
+        switch ($sTmp) {
+            case '01':
+                return BX_ACCOUNT_CONFIRMATION_PHONE;
+            case '10':
+                return BX_ACCOUNT_CONFIRMATION_EMAIL;
+            case '11':
+                return BX_ACCOUNT_CONFIRMATION_EMAIL_PHONE;
+        }
+        return BX_ACCOUNT_CONFIRMATION_NONE;
+    }
+
+    public function isConfirmedEmail($iAccountId = false)
+    {
+        $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+
+        $bResult = false;
+        if(self::isNeedConfirmEmail()) {
+            $a = $this->getInfo($iId);
+
+            $bResult = $a['email_confirmed'] ? true : false;
+        }
+        else 
+            $bResult = true;
+
+        bx_alert('account', 'is_confirmed_email', $iId, false, array('override_result' => &$bResult));
+
+        return $bResult;
+    }
+
+    public function isConfirmedPhone($iAccountId = false)
+    {
+        $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+
+        $bResult = false;        
+        if(self::isNeedConfirmPhone()) {
+            $a = $this->getInfo((int)$iAccountId);
+
+            $bResult = $a['phone_confirmed'] ? true : false;
+        }
+        else
+            $bResult = true;
+
+        bx_alert('account', 'is_confirmed_phone', $iId, false, array('override_result' => &$bResult));
+
+        return $bResult;
+    }
+
+    static public function isNeedConfirmEmail()
+    {
+        if(in_array(getParam('sys_account_confirmation_type'), array(BX_ACCOUNT_CONFIRMATION_EMAIL, BX_ACCOUNT_CONFIRMATION_EMAIL_PHONE, BX_ACCOUNT_CONFIRMATION_EMAIL_OR_PHONE)))
+            return true;
+        return false;
+    }
+    
+    static public function isNeedConfirmPhone()
+    {
+        if(in_array(getParam('sys_account_confirmation_type'), array(BX_ACCOUNT_CONFIRMATION_PHONE, BX_ACCOUNT_CONFIRMATION_EMAIL_PHONE, BX_ACCOUNT_CONFIRMATION_EMAIL_OR_PHONE)))
+            return true;
+        return false;
+    }
+    
+    public function isLocked($iAccountId = false)
+    {
+        $a = $this->getInfo((int)$iAccountId);
+        return $a['locked'] ? true : false;
+    }
+    
+    //
+
+    /**
+     * Set account email to confirmed or unconfirmed
+     * @param  int  $isConfirmed - false: mark email as unconfirmed, true: as confirmed
+     * @param  int  $iAccountId  - optional account id
+     * @return true on success or false on error
+     */
+    public function updateEmailConfirmed($isConfirmed, $isAutoSendConfrmationEmail = true, $iAccountId = false)
+    {
+        $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+
+        if (!$isConfirmed && $isAutoSendConfrmationEmail && self::isNeedConfirmEmail()) // if email_confirmation procedure is enabled - send email confirmation letter
+            $this->sendConfirmationEmail($iId);
+
+        if ($this->_oQuery->updateEmailConfirmed($isConfirmed, $iId)) {
+            $this->_aInfo = false;
+            bx_alert('account', $this->isConfirmed() ? 'confirm' : 'unconfirm', $iId);
+            return true;
+        }
+        return false;
+    }
+    
+    /**
+     * Set account phone to confirmed or unconfirmed
+     * @param  int  $isConfirmed - false: mark phone as unconfirmed, true: as confirmed
+     * @param  int  $iAccountId  - optional account id
+     * @return true on success or false on error
+     */
+    public function updatePhoneConfirmed($isConfirmed, $iAccountId = false)
+    {
+        $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        
+        if ($this->_oQuery->updatePhoneConfirmed($isConfirmed, $iId)) {
+            $this->_aInfo = false;
+            bx_alert('account', $this->isConfirmed() ? 'confirm' : 'unconfirm', $iId);
+            return true;
+        }
+        return false;
+    }
+    /**
+     * Change account password
+     * @param  string  $sPassword - new password
+     * @param  int  $iAccountId  - optional account id
+     * @return true on success or false on error
+     */
+    public function updatePassword($sPassword, $iAccountId = false)
+    {
+        $sSalt = genRndSalt();
+        $sPasswordHash = encryptUserPwd($sPassword, $sSalt);
+        $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        $oAccountSender = BxDolAccount::getInstance();
+
+        $this->_oQuery->logPassword($iId);
+        $iPasswordExpired = $this->getPasswordExpiredDateByAccount($iAccountId);
+        
+        if((int)$this->_oQuery->updatePassword($sPasswordHash, $sSalt, $iId, $iPasswordExpired) > 0) {
+            bx_alert('account', 'edited', $iId, $oAccountSender ? $oAccountSender->id() : $iId, array('action' => 'reset_password'));
+            $this->doAudit($iId, '_sys_audit_action_account_reset_password');
+            return true;
+        }
+        return false;
+    }
+    /**
+     * Set account phone
+     * @param  string  $sPhone - phone number
+     * @param  int  $iAccountId  - optional account id
+     * @return true on success or false on error
+     */
+    public function updatePhone($sPhone, $iAccountId = false)
+    {
+        $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        if ($this->_oQuery->updatePhone($sPhone, $iId)) {
+            bx_alert('account', 'set_phone', $iId);
+            return true;
+        }
+        return false;
+    }
+    /**
+     * Switch context automatically to the first available profile
+     * @param $iProfileIdFilterOut profile ID to exclude from the list of possible profiles
+     * @param $iAccountId account ID to use istead of current account
+     * @return true on success or false on error
+     */ 
+    public function updateProfileContextAuto($iProfileIdFilterOut = false, $iAccountId = false)
+    {
+        $oAccount = (!$iAccountId || $iAccountId == $this->_iAccountID ? $this : BxDolAccount::getInstance ($iAccountId));
+        if (!$oAccount)
+            return false;
+        $aAccountInfo = $oAccount->getInfo();
+        $aProfiles = $oAccount->getProfiles();
+        $oProfileAccount = BxDolProfile::getInstanceAccountProfile($oAccount->id());
+
+        // unset deleted profile and account profile
+        if ($iProfileIdFilterOut)
+            unset($aProfiles[$iProfileIdFilterOut]);
+        unset($aProfiles[$oProfileAccount->id()]);
+
+        if ($aProfiles) {
+            // try to use another profile
+            reset($aProfiles);
+            $iProfileId = key($aProfiles);
+        } 
+        else {
+            // if no profiles exist, use account profile
+            $iProfileId = $oProfileAccount->id();
+        }
+
+        return $oAccount->updateProfileContext($iProfileId);
+    }
+    
+    public function updateProfileContext($iSwitchToProfileId, $iAccountId = false)
+    {
+        $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        $aInfo = $this->getInfo((int)$iId);
+        if (!$aInfo)
+            return false;
+
+        $ret = null;
+        bx_alert('account', 'before_switch_context', $iId, $iSwitchToProfileId, array('profile_id_current' => $aInfo['profile_id'], 'override_result' => &$ret));
+        if ($ret !== null)
+            return $ret;
+
+        if (!$this->_oQuery->updateCurrentProfile($iId, $iSwitchToProfileId))
+            return false;
+
+        $this->_aInfo = false;
+            
+        bx_alert('account', 'switch_context', $iId, $iSwitchToProfileId, array('profile_id_old' => $aInfo['profile_id']));
+
+        return true;
+    }
+
+    /**
+     * Send "confirmation" email
+     */
+    public function sendConfirmationEmail($iAccountId = false)
+    {
+        $iAccountId = (int)$iAccountId;
+        if(!$iAccountId)
+            $iAccountId = $this->_iAccountID;
+
+        $aAccountInfo = $this->getInfo($iAccountId);
+        if(empty($aAccountInfo) || !is_array($aAccountInfo))
+            return false;
+
+        $oKey = BxDolKey::getInstance();
+        $sConfirmationCode = $oKey->getNewKey(array('account_id' => $iAccountId));
+        $sConfirmationLink = bx_append_url_params(BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=confirm-email'), array('code' => $sConfirmationCode));
+
+        $aReplaceVars = array(
+            'name' => $this->getDisplayName($iAccountId),
+            'email' => $aAccountInfo['email'],
+            'conf_code' => $sConfirmationCode,
+            'conf_link' => $sConfirmationLink,
+            'conf_form_link' => BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=confirm-email')
+        );
+
+        $bResult = sendMailTemplate('t_Confirmation', $iAccountId, (int)$aAccountInfo['profile_id'], $aReplaceVars, BX_EMAIL_SYSTEM);
+        if($bResult)
+            $this->doAudit($iAccountId, '_sys_audit_action_account_resend_confirmation_email');
+
+        return $bResult;
+    }
+    
+    public function sendResetPasswordEmail($iAccountId = false)
+    {
+        $iAccountId = (int)$iAccountId;
+        if(!$iAccountId)
+            $iAccountId = $this->_iAccountID;
+
+        $aAccountInfo = $this->getInfo($iAccountId);
+        if(empty($aAccountInfo) || !is_array($aAccountInfo))
+            return false;
+
+        $sKey = bx_get_reset_password_key($aAccountInfo['email']);
+
+        $aReplaceVars = array(
+            'name' => $this->getDisplayName($iAccountId),
+            'email' => $aAccountInfo['email'],
+            'key' => $sKey,
+            'forgot_password_url' => bx_get_reset_password_link_by_key($sKey)
+        );
+
+        return sendMailTemplate('t_Forgot', $iAccountId, (int)$aAccountInfo['profile_id'], $aReplaceVars, BX_EMAIL_SYSTEM);
+    }
+
+    /**
+     * Get account info
+     */
+    public function getInfo($iAccountId = false)
+    {        
+        if ($iAccountId && $iAccountId != $this->_iAccountID)
+            return $this->_oQuery->getInfoById((int)$iAccountId ? (int)$iAccountId : $this->_iAccountID);
+
+        if ($this->_aInfo)
+            return $this->_aInfo;
+
+        $this->_aInfo = $this->_oQuery->getInfoById($this->_iAccountID);
+        return $this->_aInfo;
+    }
+
+    /**
+     * Get account display name
+     */
+    public function getDisplayName($iAccountId = false)
+    {
+        $aInfo = $this->getInfo($iAccountId);
+
+        $sDisplayName = !empty($aInfo['name']) ? $aInfo['name'] : _t('_sys_txt_user_n', $aInfo['id']);
+        bx_alert('account', 'account_name', $iAccountId, 0, array('info' => $aInfo, 'display_name' => &$sDisplayName));
+
+        return bx_process_output($sDisplayName);
+    }
+
+    /**
+     * Get account url
+     */
+    public function getUrl($iAccountId = false)
+    {
+        return 'javascript:void(0);';
+    }
+
+    /**
+     * Get account url
+     */
+    public function getUnit($iAccountId = false, $aParams = array())
+    {
+        
+        $sTemplate = 'unit';
+        $sTemplateSize = false;
+        $aTemplateVars = array();
+        if(!empty($aParams['template'])) {
+            if(is_string($aParams['template']))
+                $sTemplate = $aParams['template'];
+            else if(is_array($aParams['template'])) {
+                if(!empty($aParams['template']['name']))
+                    $sTemplate = $aParams['template']['name'];
+
+                if(!empty($aParams['template']['size']))
+                    $sTemplateSize = $aParams['template']['size'];
+
+                if(!empty($aParams['template']['vars']))
+                    $aTemplateVars = $aParams['template']['vars'];
+            }
+        }
+        $sTemplate = 'account_' . $sTemplate . '.html';
+        if(empty($sTemplateSize))
+            $sTemplateSize = 'thumb';
+
+        $sTitle = $this->getDisplayName($iAccountId);
+
+        $aTmplVars = array(
+            'size' => $sTemplateSize,
+            'color' => implode(', ', BxDolTemplate::getColorCode(($iAccountId ? $iAccountId : $this->_iAccountID), 1.0)),
+            'letter' => mb_strtoupper(mb_substr($sTitle, 0, 1)),
+            'content_url' => $this->getUrl($iAccountId),
+            'title' => $sTitle,
+            'title_attr' => bx_html_attribute($sTitle),
+            'bx_if:show_online' => array(
+                'condition' => $this->isOnline($iAccountId),
+                'content' => array()
+            )
+        );
+
+        if(!empty($aTemplateVars) && is_array($aTemplateVars))
+            $aTmplVars = array_merge ($aTmplVars, $aTemplateVars);
+
+        return BxDolTemplate::getInstance()->parseHtmlByName($sTemplate, $aTmplVars);
+    }
+
+    /**
+     * Get picture url
+     */
+    public function getPicture($iAccountId = false)
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account.svg');
+    }
+
+    /**
+     * Get big (2x) avatar url
+     */
+    public function getAvatarBig($iAccountId = false)
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account.svg');
+    }
+
+    /**
+     * Get avatar url
+     */
+    public function getAvatar($iAccountId = false)
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account.svg');
+    }
+
+    /**
+     * Get thumb picture url
+     */
+    public function getThumb($iAccountId = false)
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account.svg');
+    }
+
+    /**
+     * Get icon picture url
+     */
+    public function getIcon($iAccountId = false)
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account.svg');
+    }
+
+    /**
+     * Get account email
+     */
+    public function getEmail($iAccountId = false)
+    {
+        $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        $aAccountInfo = $this->getInfo($iAccountId);
+        return $aAccountInfo['email'];
+    }
+
+    /**
+     * Get language ID
+     */
+    public function getLanguageId($iAccountId = false)
+    {
+        $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        $aAccountInfo = $this->getInfo($iAccountId);
+        return $aAccountInfo['lang_id'];
+    }
+
+    /**
+     * Is account online
+     */
+    public function isOnline($iAccountId = false)
+    {
+        $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        return $this->_oQuery->isOnline($iAccountId);
+    }
+
+    /**
+     * Validate account.
+     * @param $s - account identifier (id or email)
+     * @return account id or false if account was not found
+     */
+    static public function getID($s)
+    {
+        $oQuery = BxDolAccountQuery::getInstance();
+
+        bx_import('BxDolForm');
+        if (BxDolFormCheckerHelper::checkEmail($s)) {
+            $iId = (int)$oQuery->getIdByEmail($s);
+            return $iId ? $iId : false;
+        }
+        if (preg_match("/^\+[0-9\s]+$/", $s)) {
+            $iId = (int)$oQuery->getIdByPhone($s);
+            return $iId ? $iId : false;
+        }
+        
+        $iId = $oQuery->getIdById((int)$s);
+        return $iId ? $iId : false;
+    }
+
+    /**
+     * Check if profiles limit reached
+     */
+    public function isProfilesLimitReached ()
+    {
+        $iProfilesLimit = (int)getParam('sys_account_limit_profiles_number');
+        bx_alert('account', 'get_limit_profiles_number', 0, 0, array('account_id' => $this->_iAccountID, 'number' => &$iProfilesLimit));
+        if (!isAdmin() && $iProfilesLimit && ($iProfilesNum = $this->getProfilesNumber()) && $iProfilesNum >= $iProfilesLimit)
+            return true;
+
+        return false;
+    }
+
+    /**
+     * Get number of profiles associated with the account
+     */
+    public function getProfilesNumber ($isFilterNonSwitchableProfiles = true)
+    {
+        $a = $this->getProfilesIds($isFilterNonSwitchableProfiles);
+        return count($a);
+    }
+    
+    /**
+     * Get all profile ids associated with the account
+     */
+    public function getProfilesIds ($isFilterNonSwitchableProfiles = true, $isFilterSystemProfiles = true)
+    {
+        $a = $this->getProfiles ($isFilterNonSwitchableProfiles, $isFilterSystemProfiles);
+        return $a ? array_keys($a) : array();
+    }
+
+    /**
+     * Get all profiles associated with the account
+     */
+    public function getProfiles ($isFilterNonSwitchableProfiles = true, $isFilterSystemProfiles = true)
+    {
+        $oProfileQuery = BxDolProfileQuery::getInstance();
+        $aProfiles = $oProfileQuery->getProfilesByAccount($this->_iAccountID);
+
+        if ($isFilterNonSwitchableProfiles) {
+            foreach ($aProfiles as $iProfileId => $aProfile) {
+                if ('system' == $aProfile['type'])
+                    continue;
+                if (!BxDolService::call($aProfile['type'], 'act_as_profile'))
+                    unset($aProfiles[$iProfileId]);
+            }
+        }
+
+        if ($isFilterSystemProfiles) {
+            foreach ($aProfiles as $iProfileId => $aProfile) {
+                if ('system' == $aProfile['type'])
+                    unset($aProfiles[$iProfileId]);
+            }
+        }
+
+        return $aProfiles;
+    }
+
+    /**
+     * Delete profile.
+     * @param $bDeleteWithContent - delete associated profiles with all its contents
+     */
+    function delete($bDeleteWithContent = false)
+    {
+        $aAccountInfo = $this->_oQuery->getInfoById($this->_iAccountID);
+        if (!$aAccountInfo)
+            return false;
+
+        // create system event before deletion
+        $isStopDeletion = false;
+        bx_alert('account', 'before_delete', $this->_iAccountID, 0, array('delete_with_content' => $bDeleteWithContent, 'stop_deletion' => &$isStopDeletion));
+        if ($isStopDeletion)
+            return false;
+
+        $oAccountQuery = BxDolAccountQuery::getInstance();
+
+        $oProfileQuery = BxDolProfileQuery::getInstance();
+        $aProfiles = $oProfileQuery->getProfilesByAccount($this->_iAccountID);
+        foreach ($aProfiles as $iProfileId => $aRow) {
+            $oProfile = BxDolProfile::getInstance($iProfileId);
+            if (!$oProfile)
+                continue;
+            $oProfile->delete(false, $bDeleteWithContent, true);
+        }
+
+        // delete profile
+        if (!$oAccountQuery->delete($this->_iAccountID))
+            return false;
+
+        // unset class instance to prevent creating the instance again
+        $sClass = __CLASS__ . '_' . $this->_iAccountID;
+        unset($GLOBALS['bxDolClasses'][$sClass]);
+
+       // create system event
+        bx_alert('account', 'delete', $this->_iAccountID, 0, array ('delete_with_content' => $bDeleteWithContent));
+        
+        $this->doAudit($this->_iAccountID, $bDeleteWithContent ? '_sys_audit_action_account_deleted_with_content' : '_sys_audit_action_account_deleted');
+        
+        return true;
+    }
+
+    /**
+     * Add permament messages.
+     */
+    public function addInformerPermanentMessages ($oInformer)
+    {
+        if (!$this->isConfirmed()) {
+            if (!$this->isConfirmedEmail()) {
+                $sUrl = BxDolPermalinks::getInstance()->permalink('page.php?i=confirm-email') . '&resend=1';
+                $aAccountInfo = $this->getInfo();
+                $oInformer->add('sys-account-unconfirmed-email', _t('_sys_txt_account_unconfirmed_email', $sUrl, $aAccountInfo['email']), BX_INFORMER_ALERT);
+            }
+            if (!$this->isConfirmedPhone()) {
+                $sUrl = BxDolPermalinks::getInstance()->permalink('page.php?i=confirm-phone') . '';
+                $aAccountInfo = $this->getInfo();
+                $oInformer->add('sys-account-unconfirmed-phone', _t('_sys_txt_account_unconfirmed_phone', $sUrl), BX_INFORMER_ALERT);
+            }
+        }
+        $this->isNeedChangePassword();
+    }
+
+    /**
+     * Get unsubscribe link for the specified mesage type
+     */
+    public function getUnsubscribeLink($iEmailType, $iAccountId = false)
+    {
+        $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        $sUrl = '';
+        switch ($iEmailType) {
+            case BX_EMAIL_NOTIFY:
+                $sUrl = 'page.php?i=unsubscribe-notifications';
+                break;
+            case BX_EMAIL_MASS:
+                $sUrl = 'page.php?i=unsubscribe-news';
+                break;
+            default:
+                return '';
+        }
+        return BxDolPermalinks::getInstance()->permalink($sUrl) . '&id=' . $iAccountId . '&code=' . $this->getEmailHash();
+    }
+
+    public function getEmailHash($iAccountId = false)
+    {
+        $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        $a = $this->getInfo();
+        return md5($a['email'] . $a['salt'] . BX_DOL_SECRET);
+    }
+    
+    public function getPasswordExpiredDate($iPasswordExpiredForMembership, $iAccountId = false)
+    {
+        if ($iPasswordExpiredForMembership == 0)
+            return 0;
+        
+        $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        
+        $aAccountInfo = $this->_oQuery->getInfoById($iAccountId);
+                
+        $iLastPassChanged = $this->_oQuery->getLastPasswordChanged($iAccountId);
+        if ($iLastPassChanged == 0)
+            $iLastPassChanged = $aAccountInfo['added'];
+
+        return $iPasswordExpiredForMembership * 86400 + $iLastPassChanged;
+    }
+    
+    public function getPasswordExpiredDateByAccount($iAccountId = false)
+    {
+        $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        
+        $oACL = BxDolAcl::getInstance();
+
+        $aProfiles = BxDolAccount::getInstance($iAccountId)->getProfiles();
+        $iPasswordExpiredForMembership = 0;
+        foreach ($aProfiles as $aProfile) {
+            $aMembersipInfo = $oACL->getMemberMembershipInfo($aProfile['id']);
+            $Memberships = [];
+            BxDolAclQuery::getInstance()->getLevels(['type' => 'by_id', 'value' => $aMembersipInfo['id']], $aMembership);
+            if($aMembership['password_expired'] > 0){
+                if ($iPasswordExpiredForMembership > 0 && $aMembership['password_expired'] < $iExpired)
+                    $iPasswordExpiredForMembership = $aMembership['password_expired'];
+                if ($iPasswordExpiredForMembership == 0 )
+                    $iPasswordExpiredForMembership = $aMembership['password_expired'];
+            }
+        }
+
+        return $this->getPasswordExpiredDate($iPasswordExpiredForMembership, $iAccountId);  
+    }
+    
+    public function isNeedChangePassword($iAccountId = false)
+    {
+        $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        
+        $aAccountInfo = $this->getInfo();
+        list($sPageLink, $aPageParams) = bx_get_base_url_inline();
+        $bNeedRedirectToChangePassword = true;
+
+        if (isset($aPageParams['i']) && $aPageParams['i'] == 'account-settings-password')
+            $bNeedRedirectToChangePassword = false;
+
+        
+        bx_alert('account', 'is_need_to_change_password',  $iAccountId, false, array('override_result' => &$bNeedRedirectToChangePassword));
+        
+        if ($aAccountInfo['password_expired'] >0 && $aAccountInfo['password_expired'] < time() && $bNeedRedirectToChangePassword){
+            if (getParam('sys_account_accounts_force_password_change_after_expiration') == 'on'){
+                header('Location: ' . BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=account-settings-password'));
+                exit;
+            }
+            else{
+                $oInformer = BxDolInformer::getInstance(BxDolTemplate::getInstance());
+                $oInformer->add('sys-account-need-to-change-password', _t('_sys_txt_account_need_to_change_password', BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=account-settings-password')), BX_INFORMER_ALERT);
+            }
+        }
+    }
+    
+    public function doAudit($iAccountId, $sAction, $aData = array())
+    {
+        $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        bx_audit(
+            $iAccountId, 
+            'bx_accounts', 
+            $sAction,  
+            array('content_title' => $this->getEmail(), 'data' => $aData)
+        );
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    static public function isAllowedCreate ($iProfileId, $isPerformAction = false)
+    {
+        $aCheck = checkActionModule($iProfileId, 'create account', 'system', $isPerformAction);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return MsgBox($aCheck[CHECK_ACTION_MESSAGE]);
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+    
+    static public function isAllowedCreateMultiple ($iProfileId, $isPerformAction = false)
+    {
+        $iLimit = (int)getParam('sys_account_limit_profiles_number');
+
+        $bResult = false;
+        if(isAdmin() || $iLimit != 1)
+            $bResult = true;
+
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if($iLimit == 1 && $oProfile->getModule() == 'system')
+            $bResult = true;
+
+        if($oProfile && $oProfile->getAccountId() != getLoggedId())
+            $bResult = false;
+
+        bx_alert('profile', 'allow_create_another_profile', $iProfileId, 0, [
+            'override_result' => &$bResult
+        ]);
+
+        return $bResult;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    static public function isAllowedEdit ($iProfileId, $aContentInfo, $isPerformAction = false)
+    {
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if (!$oProfile)
+            return _t('_sys_txt_access_denied');
+
+        $aProfileInfo = $oProfile->getInfo();
+        if (!$aProfileInfo || getLoggedId() != $aProfileInfo['account_id'])
+            return _t('_sys_txt_access_denied');
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    static public function isAllowedDelete ($iProfileId, $aContentInfo, $isPerformAction = false)
+    {
+        $iAccountId = (int)BxDolProfile::getInstance($iProfileId)->getAccountId();
+        if(isAdmin($iAccountId) && $iAccountId == (int)$aContentInfo['id'])
+            return _t('_sys_txt_access_denied');
+
+        $aCheck = checkActionModule($iProfileId, 'delete account', 'system', $isPerformAction);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+    
+    static public function pruning ()
+    {
+        $iCount = 0;
+        $iTime = time() - getParam('sys_account_accounts_pruning_interval') * 86400;
+        $oAccountQuery = BxDolAccountQuery::getInstance();
+        $aAccounts = [];
+        $bSuspend = false;
+        
+        switch(getParam('sys_account_accounts_pruning')) {
+ 
+            case 'no_login_delete':
+                $aAccounts = $oAccountQuery->getAccountsForPruning('no_login', $iTime);   
+                break;
+            
+            case 'no_login_suspend':
+                $bSuspend = true;
+                $aAccounts = $oAccountQuery->getAccountsForPruning('no_login', $iTime);   
+                break;  
+                
+    		case 'no_confirm_delete':
+                $aAccounts = $oAccountQuery->getAccountsForPruning('no_confirm', $iTime);
+                foreach ($aAccounts as $k => $aAccount) {
+                    $oAccount = BxDolAccount::getInstance($aAccount['id']);
+                    if($oAccount->isConfirmed()){
+                        unset($aAccounts[$k]);
+                    }
+                }
+                break;
+                
+            case 'no_profile_delete':
+                $aAccounts = $oAccountQuery->getAccountsForPruning('no_profile', $iTime);
+                break;
+        }
+
+        if ($bSuspend){
+            foreach ($aAccounts as $k => $aAccount) {
+                $oAccount = BxDolAccount::getInstance($aAccount['id']);
+                $oProfile = BxDolProfile::getInstanceAccountProfile($aAccount['id']);
+                $oProfile->suspend(BX_PROFILE_ACTION_AUTO, 0 ,false);
+                $aProfiles = $oAccount->getProfiles();
+                foreach($aProfiles as $aProfile){
+                    BxDolProfile::getInstance($aProfile['id'])->suspend(BX_PROFILE_ACTION_AUTO, 0 ,false);
+                }
+                $iCount++;
+            }
+        }
+        else{
+            foreach ($aAccounts as $k => $aAccount) {
+                $oAccount = BxDolAccount::getInstance($aAccount['id']);
+                $oAccount->delete(false);
+                $iCount++;
+            }
+        }
+
+        return $iCount;
+    }
+
+}
+
+/** @} */

+ 55 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolApiQuery.php

@@ -0,0 +1,55 @@
+<?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
+ * @{
+ */
+
+/**
+ * Database queries for API interface
+ * @see BxDolApiQuery
+ */
+class BxDolApiQuery 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
+     */
+    static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolApiQuery();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    public function getOrigin ($sUrl)
+    {
+        return $this->getOne("SELECT `url` FROM `sys_api_origins` WHERE `url` = :url", ['url' => $sUrl]);
+    }
+
+    public function getKey ($sKey)
+    {
+        return $this->getOne("SELECT `key` FROM `sys_api_keys` WHERE `key` = :key", ['key' => $sKey]);
+    }
+}
+
+/** @} */

+ 2237 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolCmts.php

@@ -0,0 +1,2237 @@
+<?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
+ * @{
+ */
+
+bx_import('BxDolAcl');
+bx_import('BxDolPrivacy');
+
+define('BX_DOL_CMT_TYPE_COMMENT', 'comment');
+define('BX_DOL_CMT_TYPE_REVIEW', 'review');
+
+define('BX_CMT_OLD_VOTES', 365*86400); ///< comment votes older than this number of seconds will be deleted automatically
+
+define('BX_CMT_ACTION_POST', 'post');
+define('BX_CMT_ACTION_EDIT', 'edit');
+
+define('BX_CMT_DISPLAY_FLAT', 'flat');
+define('BX_CMT_DISPLAY_THREADED', 'threaded');
+
+define('BX_CMT_BROWSE_HEAD', 'head');
+define('BX_CMT_BROWSE_TAIL', 'tail');
+define('BX_CMT_BROWSE_POPULAR', 'popular');
+define('BX_CMT_BROWSE_CONNECTION', 'connection');
+
+define('BX_CMT_ORDER_BY_DATE', 'date');
+define('BX_CMT_ORDER_BY_POPULAR', 'popular');
+
+define('BX_CMT_FILTER_ALL', 'all');
+define('BX_CMT_FILTER_OTHERS', 'others');
+define('BX_CMT_FILTER_FRIENDS', 'friends');
+define('BX_CMT_FILTER_SUBSCRIPTIONS', 'subscriptions');
+define('BX_CMT_FILTER_PINNED', 'pinned');
+
+define('BX_CMT_ORDER_WAY_ASC', 'asc');
+define('BX_CMT_ORDER_WAY_DESC', 'desc');
+
+define('BX_CMT_PFP_TOP', 'top');
+define('BX_CMT_PFP_BOTTOM', 'bottom');
+
+define('BX_CMT_RATE_VALUE_PLUS', 1);
+define('BX_CMT_RATE_VALUE_MINUS', -1);
+
+define('BX_CMT_USAGE_BLOCK', 'block');
+define('BX_CMT_USAGE_INLINE', 'inline');
+define('BX_CMT_USAGE_DEFAULT', BX_CMT_USAGE_BLOCK);
+
+define('BX_CMT_STATUS_ACTIVE', 'active');
+define('BX_CMT_STATUS_HIDDEN', 'hidden');
+define('BX_CMT_STATUS_PENDING', 'pending');
+
+
+/**
+ * Comments for any content
+ *
+ * Related classes:
+ * - BxDolCmtsQuery - comments database queries
+ * - BxBaseCmts - comments base representation
+ * - BxTemplCmts - custom template representation
+ *
+ * AJAX comments for any content.
+ * Self moderated - users rate all comments, and if comment is
+ * below viewing treshold it is hidden by default.
+ *
+ * To add comments section to your module you need to add a record to 'sys_objects_cmts' table:
+ *
+ * - ID - autoincremented id for internal usage
+ * - ObjectName - your unique module name, with vendor prefix, lowercase and spaces are underscored
+ * - TableCmts - table name where comments are stored
+ * - Html - 0 - convert new lines to <br /> on saving, 1 - standard(default) visual editor, 2 - full visual editor, 3 - mini visual editor.
+ * - PerView - number of comments on a page
+ * - IsRatable - 0 or 1 allow to rate comments or not
+ * - ViewingThreshold - comment viewing treshost, if comment is below this number it is hidden by default
+ * - IsOn - is this comment object enabled
+ * - RootStylePrefix - toot comments style prefix, if you need root comments look different\
+ * - ObjectVote - Vote object name to process comments' votes. May be empty if Comment Vote is not needed.
+ * - TriggerTable - table to be updated upon each comment
+ * - TriggerFieldId - TriggerTable table field with unique record id of
+ * - TriggerFieldComments - TriggerTable table field with comments count, it will be updated automatically upon eaech comment
+ * - ClassName - your custom class name if you need to override default class, this class must have the same constructor arguments
+ * - ClassFile - file where your ClassName is stored.
+ *
+ * You can refer to BoonEx modules for sample record in this table.
+ *
+ *
+ *
+ * @section example Example of usage:
+ * After filling in the table you can show comments section in any place, using the following code:
+ *
+ * @code
+ * $o = new BxTemplCmts('value of ObjectName field', $iYourEntryId);
+ * if ($o->isEnabled())
+ *     echo $o->getCommentsBlock ();
+ * @endcode
+ *
+ * Please note that you never need to use BxDolCmts class directly, use BxTemplCmts instead.
+ * Also if you override comments class with your own then make it child of BxTemplCmts class.
+ *
+ *
+ *
+ * @section acl Memberships/ACL:
+ * - comments post
+ * - comments edit own
+ * - comments remove own
+ * - comments edit all
+ *
+ *
+ *
+ * @section alerts Alerts:
+ * Alerts type/unit - every module has own type/unit, it equals to ObjectName.
+ *
+ * The following alerts are rised
+ *
+ * - commentPost - comment was posted
+ *      - $iObjectId - entry id
+ *      - $iSenderId - author of comment
+ *      - $aExtra['comment_id'] - just added comment id
+ *
+ * - commentRemoved - comments was removed
+ *      - $iObjectId - entry id
+ *      - $iSenderId - comment deleter id
+ *      - $aExtra['comment_id'] - removed comment id
+ *
+ * - commentUpdated - comments was updated
+ *      - $iObjectId - entry id
+ *      - $iSenderId - comment deleter id
+ *      - $aExtra['comment_id'] - updated comment id
+ *
+ * - commentRated - comments was rated
+ *      - $iObjectId - entry id
+ *      - $iSenderId - comment rater id
+ *      - $aExtra['comment_id'] - rated comment id
+ *      - $aExtra['rate'] - comment rate 1 or -1
+ *
+ */
+class BxDolCmts extends BxDolFactory implements iBxDolReplaceable, iBxDolContentInfoService
+{
+    /**
+     * System tables which are UNITED for all 
+     * comment systems and cannot be overwritten.
+     */
+    public static $sTableSystems = 'sys_objects_cmts';
+    public static $sTableIds = 'sys_cmts_ids';
+
+    /**
+     * System tables which are used by default and
+     * can be overwritten in comment systems.
+     * @see BxDolCmts::getTableNameImages and BxDolCmts::getTableNameImages2Entries
+     */
+    protected $_sTableImages = 'sys_cmts_images';
+    protected $_sTableImages2Entries = 'sys_cmts_images2entries';
+
+    protected $_sType;
+    protected $_oQuery = null;
+    protected $_oTemplate = null;
+
+    protected $_bPostQuote;
+    protected $_bMinPostForm;
+    protected $_sFormObject;
+    protected $_sFormDisplayPost;
+    protected $_sFormDisplayEdit;
+
+    protected $_sConnObjFriends;
+    protected $_sConnObjSubscriptions;
+
+    protected $_sMenuObjManage;
+    protected $_sMenuObjActions;
+    protected $_sMenuObjCounters;
+    protected $_sMenuObjMeta;
+
+    protected $_sMetatagsObj;
+
+    protected $_sViewUrl = '';
+    protected $_sBaseUrl = '';
+    protected $_sListAnchor = '';
+    protected $_sItemAnchor = '';
+
+    protected $_sSystem = 'profile'; ///< current comment system name
+    protected $_aSystem = array (); ///< current comments system array
+    protected $_iId = 0; ///< obect id to be commented
+    protected $_iAuthorId = 0; ///< currently logged in user who browse, post, etc.
+
+    protected $_aT = array (); ///< an array of lang keys
+    protected $_aMarkers = array ();
+
+    protected $_sDisplayType = '';
+    protected $_sDpSessionKey = '';
+    protected $_iDpMaxLevel = 0;
+
+    protected $_sBrowseType = '';
+    protected $_bBrowseFilter = false;
+    protected $_sBrowseFilter = '';
+    protected $_sBpSessionKeyType = '';
+    protected $_sBpSessionKeyFilter = '';
+    protected $_aOrder = array();
+
+    protected $_sSnippetLenthLiveSearch = 50;
+
+    protected $_iRememberTime = 2592000;
+
+    /**
+     * Constructor
+     * $sSystem - comments system name
+     * $iId - obect id to be commented
+     */
+    protected function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
+    {
+        parent::__construct();
+
+        $this->_sType = BX_DOL_CMT_TYPE_COMMENT;
+
+        $this->_aSystems = $this->getSystems();
+        if(!isset($this->_aSystems[$sSystem]))
+            return;
+
+        $this->_sSystem = $sSystem;
+        $this->_aSystem = $this->_aSystems[$sSystem];
+        $this->_aSystem['is_browse_filter'] = (int)$this->_bBrowseFilter;
+
+        $this->_iAuthorId = $this->_getAuthorId();
+
+        $this->_iDpMaxLevel = (int)$this->_aSystem['number_of_levels'];
+        $this->_sDisplayType = $this->_iDpMaxLevel == 0 ? BX_CMT_DISPLAY_FLAT : BX_CMT_DISPLAY_THREADED;
+        $this->_sDpSessionKey = 'bx_' . $this->_sSystem . '_dp_';
+
+        $this->_sBrowseType = $this->_aSystem['browse_type'];
+        $this->_sBrowseFilter = BX_CMT_FILTER_ALL;
+        $this->_sBpSessionKeyType = 'bx_' . $this->_sSystem . '_bpt_';
+        $this->_sBpSessionKeyFilter = 'bx_' . $this->_sSystem . '_bpf_';
+        $this->_aOrder = array(
+            'by' => BX_CMT_ORDER_BY_DATE,
+            'way' => BX_CMT_ORDER_WAY_ASC
+        );
+
+        list($mixedUserDp, $mixedUserBpType, $mixedUserBpFilter) = $this->_getUserChoice();
+        if(!empty($mixedUserDp))
+            $this->_sDisplayType = $mixedUserDp;
+        if(!empty($mixedUserBpType))
+            $this->_sBrowseType = $mixedUserBpType;
+        if(!empty($mixedUserBpFilter))
+            $this->_sBrowseFilter = $mixedUserBpFilter;
+
+        $this->_sViewUrl = 'page.php?i=cmts-view';
+        $this->_sBaseUrl = $this->_aSystem['base_url'];
+
+        $this->_sListAnchor = "cmts-anchor-%s-%d";
+        $this->_sItemAnchor = "cmt-anchor-%s-%d-%d";
+
+        $this->_oQuery = new BxDolCmtsQuery($this);
+
+        $this->_bPostQuote = false;
+        $this->_bMinPostForm = true;
+        $this->_sFormObject = 'sys_comment';
+        $this->_sFormDisplayPost = 'sys_comment_post';
+        $this->_sFormDisplayEdit = 'sys_comment_edit';
+
+        $this->_sConnObjFriends = 'sys_profiles_friends';
+        $this->_sConnObjSubscriptions = 'sys_profiles_subscriptions';
+
+        $this->_sMenuObjManage = 'sys_cmts_item_manage';
+        $this->_sMenuObjActions = 'sys_cmts_item_actions';
+        $this->_sMenuObjCounters = 'sys_cmts_item_counters';
+        $this->_sMenuObjMeta = 'sys_cmts_item_meta';
+
+        $this->_sMetatagsObj = 'sys_cmts';
+
+        $this->_aT = array(
+            'block_comments_title' => '_cmt_block_comments_title',
+            'txt_sample_single' => '_cmt_txt_sample_comment_single',
+            'txt_sample_vote_single' => '_cmt_txt_sample_vote_single',
+            'txt_sample_reaction_single' => '_cmt_txt_sample_reaction_single',
+            'txt_sample_score_up_single' => '_cmt_txt_sample_score_up_single',
+            'txt_sample_score_down_single' => '_cmt_txt_sample_score_down_single',
+            'txt_min_form_placeholder' => '_cmt_txt_min_form_placeholder'
+        );
+
+        if ($iInit)
+            $this->init($iId);
+
+        if ($oTemplate)
+            $this->_oTemplate = $oTemplate;
+        else
+            $this->_oTemplate = BxDolTemplate::getInstance();
+    }
+
+    /**
+     * get comments object instanse
+     * @param $sSys comments object name
+     * @param $iId associated content id, where comments are postred in
+     * @param $iInit perform initialization
+     * @return null on error, or ready to use class instance
+     */
+    public static function getObjectInstance($sSys, $iId, $iInit = true, $oTemplate = false)
+    {
+        if(isset($GLOBALS['bxDolClasses']['BxDolCmts!' . $sSys . $iId]))
+            return $GLOBALS['bxDolClasses']['BxDolCmts!' . $sSys . $iId];
+
+        $aSystems = self::getSystems();
+        if (!isset($aSystems[$sSys]))
+            return null;
+
+        $sClassName = 'BxTemplCmts';
+        if(!empty($aSystems[$sSys]['class_name'])) {
+            $sClassName = $aSystems[$sSys]['class_name'];
+            if(!empty($aSystems[$sSys]['class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aSystems[$sSys]['class_file']);
+        }
+
+        $o = new $sClassName($sSys, $iId, $iInit, $oTemplate);
+        return ($GLOBALS['bxDolClasses']['BxDolCmts!' . $sSys . $iId] = $o);
+    }
+    
+    /**
+     * get comments object instanse
+     * @param $iUniqId unique comment id
+     * @return null on error, or ready to use class instance
+     */
+    public static function getObjectInstanceByUniqId($iUniqId, $iInit = true, $oTemplate = false)
+    {
+        $aData = BxDolCmtsQuery::getInfoByUniqId($iUniqId);
+        if(empty($aData) || !is_array($aData))
+            return null;
+
+        return self::getObjectInstance($aData['system_name'], $aData['cmt_object_id']);
+    }
+
+    public static function &getSystems ()
+    {
+        $sKey = 'bx_dol_cache_memory_cmts_systems';
+
+        if (!isset($GLOBALS[$sKey])) {
+            $GLOBALS[$sKey] = BxDolDb::getInstance()->fromCache('sys_objects_cmts', 'getAllWithKey', '
+                SELECT
+                    `ID` as `system_id`,
+                    `Name` AS `name`,
+                    `Module` AS `module`,
+                    `Table` AS `table`,
+                    `CharsPostMin` AS `chars_post_min`,
+                    `CharsPostMax` AS `chars_post_max`,
+                    `CharsDisplayMax` AS `chars_display_max`,
+                    `Html` AS `html`,
+                    `PerView` AS `per_view`,
+                    `PerViewReplies` AS `per_view_replies`,
+                    `BrowseType` AS `browse_type`,
+                    `IsBrowseSwitch` AS `is_browse_switch`,
+                    `PostFormPosition` AS `post_form_position`,
+                    `NumberOfLevels` AS `number_of_levels`,
+                    `IsDisplaySwitch` AS `is_display_switch`,
+                    `IsRatable` AS `is_ratable`,
+                    `ViewingThreshold` AS `viewing_threshold`,
+                    `IsOn` AS `is_on`,
+                    `RootStylePrefix` AS `root_style_prefix`,
+                    `BaseUrl` AS `base_url`,
+                    `ObjectVote` AS `object_vote`,
+                    `ObjectReaction` AS `object_reaction`,
+                    `ObjectScore` AS `object_score`,
+                    `ObjectReport` AS `object_report`,
+                    `TriggerTable` AS `trigger_table`,
+                    `TriggerFieldId` AS `trigger_field_id`,
+                    `TriggerFieldAuthor` AS `trigger_field_author`,
+                    `TriggerFieldTitle` AS `trigger_field_title`,
+                    `TriggerFieldComments` AS `trigger_field_comments`,
+                    `ClassName` AS `class_name`,
+                    `ClassFile` AS `class_file`
+                FROM `' . self::$sTableSystems . '`', 'name');
+        }
+        return $GLOBALS[$sKey];
+    }
+
+    public static function getGlobalInfo ($iUniqueId)
+    {
+        return BxDolDb::getInstance()->getRow("SELECT `ti`.*, `ts`.`Name` AS `system_name` FROM `" . self::$sTableIds . "` AS `ti` LEFT JOIN `" . self::$sTableSystems . "` AS `ts` ON `ti`.`system_id`=`ts`.`ID` WHERE `ti`.`id`=:id LIMIT 1", array(
+            'id' => (int)$iUniqueId
+        ));
+    }
+
+    public static function getGlobalNumByParams($aParams = [])
+    {
+        $sQuery = "SELECT COUNT(*) FROM `" . self::$sTableIds . "` WHERE 1";
+        foreach($aParams as $aValue)
+            $sQuery .= " AND `" . $aValue['key'] ."` " . $aValue['operator'] . " '" . $aValue['value'] . "'";
+
+        return BxDolDb::getInstance()->getOne($sQuery);
+    }
+
+    public function init ($iId)
+    {
+        if (empty($this->iId) && $iId)
+            $this->setId($iId);
+
+        $this->addMarkers(array(
+            'object_id' => $this->getId(),
+            'user_id' => $this->_getAuthorId()
+        ));
+    }
+
+    public function getId ()
+    {
+        return $this->_iId;
+    }
+
+    public function isEnabled ()
+    {
+        return isset($this->_aSystem['is_on']) && $this->_aSystem['is_on'];
+    }
+
+    public function getSystemId()
+    {
+        return $this->_aSystem['system_id'];
+    }
+
+    public function getSystemName()
+    {
+        return $this->_sSystem;
+    }
+
+    public function getSystemInfo()
+    {
+        return $this->_aSystem;
+    }
+
+    public function getStorageObjectName()
+    {
+    	return $this->_getFormObject()->getStorageObjectName();
+    }
+
+    public function getTranscoderPreviewName()
+    {
+    	return $this->_getFormObject()->getTranscoderPreviewName();
+    }
+    
+    public function getFormObject()
+    {
+    	return $this->_getFormObject();
+    }
+
+    public function getTableNameImages()
+    {
+        return $this->_sTableImages;
+    }
+
+    public function getTableNameImages2Entries()
+    {
+        return $this->_sTableImages2Entries;
+    }
+
+    public function getLanguageKey($sIndex)
+    {
+        return isset($this->_aT[$sIndex]) ? $this->_aT[$sIndex] : '';
+    }
+
+    public function getMaxLevel()
+    {
+        return $this->_iDpMaxLevel;
+    }
+
+    public function getOrder ()
+    {
+        return $this->_sOrder;
+    }
+
+    public function getPerView ($iCmtParentId = 0)
+    {
+        return $iCmtParentId == 0 ? $this->_aSystem['per_view'] : $this->_aSystem['per_view_replies'];
+    }
+
+    public function getStatusAdmin()
+    {
+        return $this->isEditAllowedAll() || $this->isRemoveAllowedAll() || $this->isAutoApprove() ? BX_CMT_STATUS_ACTIVE : BX_CMT_STATUS_PENDING;
+    }
+
+    public function getViewUrl($iCmtId, $bAbsolute = true)
+    {
+    	if(empty($this->_aSystem['trigger_field_title']))
+            return '';
+
+        $s = BxDolPermalinks::getInstance()->permalink($this->_sViewUrl, [
+            'sys' => $this->_sSystem,
+            'id' => $this->_iId,
+            'cmt_id' => $iCmtId
+        ]);
+
+    	return $bAbsolute ? bx_absolute_url($s) : $s;
+    }
+
+    public function getViewText($mixedItem)
+    {
+        if(!is_array($mixedItem))
+            $mixedItem = $this->getCommentSimple((int)$mixedItem);
+
+        if(empty($mixedItem) || !is_array($mixedItem))
+            return '';
+
+        return $this->_prepareTextForOutput($mixedItem['cmt_text'], (int)$mixedItem['cmt_id']);
+    }
+
+    public function getBaseUrl()
+    {
+        $sUrl = $this->_replaceMarkers($this->_sBaseUrl);
+        $sUrl = BxDolPermalinks::getInstance()->permalink($sUrl);
+        if(get_mb_substr($sUrl, 0, 6) != 'http:/' && get_mb_substr($sUrl, 0, 7) != 'https:/')
+            $sUrl = BX_DOL_URL_ROOT . $sUrl;
+        return $sUrl;
+    }
+
+    public function getListUrl()
+    {
+        $sBaseUrl = $this->getBaseUrl();
+        if(empty($sBaseUrl))
+            return '';
+
+        return $sBaseUrl . $this->getListAnchor(true);
+    }
+
+    public function getItemUrl($iItemId)
+    {
+        $sBaseUrl = $this->getBaseUrl();
+        if(empty($sBaseUrl))
+            return '';
+
+        return $sBaseUrl . $this->getItemAnchor($iItemId, true);
+    }
+
+    public function getListAnchor($bWithHash = false)
+    {
+        return ($bWithHash ? '#' : '') . sprintf($this->_sListAnchor, str_replace('_', '-', $this->getSystemName()), $this->getId());
+    }
+
+    public function getItemAnchor($iItemId, $bWithHash = false)
+    {
+        return ($bWithHash ? '#' : '') . sprintf($this->_sItemAnchor, str_replace('_', '-', $this->getSystemName()), $this->getId(), $iItemId);
+    }
+
+    public function getAttachments($iCmtId)
+    {
+        $aResult = array();
+
+        if(!$this->isAttachImageEnabled())
+            return $aResult; 
+
+        $aFiles = $this->_oQuery->getFiles($this->_aSystem['system_id'], $iCmtId);
+        if(empty($aFiles) || !is_array($aFiles)) 
+            return $aResult;
+
+        $oStorage = BxDolStorage::getObjectInstance($this->getStorageObjectName());
+        $oTranscoder = BxDolTranscoderImage::getObjectInstance($this->getTranscoderPreviewName());
+
+        foreach($aFiles as $aFile) {
+            $bImage = $oTranscoder && $oTranscoder->isMimeTypeSupported($aFile['mime_type']);
+
+            $sPreview = '';
+            if($bImage)
+                $sPreview = $oTranscoder->getFileUrl($aFile['image_id']);
+
+            if(!$sPreview)
+                $sPreview = $this->_oTemplate->getIconUrl($oStorage->getIconNameByFileName($aFile['file_name']));
+
+            $sUrl = $oStorage->getFileUrlById($aFile['image_id']);
+
+            $aResult[] = array(
+                'id' => $aFile['image_id'],
+                'src' => $sPreview,
+                'src_orig' => $bImage ? $sUrl : '',
+                'url' => !$bImage ? $sUrl : ''
+            );
+        }
+
+        return $aResult;
+    }
+
+    public function getConnectionObject($sType)
+    {
+        $sResult = '';
+
+        switch($sType) {
+            case BX_CMT_FILTER_FRIENDS:
+                $sResult = $this->_sConnObjFriends;
+                break;
+            case BX_CMT_FILTER_SUBSCRIPTIONS:
+                $sResult = $this->_sConnObjSubscriptions;
+                break;
+        }
+
+        return $sResult;
+    }
+
+    public function getVoteObject($iEniqId)
+    {
+        if(empty($this->_aSystem['object_vote']))
+            $this->_aSystem['object_vote'] = 'sys_cmts';
+
+        $oVote = BxDolVote::getObjectInstance($this->_aSystem['object_vote'], $iEniqId, true, $this->_oTemplate);
+        if(!$oVote || !$oVote->isEnabled())
+            return false;
+
+        return $oVote;
+    }
+
+    public function getReactionObject($iEniqId)
+    {
+        if(empty($this->_aSystem['object_reaction']))
+            $this->_aSystem['object_reaction'] = 'sys_cmts_reactions';
+
+        $oReaction = BxDolVote::getObjectInstance($this->_aSystem['object_reaction'], $iEniqId, true, $this->_oTemplate);
+        if(!$oReaction || !$oReaction->isEnabled())
+            return false;
+
+        return $oReaction;
+    }
+
+    public function getScoreObject($iEniqId)
+    {
+        if(empty($this->_aSystem['object_score']))
+            $this->_aSystem['object_score'] = 'sys_cmts';
+
+        $oScore = BxDolScore::getObjectInstance($this->_aSystem['object_score'], $iEniqId, true, $this->_oTemplate);
+        if(!$oScore || !$oScore->isEnabled())
+            return false;
+
+        return $oScore;
+    }
+
+    public function getReportObject($iEniqId)
+    {
+        if(empty($this->_aSystem['object_report']))
+            $this->_aSystem['object_report'] = 'sys_cmts';
+
+        $oReport = BxDolReport::getObjectInstance($this->_aSystem['object_report'], $iEniqId, true, $this->_oTemplate);
+        if(!$oReport || !$oReport->isEnabled())
+            return false;
+
+        return $oReport;
+    }
+
+    public function getNotificationId()
+    {
+        return 'cmts-notification-' . $this->_sSystem . '-' . $this->_iId;
+    }
+
+    /**
+     * @deprecated since version 10.0.0-B3 and can be removed in later versions.
+     */
+    public function setTableNameFiles($sTable)
+    {
+        $this->_sTableImages = $sTable;
+    	$this->_oQuery->setTableNameFiles($sTable);
+    }
+
+    /**
+     * @deprecated since version 10.0.0-B3 and can be removed in later versions.
+     */
+    public function setTableNameFiles2Entries($sTable)
+    {
+    	$this->_sTableImages2Entries = $sTable;
+    	$this->_oQuery->setTableNameFiles2Entries($sTable);
+    }
+
+    public function isHtml ()
+    {
+        return $this->_aSystem['html'] > 0;
+    }
+
+    public function isRatable ()
+    {
+        return $this->_aSystem['is_ratable'];
+    }
+
+    public function isAttachImageEnabled()
+    {
+        return true;
+    }
+
+    public function isAutoApprove()
+    {
+        return getParam('sys_cmts_enable_auto_approve') == 'on';
+    }
+
+    /**
+     * set id to operate with votes
+     */
+    public function setId ($iId)
+    {
+        if ($iId == $this->getId()) return;
+        $this->_iId = $iId;
+    }
+
+    /**
+     * Add replace markers.
+     * @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;
+    }
+
+    /**
+     * Database functions
+     */
+    public function getQueryObject ()
+    {
+        return $this->_oQuery;
+    }
+
+    public function getCommentsTableName ()
+    {
+        return $this->_oQuery->getTableName ();
+    }
+
+    public function getObjectAuthorId ($iObjectId = 0)
+    {
+    	if(empty($this->_aSystem['trigger_field_author']))
+    		return 0;
+
+        return $this->_oQuery->getObjectAuthorId ($iObjectId ? $iObjectId : $this->getId());
+    }
+
+    public function getObjectTitle ($iObjectId = 0)
+    {
+        if(empty($this->_aSystem['trigger_field_title']))
+            return '';
+
+        $sTitle = $this->_oQuery->getObjectTitle ($iObjectId ? $iObjectId : $this->getId());
+        if(get_mb_substr($sTitle, 0, 1) == '_')
+            $sTitle = _t($sTitle);
+
+        return $sTitle;
+    }
+    
+    public function getObjectPrivacyView ($iObjectId = 0)
+    {
+        if(empty($iObjectId))
+            $iObjectId = $this->getId();
+
+        $sFieldPrivacyView = '';
+        if(!empty($this->_aSystem['trigger_field_privacy_view']))
+            $sFieldPrivacyView = $this->_aSystem['trigger_field_privacy_view'];
+
+        if(($iPrivacyView = $this->_oQuery->getObjectPrivacyView($iObjectId, $sFieldPrivacyView)) !== false)
+            return $iPrivacyView;
+
+        return BX_DOL_PG_ALL;
+    }
+
+    /*
+     * Gets Content Filter object.
+     */
+    public function getObjectContentFilter ()
+    {
+        $oCf = BxDolContentFilter::getInstance();
+        if(!$oCf->isEnabledForComments())
+            return false;
+
+        return $oCf;
+    }
+
+    public function getCommentsCountAll ($iObjectId = 0, $bForceCalculate = false)
+    {
+        return $this->_oQuery->getCommentsCountAll ($iObjectId ? $iObjectId : $this->getId(), $this->_getAuthorId(), $bForceCalculate);
+    }
+
+    public function getCommentsCount ($iObjectId = 0, $iCmtVParentId = -1, $sFilter = '')
+    {
+        return $this->_oQuery->getCommentsCount ($iObjectId ? $iObjectId : $this->getId(), $iCmtVParentId, $this->_getAuthorId(), $sFilter);
+    }
+
+    public function getCommentsArray ($iVParentId, $sFilter, $aOrder, $iStart = 0, $iCount = -1)
+    {
+        return $this->_oQuery->getComments ($this->getId(), $iVParentId, $this->_getAuthorId(), $sFilter, $aOrder, $iStart, $iCount);
+    }
+
+    public function getCommentsBy ($aParams = [])
+    {
+        return $this->_oQuery->getCommentsBy($aParams); 
+    }
+
+    /**
+     * Get comment's unique id.
+     */
+    public function getCommentUniqId($iCmtId, $iAuthorId = 0)
+    {
+        return $this->_oQuery->getUniqId($this->getSystemId(), $iCmtId, ['author_id' => $iAuthorId]);
+    }
+
+    /**
+     * Get comment's short info.
+     */
+    public function getCommentSimple ($iCmtId)
+    {
+        return $this->_oQuery->getCommentSimple ($this->getId(), $iCmtId);
+    }
+
+    /**
+     * Get comment's full info.
+     */
+    public function getCommentRow ($iCmtId)
+    {
+        return $this->_oQuery->getComment ($this->getId(), $iCmtId);
+    }
+
+    public function onObjectDelete ($iObjectId = 0)
+    {
+        // delete comments
+        $aFiles = $aCmtIds = [];
+        $this->_oQuery->deleteObjectComments ($iObjectId ? $iObjectId : $this->getId(), $aFiles, $aCmtIds);
+
+        // delete votes
+        $this->deleteVotes($aCmtIds);
+
+        // delete reactions
+        $this->deleteReactions($aCmtIds);
+
+        // delete scores
+        $this->deleteScores($aCmtIds);
+
+        // delete reports
+        $this->deleteReports($aCmtIds);
+
+        // delete meta info
+        $this->deleteMetaInfo($aCmtIds);
+
+        // delete files
+        if ($aFiles) {
+            $oStorage = BxDolStorage::getObjectInstance($this->getStorageObjectName());
+            if ($oStorage)
+                $oStorage->queueFilesForDeletion($aFiles);
+        }
+
+        // delete unique IDs
+        $this->deleteUniqueIds($aCmtIds);
+    }
+
+    public static function onAuthorDelete ($iAuthorId)
+    {
+        $aSystems = self::getSystems();
+        foreach($aSystems as $sSystem => $aSystem) {
+            $o = self::getObjectInstance($sSystem, 0);
+            $oQuery = $o->getQueryObject();
+
+            // delete comments
+            $aFiles = $aCmtIds = array ();
+            $oQuery->deleteAuthorComments($iAuthorId, $aFiles, $aCmtIds);
+
+            // delete votes
+            $o->deleteVotes($aCmtIds);
+
+            // delete reactions
+            $o->deleteReactions($aCmtIds);
+
+            // delete scores
+            $o->deleteScores($aCmtIds);
+
+            // delete reports
+            $o->deleteReports($aCmtIds);
+
+            // delete meta info
+            $o->deleteMetaInfo($aCmtIds);
+    
+            // delete files
+            $oStorage = BxDolStorage::getObjectInstance($o->getStorageObjectName());
+            if ($oStorage)
+                $oStorage->queueFilesForDeletion($aFiles);
+            
+            // delete unique IDs
+            $o->deleteUniqueIds($aCmtIds);
+        }
+        return true;
+    }
+
+    public static function onModuleEnable ($sModuleName)
+    {
+        $aSystems = self::getSystems();
+        foreach($aSystems as $sSystem => $aSystem) {
+            if ($sModuleName !== $aSystem['module'])
+                continue;
+
+            $o = self::getObjectInstance($sSystem, 0);
+            $o->registerTranscoders();
+        }
+
+        return true;
+    }
+
+    public static function onModuleDisable ($sModuleName)
+    {
+        $aSystems = self::getSystems();
+        foreach($aSystems as $sSystem => $aSystem) {
+            if ($sModuleName !== $aSystem['module'])
+                continue;
+
+            $o = self::getObjectInstance($sSystem, 0);
+            $o->unregisterTranscoders();
+        }
+
+        return true;
+    }
+
+    public static function onModuleUninstall ($sModuleName, &$iFiles = null)
+    {
+        $aSystems = self::getSystems();
+        foreach($aSystems as $sSystem => $aSystem) {
+            if ($sModuleName !== $aSystem['module'])
+                continue;
+
+            $o = self::getObjectInstance($sSystem, 0);
+            $oQuery = $o->getQueryObject();
+
+            // delete comments
+            $aFiles = $aCmtIds = array ();
+            $oQuery->deleteAll($aSystem['system_id'], $aFiles, $aCmtIds);
+
+            // delete votes
+            $o->deleteVotes($aCmtIds);
+
+            // delete reactions
+            $o->deleteReactions($aCmtIds);
+
+            // delete scores
+            $o->deleteScores($aCmtIds);
+
+            // delete reports
+            $o->deleteReports($aCmtIds);
+
+            // delete meta info
+            $o->deleteMetaInfo($aCmtIds);
+
+            // delete files
+            $oStorage = BxDolStorage::getObjectInstance($o->getStorageObjectName());
+            if ($oStorage && $aFiles)
+                $oStorage->queueFilesForDeletion($aFiles);
+
+            if (null !== $iFiles)
+                $iFiles += count($aFiles);
+
+            // delete unique IDs
+            $o->deleteUniqueIds($aCmtIds);
+        }
+
+        return true;
+    }
+
+    public function deleteVotes ($mixedCmtId)
+    {
+        if(!is_array($mixedCmtId))
+            $mixedCmtId = [$mixedCmtId];
+
+        foreach($mixedCmtId as $iCmtId)
+            if(($oReport = $this->getVoteObject($this->_oQuery->getUniqId($this->_aSystem['system_id'], $iCmtId))) !== false)
+                $oReport->onObjectDelete();
+    }
+
+    public function deleteReactions ($mixedCmtId)
+    {
+        if(!is_array($mixedCmtId))
+            $mixedCmtId = [$mixedCmtId];
+
+        foreach($mixedCmtId as $iCmtId)
+            if(($oReport = $this->getReactionObject($this->_oQuery->getUniqId($this->_aSystem['system_id'], $iCmtId))) !== false)
+                $oReport->onObjectDelete();
+    }
+
+    public function deleteScores ($mixedCmtId)
+    {
+        if(!is_array($mixedCmtId))
+            $mixedCmtId = [$mixedCmtId];
+
+        foreach($mixedCmtId as $iCmtId)
+            if(($oReport = $this->getScoreObject($this->_oQuery->getUniqId($this->_aSystem['system_id'], $iCmtId))) !== false)
+                $oReport->onObjectDelete();
+    }
+
+    public function deleteReports ($mixedCmtId)
+    {
+        if(!is_array($mixedCmtId))
+            $mixedCmtId = [$mixedCmtId];
+
+        foreach($mixedCmtId as $iCmtId)
+            if(($oReport = $this->getReportObject($this->_oQuery->getUniqId($this->_aSystem['system_id'], $iCmtId))) !== false)
+                $oReport->onObjectDelete();
+    }
+
+    public function deleteMetaInfo ($mixedCmtId)
+    {
+        if(!is_array($mixedCmtId))
+            $mixedCmtId = array($mixedCmtId);
+
+        $oMetatags = false;
+        if(!empty($this->_sMetatagsObj))
+            $oMetatags = BxDolMetatags::getObjectInstance($this->_sMetatagsObj);
+
+        if($oMetatags)
+            foreach($mixedCmtId as $iCmtId)
+                $oMetatags->onDeleteContent($this->_oQuery->getUniqId($this->_aSystem['system_id'], $iCmtId));
+    }
+
+    public function deleteUniqueIds ($mixedCmtId)
+    {
+        if(!is_array($mixedCmtId))
+            $mixedCmtId = array($mixedCmtId);
+
+        foreach($mixedCmtId as $iCmtId)
+            $this->_oQuery->deleteCmtIds($this->_aSystem['system_id'], $iCmtId);
+    }
+
+    /**
+     * Permissions functions
+     */
+    public function isAdmin($iCmtAuthorId)
+    {
+        return BxDolAcl::getInstance()->isMemberLevelInSet(array(MEMBERSHIP_ID_MODERATOR, MEMBERSHIP_ID_ADMINISTRATOR), $iCmtAuthorId);
+    }
+
+    public function checkAction ($sAction, $isPerformAction = false)
+    {
+        $iId = $this->_getAuthorId();
+        $a = checkActionModule($iId, $sAction, 'system', $isPerformAction);
+        return $a[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    public function checkActionErrorMsg ($sAction)
+    {
+        $iId = $this->_getAuthorId();
+        $a = checkActionModule($iId, $sAction, 'system');
+        return $a[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED ? $a[CHECK_ACTION_MESSAGE] : '';
+    }
+
+    public function isViewAllowed ($isPerformAction = false)
+    {
+        if (BxDolRequest::serviceExists($this->_aSystem['module'], 'check_allowed_comments_view')) {
+            $mixedResult = BxDolService::call($this->_aSystem['module'], 'check_allowed_comments_view', array($this->getId(), $this->getSystemName()));
+            if($mixedResult !== CHECK_ACTION_RESULT_ALLOWED)
+                return $mixedResult;
+        }
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    public function isVoteAllowed ($aCmt, $isPerformAction = false)
+    {
+        if(!$this->isRatable())
+            return false;
+
+        $oVote = $this->getVoteObject($aCmt['cmt_id']);
+        if($oVote === false)
+            return false;
+
+        $iUserId = (int)$this->_getAuthorId();
+        if($iUserId == 0)
+            return false;
+
+        if(isAdmin())
+            return true;
+
+        return $oVote->isAllowedVote($isPerformAction);
+    }
+
+    public function isScoreAllowed ($aCmt, $isPerformAction = false)
+    {
+        if(!$this->isRatable())
+            return false;
+
+        $oScore = $this->getScoreObject($aCmt['cmt_id']);
+        if($oScore === false)
+            return false;
+
+        $iUserId = (int)$this->_getAuthorId();
+        if($iUserId == 0)
+            return false;
+
+        if(isAdmin())
+            return true;
+
+        return $oScore->isAllowedVote($isPerformAction);
+    }
+
+    public function isReportAllowed ($aCmt, $isPerformAction = false)
+    {
+        $oReport = $this->getReportObject($aCmt['cmt_id']);
+        if($oReport === false)
+            return false;
+
+        $iUserId = (int)$this->_getAuthorId();
+        if($iUserId == 0)
+            return false;
+
+        if(isAdmin())
+            return true;
+
+        return $oReport->isAllowedReport($isPerformAction);
+    }
+
+    public function isPostAllowed ($isPerformAction = false)
+    {
+        if (BxDolRequest::serviceExists($this->_aSystem['module'], 'check_allowed_comments_post')) {
+            $mixedResult = BxDolService::call($this->_aSystem['module'], 'check_allowed_comments_post', array($this->getId(), $this->getSystemName()));
+            if($mixedResult !== CHECK_ACTION_RESULT_ALLOWED)
+                return false;
+        }
+
+        return $this->checkAction ('comments post', $isPerformAction);
+    }
+
+    public function msgErrPostAllowed ()
+    {
+        return $this->checkActionErrorMsg('comments post');
+    }
+
+    /**
+     * Determines whether a 'reply' action allowed or not.
+     * @param integer or array $mixedCmt - ID or an array which describes the comment to be replied
+     * @param boolean $isPerformAction
+     * @return boolean
+     */
+    public function isReplyAllowed ($mixedCmt, $isPerformAction = false)
+    {
+        return $this->isPostAllowed($isPerformAction);
+    }
+
+    public function msgErrReplyAllowed ()
+    {
+        return $this->msgErrPostAllowed ();
+    }
+
+    public function isQuoteAllowed ($mixedCmt, $isPerformAction = false)
+    {
+        return $this->_bPostQuote && $this->isReplyAllowed ($mixedCmt, $isPerformAction);
+    }
+
+    public function msgErrQuoteAllowed ()
+    {
+        return $this->msgErrReplyAllowed ();
+    }
+
+    public function isPinAllowed ($aCmt, $isPerformAction = false)
+    {
+        if((int)$aCmt['cmt_parent_id'] != 0 || (int)$aCmt['cmt_pinned'] != 0)
+            return false;
+
+        if(isAdmin())
+            return true;
+        
+        return $this->checkAction ('comments pin', $isPerformAction);
+    }
+
+    public function msgErrPinAllowed ()
+    {
+        return $this->checkActionErrorMsg('comments pin');
+    }
+
+    public function isUnpinAllowed ($aCmt, $isPerformAction = false)
+    {
+        if((int)$aCmt['cmt_pinned'] == 0)
+            return false;
+
+        if(isAdmin())
+            return true;
+        
+        return $this->checkAction ('comments pin', $isPerformAction);
+    }
+
+    public function msgErrUnpinAllowed ()
+    {
+        return $this->checkActionErrorMsg('comments pin');
+    }
+
+    public function isEditAllowed ($aCmt, $isPerformAction = false)
+    {
+        if(isAdmin())
+            return true;
+
+        if($this->isEditAllowedAll($isPerformAction))
+            return true;
+
+        $mixedResult = BxDolService::call($this->_aSystem['module'], 'check_allowed_comments_post', array($this->getId(), $this->getSystemName()));
+        if($mixedResult !== CHECK_ACTION_RESULT_ALLOWED)
+            return false;
+
+        return abs($aCmt['cmt_author_id']) == $this->_getAuthorId() && $this->checkAction ('comments edit own', $isPerformAction);
+    }
+
+    public function msgErrEditAllowed ()
+    {
+        return $this->checkActionErrorMsg ('comments edit own');
+    }
+
+    public function isEditAllowedAll ($isPerformAction = false)
+    {
+        return $this->checkAction('comments edit all', $isPerformAction);
+    }
+
+    public function isRemoveAllowed ($aCmt, $isPerformAction = false)
+    {
+        if(isAdmin())
+            return true;
+
+        if($this->isRemoveAllowedAll($isPerformAction))
+            return true;
+
+        if(abs($aCmt['cmt_author_id']) == $this->_getAuthorId() && $this->checkAction ('comments remove own', $isPerformAction))
+            return true;
+        
+        $aContentInfo = BxDolRequest::serviceExists($this->_aSystem['module'], 'get_all') ? BxDolService::call($this->_aSystem['module'], 'get_all', array(array('type' => 'id', 'id' => $this->getId()))) : array();
+        $oModule = BxDolModule::getInstance($this->_aSystem['module']);
+        $CNF = $oModule->_oConfig->CNF;
+        
+        if (isset($CNF['FIELD_AUTHOR']) && $aContentInfo[$CNF['FIELD_AUTHOR']] == $this->_getAuthorId() && $this->checkAction('comments remove in own content', $isPerformAction)){
+            return true;
+        }
+        
+        if (isset($CNF['FIELD_ALLOW_VIEW_TO']) &&  $aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']] < 0){
+            $iGroupProfileId = -(int)$aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']];
+            $oProfileContext = BxDolProfile::getInstance($iGroupProfileId);
+            $oModule = BxDolModule::getInstance($oProfileContext->getModule()); 
+            if ($oModule->_oConfig->isRoles() && $oModule->getRole($iGroupProfileId, $this->_getAuthorId()) === BX_BASE_MOD_GROUPS_ROLE_ADMINISTRATOR && $this->checkAction('comments remove in group context', $isPerformAction)){
+                return true;
+            }
+        }
+        
+        return false;
+    }
+
+    public function isRemoveAllowedAll ($isPerformAction = false)
+    {
+        return $this->checkAction ('comments remove all', $isPerformAction);
+    }
+
+    public function msgErrRemoveAllowed ()
+    {
+        return $this->checkActionErrorMsg('comments remove own');
+    }
+
+    public function isMoreAllowed ($aCmt, $isPerformAction = false)
+    {
+        $oMenuManage = BxDolMenu::getObjectInstance($this->_sMenuObjManage);
+        $oMenuManage->setCmtsData($this, $aCmt['cmt_id']);
+    	return $oMenuManage->isVisible();
+    }
+
+    public function isModerator($isPerformAction = false)
+    {
+        return $this->isEditAllowedAll($isPerformAction) || $this->isRemoveAllowedAll($isPerformAction);
+    }
+
+    /**
+     * Actions functions
+     */
+    public function actionPin ()
+    {
+        if (!$this->isEnabled())
+            return echoJson(array());
+
+        $iCmtId = 0;
+        if(bx_get('Cmt') !== false)
+            $iCmtId = bx_process_input(bx_get('Cmt'), BX_DATA_INT);
+
+        $aCmt = $this->getCommentRow($iCmtId);
+        if(empty($aCmt) || !is_array($aCmt))
+            return echoJson(array());
+
+        $iWay = 0;
+        if(bx_get('way') !== false)
+            $iWay = bx_process_input(bx_get('way'), BX_DATA_INT);
+
+        if(!$this->_oQuery->updateComments(array('cmt_pinned' => $iWay ? time() : 0), array('cmt_id' => $iCmtId)))
+            return echoJson(array());
+
+        $aBp = $aDp = array();
+        $this->_getParams($aBp, $aDp);
+        $this->_prepareParams($aBp, $aDp);
+
+        echoJson(array(
+            'parent_id' => $aCmt['cmt_parent_id'],
+            'per_view' => $aBp['per_view']
+        ));
+    }
+    
+    public function actionGetFormPost ()
+    {
+        if (!$this->isEnabled())
+            return '';
+
+        $iCmtParentId = bx_get('CmtParent');
+        $iCmtParentId = $iCmtParentId !== false ? bx_process_input($iCmtParentId, BX_DATA_INT) : 0;
+        
+        $sCmtBrowse = bx_get('CmtBrowse');
+        $sCmtBrowse = $sCmtBrowse !== false ? bx_process_input($sCmtBrowse, BX_DATA_TEXT) : '';
+
+        $sCmtDisplay = bx_get('CmtDisplay');
+        $sCmtDisplay = $sCmtDisplay !== false ? bx_process_input($sCmtDisplay, BX_DATA_TEXT) : '';
+
+        $bQuote = bx_get('CmtQuote');
+        $bQuote = $bQuote !== false ? bx_process_input($bQuote, BX_DATA_INT) == 1 : false;
+
+        $bMinPostForm = bx_get('CmtMinPostForm');
+        $bMinPostForm = $bMinPostForm !== false ? bx_process_input($bMinPostForm, BX_DATA_INT) == 1 : $this->_bMinPostForm;
+
+        return $this->getFormBoxPost(array('parent_id' => $iCmtParentId, 'type' => $sCmtBrowse), array('type' => $sCmtDisplay, 'dynamic_mode' => true, 'quote' => $bQuote, 'min_post_form' => $bMinPostForm));
+    }
+
+    public function actionGetFormEdit ()
+    {
+        if (!$this->isEnabled())
+            return echoJson(array());
+
+        $iCmtId = bx_process_input(bx_get('Cmt'), BX_DATA_INT);
+        echoJson($this->getFormEdit($iCmtId, array('dynamic_mode' => true)));
+    }
+
+    public function actionGetCmt ()
+    {
+        if(!$this->isEnabled())
+            return '';
+
+        if($this->isViewAllowed() !== CHECK_ACTION_RESULT_ALLOWED)
+            return '';
+
+        $iCmtId = bx_process_input($_REQUEST['Cmt'], BX_DATA_INT);
+        $sCmtBrowse = isset($_REQUEST['CmtBrowse']) ? bx_process_input($_REQUEST['CmtBrowse'], BX_DATA_TEXT) : '';
+        $sCmtDisplay = isset($_REQUEST['CmtDisplay']) ? bx_process_input($_REQUEST['CmtDisplay'], BX_DATA_TEXT) : '';
+
+        $aCmt = $this->getCommentRow($iCmtId);
+        echoJson([
+            'parent_id' => $aCmt['cmt_parent_id'],
+            'vparent_id' => $aCmt['cmt_parent_id'],
+            'content' => $this->getComment($aCmt, ['type' => $sCmtBrowse], ['type' => $sCmtDisplay, 'dynamic_mode' => true])
+        ]);
+    }
+
+    public function actionGetCmts ()
+    {
+        if (!$this->isEnabled())
+            return '';
+
+        if($this->isViewAllowed() !== CHECK_ACTION_RESULT_ALLOWED)
+            return '';
+
+        $aBp = $aDp = array();
+        $this->_getParams($aBp, $aDp);
+
+        $aDp['dynamic_mode'] = true;
+
+        //--- Beg: Using pregenerated structure
+        if(isset($aDp['structure']) && $aDp['structure'] === true) {
+            $iCmtId = isset($aBp['parent_id']) ? (int)$aBp['parent_id'] : 0;
+
+            $mixedStructure = $this->getCommentStructure($iCmtId, $aBp, $aDp);
+            if($mixedStructure === false || empty($mixedStructure[$iCmtId]['items'])) 
+                return '';
+
+            $aDp['structure'] = $mixedStructure[$iCmtId]['items'];
+            return $this->getCommentsByStructure($aBp, $aDp);
+        }
+        //--- End: Using pregenerated structure
+        else
+            return $this->{'getComments' . ($aBp['pinned'] ? 'Pinned' : '')}($aBp, $aDp);
+    }
+
+    public function actionSubmitPostForm()
+    {
+        if(!$this->isEnabled())
+            return echoJson(array());
+
+        $iCmtParentId = 0;
+        if(bx_get('cmt_parent_id') !== false)
+            $iCmtParentId = bx_process_input(bx_get('cmt_parent_id'), BX_DATA_INT);
+
+        echoJson($this->getFormPost($iCmtParentId, array('dynamic_mode' => true)));
+    }
+
+    public function actionSubmitEditForm()
+    {
+        if (!$this->isEnabled()) {
+            echoJson(array());
+            return;
+        };
+
+        $iCmtId = 0;
+        if(bx_get('cmt_id') !== false)
+            $iCmtId = bx_process_input(bx_get('cmt_id'), BX_DATA_INT);
+
+        echoJson($this->getFormEdit($iCmtId, array('dynamic_mode' => true)));
+    }
+
+    public function actionRemove()
+    {
+        if (!$this->isEnabled())
+            return echoJson([]);
+
+        $iCmtId = 0;
+        if(bx_get('Cmt') !== false)
+            $iCmtId = bx_process_input(bx_get('Cmt'), BX_DATA_INT);
+
+        echoJson($this->remove($iCmtId));
+    }
+    
+    public function remove($iCmtId)
+    {
+        $aCmt = $this->_oQuery->getCommentsBy(array('type' => 'id', 'id' => $iCmtId));
+        if(!$aCmt)
+            return array('msg' => _t('_No such comment'));
+        
+        $iObjId = $this->getId();
+        if(!$iObjId){
+            $this->setId($aCmt['cmt_object_id']);
+            $iObjId = $aCmt['cmt_object_id'];
+        }
+        $iObjAthrId = $this->getObjectAuthorId($iObjId);
+        $iObjAthrPrivacyView = $this->getObjectPrivacyView($iObjId);
+
+        if($aCmt['cmt_replies'] > 0) {
+            if(!$this->isModerator())
+                return array('msg' => _t('_Can not delete comments with replies'));
+
+            $aReplies = $this->_oQuery->getCommentsBy(array('type' => 'parent_id', 'parent_id' => $iCmtId));
+            foreach($aReplies as $aReply)
+                $this->remove($aReply['cmt_id']);
+        }
+
+        $iPerformerId = $this->_getAuthorId();
+        if(!$this->isRemoveAllowed($aCmt))
+            return array('msg' => $aCmt['cmt_author_id'] == $iPerformerId ? strip_tags($this->msgErrRemoveAllowed()) : _t('_Access denied'));
+
+        $iCmtPrntId = (int)$aCmt['cmt_parent_id'];
+        if(!$this->_oQuery->removeComment($iObjId, $iCmtId, $iCmtPrntId)) 
+            return array('msg' => _t('_cmt_err_cannot_perform_action'));
+
+        $this->_triggerComment();
+
+        $oStorage = BxDolStorage::getObjectInstance($this->getStorageObjectName());
+
+        $aImages = $this->_oQuery->getFiles($this->_aSystem['system_id'], $iCmtId);
+        foreach($aImages as $aImage)
+            $oStorage->deleteFile($aImage['image_id']);
+
+        $this->_oQuery->deleteImages($this->_aSystem['system_id'], $iCmtId);
+
+        $this->isRemoveAllowed($aCmt, true);
+
+        $this->deleteVotes($iCmtId);
+
+        $this->deleteReactions($iCmtId);
+
+        $this->deleteScores($iCmtId);
+
+        $this->deleteReports($iCmtId);
+
+        $this->deleteMetaInfo($iCmtId);
+
+        $aAuditParams = $this->_prepareAuditParams($iCmtId, array('comment_author_id' => $aCmt['cmt_author_id'], 'comment_text' => $aCmt['cmt_text']));
+        bx_audit($iObjId, $this->_aSystem['module'], '_sys_audit_action_delete_comment', $aAuditParams);
+
+        $aAlertParams = $this->_prepareAlertParams($aCmt);
+        bx_alert($this->_sSystem, 'commentRemoved', $iObjId, $iPerformerId, $aAlertParams);
+        bx_alert('comment', 'deleted', $iCmtId, $iPerformerId, $aAlertParams);
+
+        if(!empty($iCmtPrntId)) {
+            $aCmtPrnt = $this->_oQuery->getCommentSimple($iObjId, $iCmtPrntId);
+            if(!empty($aCmtPrnt) && is_array($aCmtPrnt)) {
+                $aAlertParamsReply = $this->_prepareAlertParamsReply($aCmt, $aCmtPrnt);
+                bx_alert($this->_sSystem, 'replyRemoved', $iCmtPrntId, $iPerformerId, $aAlertParamsReply);
+                bx_alert('reply', 'deleted', $iCmtId, $iPerformerId, $aAlertParamsReply);
+            }
+        }
+
+        $this->deleteUniqueIds($iCmtId);
+
+        $iCount = (int)$this->getCommentsCountAll(0, true);
+        return [
+            'id' => $iCmtId,
+            'count' => $iCount,
+            'countf' => $iCount > 0 ? $this->getCounter() : ''
+        ];
+    }
+
+    public function add($aValues)
+    {
+        return $this->_getFormAdd($aValues);
+    }
+
+    public function actionResumeLiveUpdate()
+    {
+    	$sKey = $this->getNotificationId();
+
+    	bx_import('BxDolSession');
+    	BxDolSession::getInstance()->unsetValue($sKey);
+    }
+
+    public function actionPauseLiveUpdate()
+    {
+    	$sKey = $this->getNotificationId();
+
+    	bx_import('BxDolSession');
+    	BxDolSession::getInstance()->setValue($sKey, 1);
+    }
+
+    public function actionGetSiblingFiles()
+    {
+        $iFileId = 0;
+        if(bx_get('file_id') !== false)
+            $iFileId = bx_process_input(bx_get('file_id'), BX_DATA_INT);
+
+        if(!($aFileInfo = $this->_oQuery->getFileInfoById((int)$iFileId))) 
+            return echoJson(['error' => _t('_sys_txt_error_occured')]);
+
+        $aSiblings = [];
+        $aFiles = $this->_oQuery->getFiles($aFileInfo['system_id'], $aFileInfo['cmt_id']);
+        foreach($aFiles as $iIndex => $aFile) {
+            if($aFile['id'] != $iFileId)
+                continue;
+
+            $aSiblings['prev'] = isset($aFiles[$iIndex - 1]) ? $aFiles[$iIndex - 1] : false;
+            $aSiblings['next'] = isset($aFiles[$iIndex + 1]) ? $aFiles[$iIndex + 1] : false;
+        }
+
+        $oStorage = BxDolStorage::getObjectInstance($this->getStorageObjectName());
+        $oTranscoder = BxDolTranscoderImage::getObjectInstance($this->getTranscoderPreviewName());
+
+        foreach($aSiblings as $sSibling => $aSibling) {
+            if(!$aSibling)
+                continue;
+
+            $sFile = '';
+            if($oTranscoder && $oTranscoder->isMimeTypeSupported($aSibling['mime_type']))
+                $sFile = $oStorage->getFileUrlById($aSibling['image_id']);
+            else
+                $sFile = $this->_oTemplate->getIconUrl($oStorage->getIconNameByFileName($aFile['file_name']));
+
+            $aImageInfo = BxDolImageResize::getInstance()->getImageSize($sFile);
+
+            $iWidth = $iHeight = 0;
+            if(isset($aImageInfo['w']))
+                $iWidth = (int)$aImageInfo['w'];
+            if(isset($aImageInfo['h']))
+                $iHeight = (int)$aImageInfo['h'];
+
+            $aSiblings[$sSibling] = array_merge($aSibling, [
+                'file' => $sFile,
+                'file_id' => $aSibling['id'],
+                'w' => $iWidth, 
+                'h' => $iHeight
+            ]);
+        }
+
+        return echoJson($aSiblings);
+    }
+
+    public function onPostAfter($iCmtId, $aDp = [])
+    {
+        $iObjId = (int)$this->getId();
+
+        $aCmt = $this->_oQuery->getCommentSimple($iObjId, $iCmtId);
+        if(empty($aCmt) || !is_array($aCmt))
+            return false;
+
+        $iCmtPrntId = (int)$aCmt['cmt_parent_id'];
+        $iPerformerId = (int)$aCmt['cmt_author_id'];
+
+        $aAlertParams = $this->_prepareAlertParams($aCmt);
+        bx_alert($this->_sSystem, 'commentPost', $iObjId, $iPerformerId, $aAlertParams);
+        bx_alert('comment', 'added', $iCmtId, $iPerformerId, $aAlertParams);
+
+        $aAuditParams = $this->_prepareAuditParams($iCmtId, array('comment_author_id' => $aCmt['cmt_author_id'], 'comment_text' => $aCmt['cmt_text']));
+        bx_audit($iObjId, $this->_aSystem['module'], '_sys_audit_action_add_comment', $aAuditParams);
+
+        if(!empty($iCmtPrntId)) {
+            $aCmtPrnt = $this->_oQuery->getCommentSimple($iObjId, $iCmtPrntId);
+            if(!empty($aCmtPrnt) && is_array($aCmtPrnt)) {
+                $aAlertParamsReply = $this->_prepareAlertParamsReply($aCmt, $aCmtPrnt);
+                bx_alert($this->_sSystem, 'replyPost', $iCmtPrntId, $iPerformerId, $aAlertParamsReply);
+                bx_alert('comment', 'replied', $iCmtId, $iPerformerId, $aAlertParamsReply);
+            }
+        }
+
+        $iCount = (int)$this->getCommentsCountAll(0, true);
+
+        return [
+            'id' => $iCmtId, 
+            'parent_id' => $iCmtPrntId,
+            'count' => $iCount,
+            'countf' => $iCount > 0 ? $this->getCounter() : ''
+        ];
+    }
+
+    public function onEditAfter($iCmtId, $aDp = [])
+    {
+    	$aCmt = $this->getCommentRow($iCmtId);
+        if(empty($aCmt) || !is_array($aCmt))
+            return false;
+
+        $aBp = [];
+        $this->_getParams($aBp, $aDp);
+
+        $iObjId = (int)$this->getId();
+        $iPerformerId = $this->_getAuthorId();
+
+        $aAlertParams = $this->_prepareAlertParams($aCmt);
+        bx_alert($this->_sSystem, 'commentUpdated', $iObjId, $iPerformerId, $aAlertParams);
+        bx_alert('comment', 'edited', $iCmtId, $iPerformerId, $aAlertParams);
+
+        $aAuditParams = $this->_prepareAuditParams($iCmtId, ['comment_author_id' => $aCmt['cmt_author_id'], 'comment_text' => $aCmt['cmt_text']]);
+        bx_audit($iObjId, $this->_aSystem['module'], '_sys_audit_action_edit_comment', $aAuditParams);
+
+        return ['id' => $iCmtId, 'content' => $this->_getContent($aCmt, $aBp, $aDp)];
+    }
+
+    public function serviceGetAuthor ($iContentId)
+    {
+        $aComment = $this->_oQuery->getCommentsBy(array('type' => 'id', 'id' => $iContentId));
+        $this->setId($aComment['cmt_object_id']);
+
+        return $aComment['cmt_author_id'];
+    }
+
+    public function serviceGetDateAdded ($iContentId)
+    {
+        $aComment = $this->_oQuery->getCommentsBy(array('type' => 'id', 'id' => $iContentId));
+        $this->setId($aComment['cmt_object_id']);
+
+        return $aComment['cmt_time'];
+    }
+
+    public function serviceGetDateChanged ($iContentId)
+    {
+        return 0;
+    }
+    public function serviceGetLink ($iContentId)
+    {
+        $aComment = $this->_oQuery->getCommentsBy(array('type' => 'id', 'id' => $iContentId));
+        $this->setId($aComment['cmt_object_id']);
+
+        return $this->getViewUrl($iContentId);
+    }
+
+    public function serviceGetTitle ($iContentId)
+    {
+        return '';
+    }
+
+    public function serviceGetText ($iContentId)
+    {
+        $aComment = $this->_oQuery->getCommentsBy(array('type' => 'id', 'id' => $iContentId));
+        $this->setId($aComment['cmt_object_id']);
+
+        return $aComment['cmt_text'];
+    }
+
+    public function serviceGetThumb ($iContentId)
+    {
+        return '';
+    }
+
+    public function serviceGetInfo ($iContentId, $bSearchableFieldsOnly = true)
+    {
+        $aComment = $this->_oQuery->getCommentsBy(array('type' => 'id', 'id' => $iContentId));
+        $this->setId($aComment['cmt_object_id']);
+
+        return BxDolContentInfo::formatFields($aComment);
+    }
+
+    public function serviceGetSearchResultUnit ($iContentId, $sUnitTemplate = '')
+    {
+        $aComment = $this->_oQuery->getCommentsBy(array('type' => 'id', 'id' => $iContentId));
+        if(empty($aComment) || !is_array($aComment))
+            return '';
+
+        $this->setId($aComment['cmt_object_id']);
+
+        return $this->getComment($aComment, array(), array('type' => BX_CMT_DISPLAY_FLAT, 'view_only' => true));
+    }
+
+    public function serviceGetAll ($aParams = array())
+    {
+        if(empty($aParams) || !is_array($aParams))
+            $aParams = array('type' => 'all');
+
+        return $this->_oQuery->getCommentsBy($aParams);
+    }
+
+    public function serviceGetSearchableFieldsExtended($aInputsAdd = array())
+    {
+        $oForm = BxDolForm::getObjectInstance('sys_comment', 'sys_comment_post', $this->_oTemplate);
+        if(!$oForm)
+            return array();
+
+        $aSrchNamesExcept = array();
+        $aSrchCaptionsSystem = array(
+            'cmt_author_id' => '_sys_form_comment_input_caption_system_cmt_author_id',
+            'cmt_text' => '_sys_form_comment_input_caption_system_cmt_text'
+        );
+        $aSrchCaptions = array(
+            'cmt_author_id' => '_sys_form_comment_input_caption_cmt_author_id',
+            'cmt_text' => '_sys_form_comment_input_caption_cmt_text'
+        );
+
+        $aResult = array(
+            'cmt_author_id' => array(
+                'type' => 'text_auto', 
+                'caption_system' => $aSrchCaptionsSystem['cmt_author_id'],
+                'caption' => $aSrchCaptions['cmt_author_id'],
+                'info' => '',
+                'value' => '',
+                'values' => '',
+                'pass' => ''
+            )
+        );
+
+        $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'], $aSrchNamesExcept))
+                $aResult[$aInput['name']] = array(
+                    'type' => $aInput['type'], 
+                    'caption_system' => !empty($aInput['caption_system_src']) ? $aInput['caption_system_src'] : '',
+                    'caption' => !empty($aInput['caption_src']) ? $aInput['caption_src'] : (!empty($aSrchCaptions[$aInput['name']]) ? $aSrchCaptions[$aInput['name']] : ''),
+                    'info' => !empty($aInput['info_src']) ? $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'] : ''
+                );
+
+        return $aResult;
+    }
+
+    /**
+     * Overwrite this method and register transcoder(s) if comments object uses custom transcoder(s), 
+     * which differs from default one 'sys_cmts_images_preview'
+     */
+    public function registerTranscoders()
+    {}
+
+	/**
+     * Overwrite this method and unregister transcoder(s) if comments object uses custom transcoder(s), 
+     * which differs from default one 'sys_cmts_images_preview'
+     */
+    public function unregisterTranscoders()
+    {}
+
+    public function serviceGetSearchResultExtended($aParams, $iStart = 0, $iPerPage = 0, $bFilterMode = false)
+    {
+        if((empty($aParams) || !is_array($aParams)) && !$bFilterMode)
+            return array();
+
+        return $this->_oQuery->getCommentsBy(array('type' => 'search_ids', 'search_params' => $aParams, 'start' => $iStart, 'per_page' => $iPerPage));
+    }
+
+    public function getAuthorInfo($iAuthorId = 0)
+    {
+        return $this->_getAuthorInfo($iAuthorId);
+    }
+    
+    public function getParams(&$aBp, &$aDp)
+    {
+        return $this->_getParams($aBp, $aDp);
+    }
+    
+    public function prepareParams(&$aBp, &$aDp)
+    {
+         return $this->_prepareParams($aBp, $aDp);
+    }
+
+    /**
+     * Internal functions
+     */
+    protected function _getAuthorId ()
+    {
+        return isMember() ? bx_get_logged_profile_id() : 0;
+    }
+
+    protected function _getAuthorPassword ()
+    {
+        return getLoggedPassword();
+    }
+
+    protected function _getAuthorIp ()
+    {
+        return getVisitorIP();
+    }
+
+    protected function _getAuthorInfo($iAuthorId = 0)
+    {
+        $iUserId = $this->_getAuthorId();
+        $iAuthorId = (int)$iAuthorId;
+        $oProfile = $this->_getAuthorObject($iAuthorId);
+        if (!$oProfile->isActive() && !isAdmin() && !BxDolAcl::getInstance()->isMemberLevelInSet(array(MEMBERSHIP_ID_MODERATOR, MEMBERSHIP_ID_ADMINISTRATOR)))
+            $oProfile = BxDolProfileUndefined::getInstance();
+
+        return array(
+            $oProfile->getDisplayName(),
+            $oProfile->getUrl(),
+            $oProfile->getThumb(),
+            $oProfile->getUnit(0, array('template' => 'unit_wo_info')),
+            $oProfile->getBadges()
+        );
+    }
+
+    protected function _getAuthorObject($iAuthorId = 0)
+    {
+        return BxDolProfile::getInstanceMagic((int)$iAuthorId);
+    }
+
+    protected function _getFormObject($sAction = BX_CMT_ACTION_POST)
+    {
+        $sDisplayName = '_sFormDisplay' . ucfirst($sAction);
+
+        return BxDolForm::getObjectInstance($this->_sFormObject, $this->$sDisplayName, false, $this->_sSystem);
+    }
+    
+    protected function _unsetFormObject($sAction = BX_CMT_ACTION_POST)
+    {
+        $sDisplayName = '_sFormDisplay' . ucfirst($sAction);
+
+        return BxDolForm::unsetObjectInstance($this->_sFormObject, $this->$sDisplayName, false, $this->_sSystem);
+    }
+
+    protected function _getParams(&$aBp, &$aDp)
+    {
+        //--- Process 'Browse' params. 
+        $aBp['parent_id'] = isset($aBp['parent_id']) ? (int)$aBp['parent_id'] : 0;
+
+        $aBp['vparent_id'] = isset($aBp['vparent_id']) ? (int)$aBp['vparent_id'] : 0;
+        if(bx_get('CmtParent') !== false)
+            $aBp['vparent_id'] = bx_process_input(bx_get('CmtParent'), BX_DATA_INT);
+
+    	$aBp['type'] = isset($aBp['type']) ? $aBp['type'] : '';
+    	if(bx_get('CmtBrowse') !== false) 
+        	$aBp['type'] = bx_process_input(bx_get('CmtBrowse'), BX_DATA_TEXT);
+
+    	$aBp['filter'] = isset($aBp['filter']) ? $aBp['filter'] : '';
+    	if(bx_get('CmtFilter') !== false) 
+    	    $aBp['filter'] = bx_process_input(bx_get('CmtFilter'), BX_DATA_TEXT);
+
+        $aBp['start'] = isset($aBp['start']) ? (int)$aBp['start'] : -1;
+        if(bx_get('CmtStart') !== false) 
+            $aBp['start'] = bx_process_input($_REQUEST['CmtStart'], BX_DATA_INT);
+
+        $aBp['per_view'] = isset($aBp['per_view']) ? (int)$aBp['per_view'] : -1;
+        if(bx_get('CmtPerView') !== false) 
+            $aBp['per_view'] = bx_process_input($_REQUEST['CmtPerView'], BX_DATA_INT);
+
+        $aBp['pinned'] = isset($aBp['pinned']) ? (int)$aBp['pinned'] : 0;
+        if(bx_get('CmtPinned') !== false) 
+            $aBp['pinned'] = bx_process_input(bx_get('CmtPinned'), BX_DATA_INT);
+
+        //--- Process 'Display' params.
+        $aDp['type'] = isset($aDp['type']) ? $aDp['type'] : '';
+        if(bx_get('CmtDisplay') !== false) 
+            $aDp['type'] = bx_process_input($_REQUEST['CmtDisplay'], BX_DATA_TEXT);
+
+        if(bx_get('CmtDisplayStructure') !== false) {
+            $aDp['structure'] = bx_process_input($_REQUEST['CmtDisplayStructure'], BX_DATA_INT) == 1;
+
+            if($aDp['structure'] && bx_get('CmtParent') !== false)
+                $aBp['parent_id'] = bx_process_input(bx_get('CmtParent'), BX_DATA_INT);
+        }
+
+        $aDp['blink'] = isset($aDp['blink']) ? $aDp['blink'] : '';
+        if(bx_get('CmtBlink') !== false) 
+            $aDp['blink'] = bx_process_input($_REQUEST['CmtBlink'], BX_DATA_TEXT);
+
+        $aDp['quote'] = isset($aDp['quote']) ? (int)$aDp['quote'] : 0;
+        if(bx_get('CmtQuote') !== false) 
+            $aDp['quote'] = bx_process_input($_REQUEST['CmtQuote'], BX_DATA_INT);
+
+        $aDp['min_post_form'] = isset($aDp['min_post_form']) ? (bool)$aDp['min_post_form'] : $this->_bMinPostForm;
+        if(bx_get('CmtMinPostForm') !== false)
+            $aDp['min_post_form'] = bx_process_input(bx_get('CmtMinPostForm'), BX_DATA_INT) == 1;
+
+        $aDp['in_designbox'] = isset($aDp['in_designbox']) ? (bool)$aDp['in_designbox'] : true;
+        $aDp['dynamic_mode'] = isset($aDp['dynamic_mode']) ? (bool)$aDp['dynamic_mode'] : false;
+        $aDp['show_empty'] = isset($aDp['show_empty']) ? (bool)$aDp['show_empty'] : false;
+    }
+
+    protected function _prepareAlertParams($aCmt)
+    {
+        $iObjId = (int)$this->getId();
+        $iObjAthrId = $this->getObjectAuthorId($iObjId);
+        $iObjAthrPrivacyView = $this->getObjectPrivacyView($iObjId);
+
+        $iCmtId = (int)$aCmt['cmt_id'];
+        $iCmtUniqId = $this->getCommentUniqId($iCmtId);
+        $iCmtCf = isset($aCmt['cmt_cf']) ? (int)$aCmt['cmt_cf'] : BxDolContentFilter::getInstance()->getDefaultValue();
+
+        return array(
+            'source' => 'sys_cmts_' . $iCmtUniqId,
+
+            'object_system' => $this->_sSystem, 
+            'object_id' => $iObjId, 
+            'object_author_id' => $iObjAthrId,
+
+            'comment_id' => $iCmtId, 
+            'comment_uniq_id' => $iCmtUniqId,
+            'comment_author_id' => $aCmt['cmt_author_id'], 
+            'comment_text' => $aCmt['cmt_text'],
+
+            'privacy_view' => $iObjAthrPrivacyView,
+            'cf' => $iCmtCf
+        );
+    }
+
+    protected function _prepareAlertParamsReply($aCmt, $aCmtPrnt)
+    {
+        $iObjId = (int)$this->getId();
+        $iObjAthrId = $this->getObjectAuthorId($iObjId);
+        $iObjAthrPrivacyView = $this->getObjectPrivacyView($iObjId);
+
+        $iCmtPrntId = (int)$aCmt['cmt_parent_id'];
+        $iCmtPrntUniqId = $this->getCommentUniqId($iCmtPrntId);
+
+        $iCmtId = (int)$aCmt['cmt_id'];
+        $iCmtUniqId = $this->getCommentUniqId($iCmtId);
+
+        return array(
+            'source' => 'sys_cmts_' . $iCmtUniqId,
+
+            'object_system' => $this->_sSystem, 
+            'object_id' => $iObjId, 
+            'object_author_id' => $iObjAthrId,
+
+            'parent_id' => $iCmtPrntId,
+            'parent_uniq_id' => $iCmtPrntUniqId,
+            'parent_author_id' => $aCmtPrnt['cmt_author_id'],
+
+            'comment_id' => $iCmtId,
+            'comment_uniq_id' => $iCmtUniqId,
+            'comment_author_id' => $aCmt['cmt_author_id'],  
+            'comment_text' => $aCmt['cmt_text'],
+
+            'privacy_view' => $iObjAthrPrivacyView,
+        );
+    }
+
+    protected function _prepareAuditParams($iId, $aData)
+    {
+        $sModule = $this->_aSystem['module'];
+        $oModule = BxDolModule::getInstance($sModule);
+        $CNF = isset($oModule->_oConfig->CNF) ? $oModule->_oConfig->CNF : array();
+
+        $aContentInfo = BxDolRequest::serviceExists($sModule, 'get_all') ? BxDolService::call($sModule, 'get_all', array(array('type' => 'id', 'id' => $this->getId()))) : array();
+        $iContextId = 0;
+        if (!empty($aContentInfo)){
+            $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' => $this->getObjectTitle() ,
+            'context_profile_id' => $iContextId,
+            'content_info_object' =>  isset($CNF['OBJECT_CMTS_CONTENT_INFO']) ? $CNF['OBJECT_CMTS_CONTENT_INFO'] : '',
+            'data' => array_merge(array('comment_id' => $iId), $aData)
+        );
+        if ($iContextId > 0)
+            $AuditParams['context_profile_title'] = BxDolProfile::getInstance($iContextId)->getDisplayName();
+        
+        return $AuditParams;
+    }
+
+    protected function _prepareTextForOutput ($s, $iCmtId = 0)
+    {
+    	$iDataAction = !$this->isHtml() ? BX_DATA_TEXT_MULTILINE : BX_DATA_HTML;
+    	$s = bx_process_output($s, $iDataAction);
+    	$s = bx_linkify_html($s, 'class="' . BX_DOL_LINK_CLASS . '"');
+
+        if($iCmtId && $this->_sMetatagsObj && ($oMetatags = BxDolMetatags::getObjectInstance($this->_sMetatagsObj)) !== false)
+            $s = $oMetatags->metaParse($this->_oQuery->getUniqId($this->_aSystem['system_id'], $iCmtId), $s);
+
+        return $s;
+    }
+
+    protected function _prepareStructureBp($sDType, &$aBp)
+    {
+        $aBp['type'] = !empty($aBp['type']) ? $aBp['type'] : $this->_sBrowseType;
+        $aBp['filter'] = !empty($aBp['filter']) ? $aBp['filter'] : $this->_sBrowseFilter;
+        $aBp['parent_id'] = isset($aBp['parent_id']) ? $aBp['parent_id'] : 0;
+        $aBp['start'] = isset($aBp['start']) ? $aBp['start'] : -1;
+        $aBp['init_view'] = isset($aBp['init_view']) ? $aBp['init_view'] : -1;
+        $aBp['per_view'] = isset($aBp['per_view']) ? $aBp['per_view'] : -1;
+        $aBp['order']['by'] = isset($aBp['order_by']) ? $aBp['order_by'] : $this->_aOrder['by'];
+        $aBp['order']['way'] = isset($aBp['order_way']) ? $aBp['order_way'] : $this->_aOrder['way'];
+
+        if($aBp['per_view'] == -1)
+            switch($sDType) {
+                case BX_CMT_DISPLAY_FLAT:
+                    $aBp['per_view'] = $this->getPerView(0);
+                    break;
+
+                case BX_CMT_DISPLAY_THREADED:
+                    $aBp['per_view'] = $this->getPerView($aBp['parent_id']);
+                    break;
+            }
+    }
+
+    protected function _prepareParams(&$aBp, &$aDp)
+    {
+        $aBp['type'] = !empty($aBp['type']) ? $aBp['type'] : $this->_sBrowseType;
+        $aBp['filter'] = !empty($aBp['filter']) ? $aBp['filter'] : $this->_sBrowseFilter;
+        $aBp['parent_id'] = isset($aBp['parent_id']) ? $aBp['parent_id'] : 0;
+        $aBp['start'] = isset($aBp['start']) ? $aBp['start'] : -1;
+        $aBp['init_view'] = isset($aBp['init_view']) ? $aBp['init_view'] : -1;
+        $aBp['per_view'] = isset($aBp['per_view']) ? $aBp['per_view'] : -1;
+        $aBp['pinned'] = isset($aBp['pinned']) ? (int)$aBp['pinned'] : 0;
+        $aBp['order']['by'] = isset($aBp['order_by']) ? $aBp['order_by'] : $this->_aOrder['by'];
+        $aBp['order']['way'] = isset($aBp['order_way']) ? $aBp['order_way'] : $this->_aOrder['way'];
+
+        $aDp['type'] = !empty($aDp['type']) ? $aDp['type'] : $this->_sDisplayType;
+        $aDp['blink'] = !empty($aDp['blink']) ? $aDp['blink'] : array();
+        if(!is_array($aDp['blink']))
+            $aDp['blink'] = explode(',', $aDp['blink']);
+        $aDp['quote'] = !empty($aDp['quote']) ? (int)$aDp['quote'] : 0;
+        if(!isset($aDp['min_post_form']))
+            $aDp['min_post_form'] = $this->_bMinPostForm;
+
+        switch($aDp['type']) {
+            case BX_CMT_DISPLAY_FLAT:
+                $aBp['vparent_id'] = -1;
+                $aBp['per_view'] = $aBp['per_view'] != -1 ? $aBp['per_view'] : $this->getPerView(0);
+                break;
+
+            case BX_CMT_DISPLAY_THREADED:
+                $iParent = 0;
+                if(isset($aBp['vparent_id']))
+                    $iParent = $aBp['vparent_id'];
+                else if(isset($aBp['parent_id']))
+                    $iParent = $aBp['parent_id'];
+
+                $aBp['per_view'] = $aBp['per_view'] != -1 ? $aBp['per_view'] : $this->getPerView($iParent);
+                break;
+        }
+
+        switch ($aBp['type']) {
+            case BX_CMT_BROWSE_POPULAR:
+                $aBp['order'] = array(
+                    'by' => BX_CMT_ORDER_BY_POPULAR,
+                    'way' => BX_CMT_ORDER_WAY_DESC
+                );
+                break;
+        }
+
+        if(!isset($aBp['count']))
+            $aBp['count'] = $this->getCommentsCount($this->_iId, $aBp['vparent_id'], $aBp['filter']);
+
+        if($aBp['start'] != -1)
+            return;
+
+        $aBp['start'] = 0;
+        if($aBp['type'] == BX_CMT_BROWSE_TAIL) {
+            $sPerView = ($aBp['init_view'] != -1 ? 'init' : 'per') . '_view';
+
+            $aBp['start'] = $aBp['count'] - $aBp[$sPerView];
+            if($aBp['start'] < 0) {
+                $aBp[$sPerView] += $aBp['start'];
+                $aBp['start'] = 0;
+            }
+        }
+
+        $this->_setUserChoice($aDp['type'], $aBp['type'], $aBp['filter']);
+    }
+
+    protected function _triggerComment()
+    {
+        if(!$this->_aSystem['trigger_table'] || !$this->_aSystem['trigger_field_id'] || !$this->_aSystem['trigger_field_comments'])
+            return false;
+
+        $iId = $this->getId();
+        if(!$iId)
+            return false;
+
+        $iCount = $this->getCommentsCount($iId);
+        return $this->_oQuery->updateTriggerTable($iId, $iCount);
+    }
+
+    /**
+     * 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);
+    }
+
+    protected function _getUserChoice()
+    {
+        $mixedDp = $mixedBpType = $mixedBpFilter = false;
+        if(!isLogged())
+            return array($mixedDp, $mixedBpType, $mixedBpFilter);
+
+        $iUserId = $this->_getAuthorId();
+
+        $oSession = BxDolSession::getInstance();
+
+        $mixedDp = $oSession->getValue($this->_sDpSessionKey . $iUserId);
+        $mixedBpType = $oSession->getValue($this->_sBpSessionKeyType . $iUserId);
+        $mixedBpFilter = $oSession->getValue($this->_sBpSessionKeyFilter . $iUserId);
+
+        return array($mixedDp, $mixedBpType, $mixedBpFilter);
+    }
+
+    protected function _setUserChoice($sDp, $sBpType, $sBpFilter)
+    {
+        if(!isLogged())
+            return;
+
+        $iUserId = $this->_getAuthorId();
+
+        $oSession = BxDolSession::getInstance();
+
+        if(!empty($sDp))
+            $oSession->setValue($this->_sDpSessionKey . $iUserId, $sDp);
+
+        if(!empty($sBpType))
+            $oSession->setValue($this->_sBpSessionKeyType . $iUserId, $sBpType);
+
+        if(!empty($sBpFilter))
+            $oSession->setValue($this->_sBpSessionKeyFilter . $iUserId, $sBpFilter);
+    }
+
+    protected function _sendNotificationEmail($iCmtId, $iCmtParentId)
+    {
+        $aCmt = $this->getCommentRow($iCmtId);
+        $aCmtParent = $this->getCommentRow($iCmtParentId);
+        if(empty($aCmt) || !is_array($aCmt) || empty($aCmtParent) || !is_array($aCmtParent) || (int)$aCmt['cmt_author_id'] == (int)$aCmtParent['cmt_author_id'])
+            return;
+
+        $oProfile = $this->_getAuthorObject($aCmtParent['cmt_author_id']);
+
+        if($oProfile instanceof BxDolProfileUndefined)
+        	return;
+
+        $iAccount = $oProfile->getAccountId();
+        $aAccount = BxDolAccount::getInstance($iAccount)->getInfo();
+
+        $aPlus = array();
+        $aPlus['sender_display_name'] = $oProfile->getDisplayName();
+        $aPlus['reply_text'] = $this->_prepareTextForOutput($aCmt['cmt_text'], $iCmtId);
+
+        $sPageUrl = $this->getBaseUrl();
+        if(empty($sPageUrl))
+            $sPageUrl = $this->getViewUrl($iCmtParentId);
+        else 
+            $sPageUrl .= $this->getItemAnchor($iCmtParentId, true);
+        $aPlus['page_url'] = $sPageUrl;
+
+        $sPageTitle = $this->getObjectTitle();
+        if(empty($sPageTitle))
+            $sPageTitle = _t('_Content');
+        $aPlus['page_title'] = $sPageTitle;
+
+        $aTemplate = BxDolEmailTemplates::getInstance()->parseTemplate('t_CommentReplied', $aPlus);
+        return $aTemplate && sendMail($aAccount['email'], $aTemplate['Subject'], $aTemplate['Body']);
+    }
+
+    protected function _isShowDoComment($aParams, $isAllowedComment, $bCount)
+    {
+        $bShowDoComment = !isset($aParams['show_do_comment']) || $aParams['show_do_comment'] == true;
+
+        return $bShowDoComment && ($isAllowedComment || $bCount);
+    }
+
+    protected function _isShowCounter($aParams, $isAllowedComment, $bCount)
+    {
+        $bShowCounter = isset($aParams['show_counter']) && $aParams['show_counter'] === true;
+
+        return $bShowCounter && ($isAllowedComment || $bCount);
+    }
+
+    /**
+     * Note. By default image based controls aren't used.
+     * Therefore it can be overwritten in custom template.
+     */
+    protected function _getImageDo()
+    {
+    	return '';
+    }
+
+    protected function _getIconDo()
+    {
+    	return 'far comment';
+    }
+
+    protected function _getTitleDo()
+    {
+    	return '_cmt_txt_do';
+    }
+    
+    public function _getStructure($mixedItem, $aBp, &$iLevel, &$aStructure)
+    {
+        $bItem = !empty($mixedItem) && is_array($mixedItem);
+
+        if($bItem) {
+            $iI = 'i' . $mixedItem['cmt_id'];
+            $aStructure[$iI] = array(
+                'id' => $mixedItem['cmt_id'], 
+                'order' => isset($mixedItem['cmt_order']) ? $mixedItem['cmt_order'] : 0, 
+                'data' => $this->getCommentSimple((int)$mixedItem['cmt_id']),
+                'items' => array(),
+            );
+            $aStructure[$iI]['data']['author_data'] = BxDolProfile::getData($aStructure[$iI]['data']['cmt_author_id']);
+        }
+
+        if(!$bItem || (int)$mixedItem['cmt_replies'] > 0) {
+            $aItems = $this->_oQuery->getStructure($this->_iId, $this->_iAuthorId, $bItem ? $mixedItem['cmt_id'] : 0, $aBp['filter'], $aBp['order']);
+            if(!empty($aItems)) {
+                if($bItem && $iLevel < (int)$this->_aSystem['number_of_levels']) {
+                    $iPassLevel = $iLevel + 1;
+                    $aPassStructure = &$aStructure[$iI]['items'];
+                }
+                else {
+                    $iPassLevel = $iLevel;
+                    $aPassStructure = [];//&$aStructure TODO: need to be checked by author, changed to avoid *RECURSION*;
+                }
+                
+                foreach($aItems as $aItem)
+                    $this->_getStructure($aItem, $aBp, $iPassLevel, $aPassStructure);
+                
+                //--- Sort subitems
+                $iWay = isset($aBp['order']['way']) && $aBp['order']['way'] == 'desc' ? -1 : 1;
+
+                uasort($aPassStructure, function($aItem1, $aItem2) use ($iWay) {
+                    if($aItem1['order'] == $aItem2['order'])
+                        return 0;
+
+                    return $iWay * ($aItem1['order'] < $aItem2['order'] ? -1 : 1);
+                });
+                $aStructure[$iI]['items'] = $aPassStructure;
+            }
+        }
+    }
+}
+
+/** @} */

+ 895 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolConnection.php

@@ -0,0 +1,895 @@
+<?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);
+
+        return ($GLOBALS['bxDolClasses']['BxTemplConnection!'.$sObject] = $o);
+    }
+
+    /**
+     * Get connection type.
+     * return BX_CONNECTIONS_TYPE_ONE_WAY or BX_CONNECTIONS_TYPE_MUTUAL
+     */ 
+    public function getType()
+    {
+        return $this->_sType;
+    }
+
+    /**
+     * 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(($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);
+            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->checkAllowedConnect ($iInitiator, $iContent, true, $iMutual, false);
+
+        return true;
+    }
+
+    /**
+     * 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,
+        ));
+
+        return true;
+    }
+
+    /**
+     * 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)
+    {
+        return $this->_oQuery->getConnectedContentCount($iInitiator, $isMutual);
+    }
+
+    /**
+     * 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');
+    }
+}
+
+/** @} */

+ 329 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolContentInfo.php

@@ -0,0 +1,329 @@
+<?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
+ * @{
+ */
+
+/**
+ * Get content info from Content modules
+ *
+ * Add record to sys_objects_content_info table to provide unified access to module's content,
+ * to get content info just create an instance of this class and request content info by id,
+ * for example:
+ * @code
+ *  BxDolContentInfo::getObjectInstance('my_system')->getInfo(25); // 25 - is object id
+ * @endcode
+ *
+ * Description of sys_objects_content_info table fields:
+ * @code
+ *  `name` - system name, it is better to use unique module prefix here, lowercase and all spaces are underscored
+ *  `alert_unit` - unit name of the alert which will be fired 
+ *  `alert_action_add` - action name of the alert which is fired after a content was added
+ *  `alert_action_update` - action name of the alert which is fired after a content was updated
+ *  `alert_action_delete` - action name of the alert which is fired after a content was deleted
+ *  `class_name` - your custom class name, if you overrride default class
+ *  `class_file` - your custom class path
+ * @endcode
+ *
+ */
+class BxDolContentInfo extends BxDolFactory implements iBxDolFactoryObject
+{
+    protected $_oQuery;
+
+    protected $_sSystem;
+    protected $_aSystem;
+    protected $_aGrid;
+    protected $_bIgnoreCacheInServiceCalls = false;
+
+    protected function __construct($sSystem)
+    {
+        parent::__construct();
+
+        if(empty($sSystem))
+            return;
+
+        $aSystems = $this->getSystems();
+        if(!isset($aSystems[$sSystem]))
+            return;
+
+        $this->_sSystem = $sSystem;
+        $this->_aSystem = $aSystems[$sSystem];
+        $this->_aGrid = array();
+    }
+
+   /**
+     * get content info object instance
+     * @param $sSystem view object name
+     * @return null on error, or ready to use class instance
+     */
+    public static function getObjectInstance($sSystem)
+    {
+        $sClassKey = 'BxDolContentInfo!' . $sSystem;
+
+        if(isset($GLOBALS['bxDolClasses'][$sClassKey]))
+            return $GLOBALS['bxDolClasses'][$sClassKey];
+
+        $aSystems = self::getSystems();
+        if(!isset($aSystems[$sSystem]))
+            return null;
+
+        $sClassName = 'BxDolContentInfo';
+        if(!empty($aSystems[$sSystem]['class_name'])) {
+            $sClassName = $aSystems[$sSystem]['class_name'];
+            if(!empty($aSystems[$sSystem]['class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aSystems[$sSystem]['class_file']);
+        }
+
+        $GLOBALS['bxDolClasses'][$sClassKey] = new $sClassName($sSystem);
+        return $GLOBALS['bxDolClasses'][$sClassKey];
+    }
+
+    /**
+     * get content info object instance (for internal usage). 
+     * @see self::getObjectInstanceByAlertAdd, self::getObjectInstanceByAlertUpdate and self::getObjectInstanceByAlertDelete
+     * @param $sAlertType alert type (add, update, delete)
+     * @param $sUnit alert unit
+     * @param $sAction alert action
+     * @return null on error, or ready to use class instance
+     */
+    public static function getObjectInstanceByAlertCommon($sAlertType, $sUnit, $sAction)
+    {
+        $sAlert = $sUnit . '_' . $sAction;
+        $sClassKey = 'BxDolContentInfo!' . bx_gen_method_name($sAlertType . '_' . $sAlert);
+
+        if(isset($GLOBALS['bxDolClasses'][$sClassKey]))
+            return $GLOBALS['bxDolClasses'][$sClassKey];
+
+        $aSystems = self::getSystemsByAlertType($sAlertType);
+        if(!isset($aSystems[$sAlert]))
+            return null;
+
+        $sClassName = 'BxDolContentInfo';
+        if(!empty($aSystems[$sAlert]['class_name'])) {
+            $sClassName = $aSystems[$sAlert]['class_name'];
+            if(!empty($aSystems[$sAlert]['class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aSystems[$sAlert]['class_file']);
+        }
+
+        $GLOBALS['bxDolClasses'][$sClassKey] = new $sClassName($aSystems[$sAlert]['name']);
+        return $GLOBALS['bxDolClasses'][$sClassKey];
+    }
+
+    public static function getObjectInstanceByAlertAdd($sUnit, $sAction)
+    {
+        return self::getObjectInstanceByAlertCommon('add', $sUnit, $sAction);
+    }
+
+    public static function getObjectInstanceByAlertUpdate($sUnit, $sAction)
+    {
+        return self::getObjectInstanceByAlertCommon('update', $sUnit, $sAction);
+    }
+
+    public static function getObjectInstanceByAlertDelete($sUnit, $sAction)
+    {
+        return self::getObjectInstanceByAlertCommon('delete', $sUnit, $sAction);
+    }
+
+	/**
+     * get content info object instance (for internal usage). 
+     * @see self::getObjectInstanceByAlertAdd, self::getObjectInstanceByAlertUpdate and self::getObjectInstanceByAlertDelete
+     * @param $sAlertType alert type (add, update, delete)
+     * @param $sUnit alert unit
+     * @param $sAction alert action
+     * @return null on error, or ready to use class instance
+     */
+    public static function getObjectInstanceByGrid($sGrid)
+    {
+        $aGrids = self::getGrids();
+        if(!isset($aGrids[$sGrid]))
+            return null;
+
+        $oObject = self::getObjectInstance($aGrids[$sGrid]['object']);
+        $oObject->setGrid($aGrids[$sGrid]);
+
+        return $oObject;
+    }
+
+    public static function &getSystems()
+    {
+        $sKey = 'bx_dol_cache_memory_content_info_systems';
+
+        if(!isset($GLOBALS[$sKey]))
+            $GLOBALS[$sKey] = BxDolDb::getInstance()->fromCache('sys_objects_content_info', 'getAllWithKey', '
+                SELECT
+                    `id` as `id`,
+                    `name` AS `name`,
+                    `title` AS `title`,
+                    `alert_unit` AS `alert_unit`,
+                    `alert_action_add` AS `alert_action_add`,
+                    `alert_action_update` AS `alert_action_update`,
+                    `alert_action_delete` AS `alert_action_delete`,
+                    `class_name` AS `class_name`,
+                    `class_file` AS `class_file`
+                FROM `sys_objects_content_info`', 'name');
+
+        return $GLOBALS[$sKey];
+    }
+
+    public static function &getGrids()
+    {
+        $sKey = 'bx_dol_cache_memory_content_info_grids';
+
+        if(!isset($GLOBALS[$sKey]))
+            $GLOBALS[$sKey] = BxDolDb::getInstance()->fromCache('sys_objects_content_info_grids', 'getAllWithKey', '
+                SELECT
+                    `id` as `id`,
+                    `object` AS `object`,
+                    `grid_object` AS `grid_object`,
+                    `grid_field_id` AS `grid_field_id`,
+                    `condition` AS `condition`,
+                    `selection` AS `selection`
+                FROM `sys_content_info_grids`', 'grid_object');
+
+        return $GLOBALS[$sKey];
+    }
+
+    public static function &getSystemsByAlertType($sAlertType)
+    {
+        $sKey = 'bx_dol_cache_memory_content_info_systems_' . $sAlertType;
+
+        if(!isset($GLOBALS[$sKey])) {
+            $aSystems = BxDolContentInfo::getSystems();
+            foreach($aSystems as $aSystem)
+                $GLOBALS[$sKey][$aSystem['alert_unit'] . '_' . $aSystem['alert_action_' . $sAlertType]] = $aSystem;
+        }
+
+        return $GLOBALS[$sKey];
+    }
+
+    public static function formatFields($aContentInfo)
+    {
+        foreach ($aContentInfo as $sKey => $mixedValue)
+            if(is_numeric($mixedValue)) {
+                if(strpos($mixedValue, '.') !== false)
+                    $aContentInfo[$sKey] = (float)$mixedValue;
+                else 
+                    $aContentInfo[$sKey] = (int)$mixedValue;
+            }
+
+        return $aContentInfo;
+    }
+
+    public function setGrid($aGrid)
+    {
+        $this->_aGrid = $aGrid;
+    }
+
+    public function setIgnoreCacheInServiceCalls($b)
+    {
+        $this->_bIgnoreCacheInServiceCalls = $b;
+    }
+
+    public function getName()
+    {
+        return $this->_sSystem;
+    }
+
+    public function getTitle()
+    {
+        return $this->_aSystem['title'];
+    }
+
+    public function getGrid()
+    {
+        return $this->_aGrid;
+    }
+
+    public function getContentAuthor ($iContentId)
+    {
+        return $this->_call('get_author', $iContentId);
+    }
+
+    public function getContentDateAdded ($iContentId)
+    {
+        return $this->_call('get_date_added', $iContentId);
+    }
+
+    public function getContentDateChanged ($iContentId)
+    {
+        return $this->_call('get_date_changed', $iContentId);
+    }
+
+    public function getContentTitle ($iContentId)
+    {
+        return $this->_call('get_title', $iContentId);
+    }
+
+    public function getContentThumb ($iContentId)
+    {
+        return $this->_call('get_thumb', $iContentId);
+    }
+    
+    public function getContentCover ($iContentId)
+    {
+        return $this->_call('get_cover', $iContentId);
+    }
+
+    public function getContentLink ($iContentId)
+    {
+        return $this->_call('get_link', $iContentId);
+    }
+
+    public function getContentText ($iContentId)
+    {
+        return $this->_call('get_text', $iContentId);
+    }
+    
+    public function getContentPrivacy ($iContentId)
+    {
+        return $this->_call('get_privacy_view', $iContentId);
+    }
+
+    public function getContentEmbed ($iContentId)
+    {
+        return $this->_call('get_embed', $iContentId);
+    }
+
+    public function getContentInfo ($iContentId, $bSearchableFieldsOnly = true)
+    {
+        return $this->_call('get_info', $iContentId, $bSearchableFieldsOnly);
+    }
+
+    public function getContentSearchResultUnit ($iContentId, $sUnitTemplate = '')
+    {
+        return $this->_call('get_search_result_unit', $iContentId, $sUnitTemplate);
+    }
+
+    public function getAll ($aParams = array())
+    {
+        return $this->_call('get_all', $aParams);
+    }
+
+    public function getSearchableFieldsExtended ()
+    {
+        return $this->_call('get_searchable_fields_extended');
+    }
+    
+    public function getSortableFieldsExtended ()
+    {
+        return $this->_call('get_sortable_fields_extended');
+    }
+
+    public function getSearchResultExtended ($aParams, $iStart = 0, $iPerPage = 0, $bFilterMode = false)
+    {
+        return $this->_call('get_search_result_extended', $aParams, $iStart, $iPerPage, $bFilterMode);
+    }
+
+    protected function _call($sMethod)
+    {
+        if(!BxDolRequest::serviceExists($this->_sSystem, $sMethod))
+            return false;
+
+        return BxDolService::call($this->_sSystem, $sMethod, array_slice(func_get_args(), 1), 'Module', $this->_bIgnoreCacheInServiceCalls);
+    }
+}
+
+/** @} */

+ 1346 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolDb.php

@@ -0,0 +1,1346 @@
+<?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_DB_MODE_SILENT', PDO::ERRMODE_SILENT);
+define('BX_DB_MODE_EXCEPTION', PDO::ERRMODE_EXCEPTION);
+
+define('BX_DB_ERR_CONNECT_FAILD', 1);
+define('BX_DB_ERR_QUERY_ERROR', 2);
+define('BX_DB_ERR_ESCAPE', 3);
+
+define('BX_PDO_STATE_NOT_EXECUTED', NULL);
+define('BX_PDO_STATE_SUCCESS', '00000');
+
+class BxDolDb extends BxDolFactory implements iBxDolSingleton
+{	
+    protected static $_rLink;
+    protected static $_aDbCacheData;
+
+    protected static $_aParams;
+    protected static $_sParamsCacheName = 'sys_options';
+    protected static $_sParamsCacheNameMixed = 'sys_options_mixed_';
+    protected static $_sParamsCacheNameMix = 'sys_options_mix_';
+
+    protected static $_sErrorKey = 'bx_db_error';
+    protected static $_aErrors = array(
+    	BX_DB_ERR_CONNECT_FAILD => 'Database connect failed',
+    	BX_DB_ERR_QUERY_ERROR => 'Database query error',
+    	BX_DB_ERR_ESCAPE => 'Escape string error'
+    );
+
+    protected static $_bReadOnlyMode = true;
+    protected static $_bMultuServersMode = false;
+
+	protected $_bPdoPersistent;
+	protected $_iPdoFetchType;
+	protected $_iPdoErrorMode;
+
+	protected $_bErrorChecking;
+    protected $_aError;
+
+	protected $_sHost, $_sPort, $_sSocket, $_sDbname, $_sUser, $_sPassword, $_sCharset, $_sStorageEngine;
+
+    protected $_oStatement = null;
+    protected $_oDbCacheObject = null;
+
+    /**
+     * set database parameters and connect to it
+     */
+    protected function __construct($aDbConf = false)
+    {
+        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->_bPdoPersistent = true;
+        if (defined('BX_DATABASE_PERSISTENT'))
+            $this->_bPdoPersistent = (bool)BX_DATABASE_PERSISTENT;
+
+        $this->_iPdoFetchType = PDO::FETCH_ASSOC;
+        $this->_iPdoErrorMode = BX_DB_MODE_EXCEPTION;
+
+        $this->_bErrorChecking = true;
+        $this->_aError = array();
+
+        $this->_sStorageEngine = 'MYISAM';
+        if (defined('BX_DATABASE_ENGINE'))
+            $this->_sStorageEngine = BX_DATABASE_ENGINE;
+
+        $this->_sCharset = 'utf8mb4';
+        if($aDbConf === false) {
+            $this->_sHost = BX_DATABASE_HOST;
+            $this->_sPort = BX_DATABASE_PORT;
+            $this->_sSocket = BX_DATABASE_SOCK;
+            $this->_sDbname = BX_DATABASE_NAME;
+            $this->_sUser = BX_DATABASE_USER;
+            $this->_sPassword = BX_DATABASE_PASS;
+        } 
+        else {
+            $this->_sHost = $aDbConf['host'];
+            $this->_sPort = $aDbConf['port'];
+            $this->_sSocket = $aDbConf['sock'];
+            $this->_sDbname = $aDbConf['name'];
+            $this->_sUser = $aDbConf['user'];
+            $this->_sPassword = $aDbConf['pwd'];
+            if(isset($aDbConf['charset']))
+            	$this->_sCharset = $aDbConf['charset'];
+            if(isset($aDbConf['error_checking']))
+                $this->_bErrorChecking = $aDbConf['error_checking'];
+            if(isset($aDbConf['storage_engine']))
+            	$this->_sStorageEngine = $aDbConf['storage_engine'];
+        }
+
+        @set_exception_handler(array($this, 'pdoExceptionHandler'));
+    }
+
+    /**
+     * 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()
+    {
+        $null = null;
+        return self::getInstanceWithConf(false, $null);
+    }
+    
+    public static function getInstanceWithConf($aDbConf, &$sError)
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__])) {
+            if($aDbConf === false && !defined('BX_DATABASE_HOST'))
+                return null;
+
+            $o = new BxDolDb($aDbConf);
+            $sErrorMessage = $o->connect();
+            if($sErrorMessage) {
+                if($sError !== null)
+                    $sError = $sErrorMessage;
+
+                return null;
+            }
+
+			$GLOBALS['bxDolClasses'][__CLASS__] = $o;
+        }
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    public static function getLink()
+    {
+    	return self::$_rLink;
+    }
+
+    public function setReadOnlyMode($b)
+    {
+        if ($b == self::$_bReadOnlyMode)
+            return;
+        self::$_bReadOnlyMode = $b;
+        if (self::$_bMultuServersMode) {
+            $this->disconnect();
+            $this->connect();
+        }
+    }
+
+    protected function balancer()
+    {
+        // regular mode - one DB server
+        if (!is_array($this->_sHost))
+            return array($this->_sHost, $this->_sPort, $this->_sSocket, $this->_sUser, $this->_sPassword, $this->_sDbname);
+
+        $iServersNum = count($this->_sHost);
+        if ($iServersNum > 1)
+            self::$_bMultuServersMode = true;
+
+        // in read/write mode always use first server, which should be always master (also check for regular mode but with array with 1 item)        
+        if (!self::$_bReadOnlyMode || 1 == $iServersNum)
+            $i = 0;
+        // in case of 2 servers - always use second server, in read only mode
+        elseif (2 == $iServersNum)
+            $i = 1;
+        // when there are more that 2 servers, randomly select read only server
+        else
+            $i = rand(1, $iServersNum-1);
+            
+        return array($this->_sHost[$i], $this->_sPort[$i], $this->_sSocket[$i], $this->_sUser[$i], $this->_sPassword[$i], $this->_sDbname[$i]);
+    }
+
+    /**
+     * connect to database with appointed parameters
+     */
+    public function connect()
+    {
+    	if(self::$_rLink)
+    		return;
+
+        list ($sHost, $sPort, $sSocket, $sUser, $sPassword, $sDBName) = $this->balancer();
+
+    	try {   
+	    	$sDsn = "mysql:host=" . $sHost . ";";
+	   		$sDsn .= $sPort ? "port=" . $sPort . ";" : "";
+	   		$sDsn .= $sSocket ? "unix_socket=" . $sSocket . ";" : "";
+	    	$sDsn .= "dbname=" . $sDBName . ";charset=" . $this->_sCharset;
+
+	        self::$_rLink = new PDO($sDsn, $sUser, $sPassword, array(
+				PDO::ATTR_ERRMODE => $this->_iPdoErrorMode,
+				PDO::ATTR_DEFAULT_FETCH_MODE => $this->_iPdoFetchType,
+				PDO::ATTR_PERSISTENT => $this->_bPdoPersistent
+	        ));
+
+            $this->pdoExec("SET NAMES 'utf8mb4' COLLATE 'utf8mb4_unicode_ci'");
+            $this->pdoExec("SET sql_mode = ''");
+            if (0 == strcasecmp($this->_sStorageEngine, 'INNODB'))
+                $this->pdoExec("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE");
+
+            $sVer = $this->getVersion();
+            $sStorageEngine = !$sVer || version_compare($sVer, '5.7.5', '>=') ? 'default_storage_engine' : 'storage_engine';
+            $this->pdoExec("SET $sStorageEngine=" . $this->_sStorageEngine);
+
+			$this->cleanMemoryAll();
+    	}
+    	catch (PDOException $oException) {
+    		$oException->errorInfo[self::$_sErrorKey] = array(
+    			'code' => BX_DB_ERR_CONNECT_FAILD,
+    			'message' => $oException->getMessage(),
+    			'trace' => $oException->getTrace()
+    		);
+
+    		throw $oException;
+    	}
+    }
+
+    /**
+     * close mysql connection
+     */
+    public function disconnect()
+    {
+        self::$_rLink = null;
+    }
+
+    /**
+     * check mysql connection
+     */
+    public function ping()
+    {
+    	try {
+    		$this->pdoQuery("SELECT 1");
+    	}
+    	catch (PDOException $e) {
+    		return false;
+    	}
+
+    	return true;
+    }
+
+    /**
+     * Can be used to execute queries which shouldn't return data
+     */
+    public function pdoExec($sQuery)
+    {
+    	return self::$_rLink->exec($sQuery);
+    }
+
+    /**
+     * Executes query and returns PDOStatement object or false 
+     */
+	public function pdoQuery($sQuery)
+    {
+    	return self::$_rLink->query($sQuery);
+    }
+
+    /**
+     * database query exception handler for exceptions appeared out of the try/catch block
+     */
+    public function pdoExceptionHandler($oException)
+    {
+        if(!($oException instanceof PDOException)) {
+            throw $oException;
+            return;
+        }
+
+		if(!isset($oException->errorInfo[self::$_sErrorKey]))
+			$oException->errorInfo[self::$_sErrorKey] = array(
+				'code' => BX_DB_ERR_QUERY_ERROR,
+				'message' => !empty($oException->errorInfo[2]) ? $oException->errorInfo[2] : $oException->getMessage(),
+				'trace' => $oException->getTrace()
+			);
+
+    	$this->error($oException->errorInfo[self::$_sErrorKey]);
+    }
+
+    /**
+     * get mysql option
+     */
+    function getOption($sName)
+    {
+    	$oStatement = $this->pdoQuery("SELECT @@{$sName}");
+    	return $this->getOne($oStatement);
+    }
+
+    /**
+     * execute sql query and return one value result
+     */
+    public function getOne($oStatement, $aBindings = array(), $iIndex = 0)
+    {
+        if(!$oStatement)
+			return false;
+		else if(!($oStatement instanceof PDOStatement) && is_string($oStatement))
+			$oStatement = $this->prepare($oStatement);
+
+		$aResult = array();
+		if($this->res($oStatement, $aBindings))
+            $aResult = $oStatement->fetch(PDO::FETCH_NUM);
+
+        return is_array($aResult) && count($aResult) ? $aResult[$iIndex] : false;
+    }
+
+    /**
+     * execute sql query and return one row result
+     */
+    function getRow($oStatement, $aBindings = array(), $iFetchType = PDO::FETCH_ASSOC)
+    {
+    	$aResult = array();
+        if(!$oStatement)
+            return $aResult;
+		else if(!($oStatement instanceof PDOStatement) && is_string($oStatement))
+			$oStatement = $this->prepare($oStatement);
+
+        if(!in_array($iFetchType, array(PDO::FETCH_NUM, PDO::FETCH_ASSOC, PDO::FETCH_BOTH)))
+            $iFetchType = $this->_iPdoFetchType;
+
+        if(!$this->res($oStatement, $aBindings))
+        	return array();
+
+		$aResult = $oStatement->fetch($iFetchType);
+		if($aResult === false)
+        	return array();
+
+        return $aResult;
+    }
+
+    /**
+     * execute sql query and return a column as result
+     */
+    function getColumn($oStatement, $aBindings = array(), $iFetchColumnNumber = 0)
+    {
+    	$aResult = array();
+        if(!$oStatement)
+            return $aResult;
+		else if(!($oStatement instanceof PDOStatement) && is_string($oStatement))
+			$oStatement = $this->prepare($oStatement);
+
+        if(!$this->res($oStatement, $aBindings))
+        	return array();
+
+		$aResult = $oStatement->fetchAll(PDO::FETCH_COLUMN, $iFetchColumnNumber);
+		if($aResult === false)
+        	return array();
+
+        return $aResult;
+    }
+
+	/**
+     * execute sql query and return the first row of result
+     * and keep $array type and poiter to all data
+     */
+    public function getFirstRow($oStatement, $aBindings = array(), $iFetchType = PDO::FETCH_ASSOC)
+    {
+        if(!$oStatement)
+            return array();
+		else if(!($oStatement instanceof PDOStatement) && is_string($oStatement))
+			$oStatement = $this->prepare($oStatement);
+
+        if(!in_array($iFetchType, array(PDO::FETCH_NUM, PDO::FETCH_ASSOC, PDO::FETCH_BOTH)))
+            $iFetchType = $this->_iPdoFetchType;
+
+        if(!$this->res($oStatement, $aBindings)) 
+        	return array();
+
+        $aResult = $oStatement->fetch($iFetchType);
+        if($aResult === false)
+        	return array();
+
+		$this->_oStatement = $oStatement;
+        return $aResult;
+    }
+
+    /**
+     * return next row of pointed last getFirstRow calling data
+     */
+    public function getNextRow($iFetchType = PDO::FETCH_ASSOC)
+    {
+    	if(!$this->_oStatement)
+            return array();
+
+		if(!in_array($iFetchType, array(PDO::FETCH_NUM, PDO::FETCH_ASSOC, PDO::FETCH_BOTH)))
+            $iFetchType = $this->_iPdoFetchType;
+
+		$aResult = $this->_oStatement->fetch($iFetchType);
+		if($aResult !== false)
+			return $aResult;
+
+		$this->_oStatement = null;
+    	return array();
+    }
+
+	/**
+     * execute sql query and return table of records as result
+     */
+    public function getAll($oStatement, $aBindings = array(), $iFetchType = PDO::FETCH_ASSOC)
+    {
+    	$aResult = array();
+        if(!$oStatement)
+            return $aResult;
+		else if(!($oStatement instanceof PDOStatement) && is_string($oStatement))
+			$oStatement = $this->prepare($oStatement);
+
+        if(!in_array($iFetchType, array(PDO::FETCH_NUM, PDO::FETCH_ASSOC, PDO::FETCH_BOTH)))
+            $iFetchType = $this->_iPdoFetchType;
+
+        if(!$this->res($oStatement, $aBindings))
+        	return array();
+
+		$aResult = $oStatement->fetchAll($iFetchType);
+		if($aResult === false)
+        	return array();
+
+        return $aResult;
+    }
+
+    /**
+     * Executes sql query and returns table of records as result.
+     * 
+     * @deprecated use getAll instead.
+     */
+    public function fillArray($oStatement, $aBindings = array(), $iFetchType = PDO::FETCH_ASSOC)
+    {
+    	return $this->getAll($oStatement, $aBindings, $iFetchType);
+    }
+
+	/**
+     * execute sql query and return table of records as result
+     */
+    public function getAllWithKey($oStatement, $sFieldKey, $aBindings = array(), $iFetchType = PDO::FETCH_ASSOC)
+    {
+    	$aResult = array();
+        if(!$oStatement)
+            return $aResult;
+		else if(!($oStatement instanceof PDOStatement) && is_string($oStatement))
+			$oStatement = $this->prepare($oStatement);
+
+        $aRow = $this->getFirstRow($oStatement, $aBindings, $iFetchType);
+        while(!empty($aRow)) {
+        	$aResult[$aRow[$sFieldKey]] = $aRow;
+
+        	$aRow = $this->getNextRow($iFetchType);
+        }
+
+        return $aResult;
+    }
+
+    /**
+     * execute sql query and return table of records as result
+     */
+    public function getPairs($oStatement, $sFieldKey, $sFieldValue, $aBindings = array(), $iFetchType = PDO::FETCH_ASSOC)
+    {
+    	$aResult = array();
+        if(!$oStatement)
+            return $aResult;
+		else if(!($oStatement instanceof PDOStatement) && is_string($oStatement))
+			$oStatement = $this->prepare($oStatement);
+
+        $aRow = $this->getFirstRow($oStatement, $aBindings, $iFetchType);
+        while(!empty($aRow)) {
+        	$aResult[$aRow[$sFieldKey]] = $aRow[$sFieldValue];
+
+        	$aRow = $this->getNextRow($iFetchType);
+        }
+
+        return $aResult;
+    }
+
+    /**
+     * return number of affected rows in current mysql result
+     * 
+     * NOTE: PDOStatement::rowCount works for SELECT queries in MySQL.
+     * So, this method should be rewritten if the other DB engine will be used.
+     */
+    public function getNumRows($oStatement = null)
+    {
+    	if($oStatement && ($oStatement instanceof PDOStatement))
+    		return $oStatement->rowCount();
+
+    	if($this->_oStatement && ($this->_oStatement instanceof PDOStatement))
+    		return $this->_oStatement->rowCount();
+
+    	return 0;
+    }
+
+    /**
+     * returns the number of rows affected by the last DELETE, INSERT, or UPDATE statement. 
+     */
+    public function getAffectedRows($oStatement = null)
+    {
+        return $this->getNumRows($oStatement);
+    }
+
+    public function lastId()
+    {
+        return self::$_rLink->lastInsertId();
+    }
+
+    /**
+     * execute any query return number of rows affected/false
+     */
+    public function query($oStatement, $aBindings = array(), $bVerbose = null)
+    {
+    	if(!$oStatement)
+            return false;
+		else if(!($oStatement instanceof PDOStatement) && is_string($oStatement))
+			$oStatement = $this->prepare($oStatement);
+
+        if($this->res($oStatement, $aBindings, $bVerbose))
+            return $oStatement->rowCount();
+
+        return false;
+    }
+
+    /**
+     * execute any query
+     */
+    public function res($oStatement, $aBindings = array(), $bVerbose = null)
+    {
+		if(!$oStatement || !($oStatement instanceof PDOStatement))
+            return false;
+
+		if($oStatement->errorCode() == BX_PDO_STATE_SUCCESS)
+			return true;
+
+        if(isset($GLOBALS['bx_profiler']))
+        	$GLOBALS['bx_profiler']->beginQuery($oStatement->queryString);
+
+        for ($iAttempts = 3; $iAttempts > 0; --$iAttempts) {
+
+            $oException = null;
+            $iErrorCode = 0;
+            $bResult = false;
+            try {
+                $bResult = $this->executeStatement($oStatement, $aBindings, $bVerbose, $iErrorCode);
+            } catch (PDOException $oException) {
+                $iErrorCode = $oException->errorInfo[1];
+            }
+
+            // if deadlock timeout - try again, make sure that deadlock timeout is short
+            if (!$bResult && (1205 == $iErrorCode || 1213 == $iErrorCode)) {
+                continue;
+            }
+
+	    	// if mysql connection is lost - reconnect and try again
+            if(!$bResult && (2006 == $iErrorCode || !$this->ping())) {
+                $this->disconnect();
+                $this->connect();
+                continue;
+            }
+
+            // in all other cases exit from the loop
+            if ($oException)
+                throw $oException;
+            break;
+        }
+
+        if(isset($GLOBALS['bx_profiler']))
+        	$GLOBALS['bx_profiler']->endQuery($oStatement);
+
+		//is needed for SILENT mode
+		if(!$bResult && !empty($this->_aError))
+			$this->error($this->_aError);
+
+        return $bResult;
+    }
+
+    /**
+     * get mysql server info
+     */
+    public function getServerInfo()
+    {
+    	return self::$_rLink->getAttribute(PDO::ATTR_SERVER_VERSION);
+    }
+
+    /**
+     * get mysql version
+     */
+    public function getVersion()
+    {
+        $s = $this->getOne("SELECT VERSION()");
+        return preg_match("/([0-9\.]+)/", $s, $m) ? $m[1] : false;
+    }
+    
+    /**
+     * get list of tables in database
+     */
+    public function listTables()
+    {
+    	$oStatement = $this->pdoQuery("SHOW TABLES FROM `" . BX_DATABASE_NAME . "`");
+
+        return $this->getColumn($oStatement);
+    }
+
+    public function getFields($sTable)
+    {
+    	$oStatement = $this->pdoQuery("SHOW COLUMNS FROM `" . $sTable . "`");
+    	$aFields = $this->getAll($oStatement);
+
+        $aResult = array('original' => array(), 'uppercase' => array());
+        foreach($aFields as $aField) {
+            $aResult['original'][] = $aField['Field'];
+            $aResult['uppercase'][] = strtoupper($aField['Field']);
+        }
+
+        return $aResult;
+    }
+
+    public function fetchField($oStatement, $iField, $aBindings = array())
+    {
+        $aResult = array();
+        if(!$oStatement)
+            return $aResult;
+
+        else if(!($oStatement instanceof PDOStatement) && is_string($oStatement)) {
+            $oStatement = $this->prepare($oStatement);
+            if(!$this->res($oStatement, $aBindings))
+                return $aResult;
+        }
+
+        return $oStatement->getColumnMeta($iField);
+    }
+
+    public function isTableExists($sTable)
+    {
+        $aTableNames = $this->listTables();
+        foreach($aTableNames as $iKey => $sTableName)
+            $aTableNames[$iKey] = strtoupper($sTableName);
+
+        return in_array(strtoupper($sTable), $aTableNames);
+    }
+
+    public function isFieldExists($sTable, $sFieldName)
+    {
+        $aFields = $this->getFields($sTable);
+        return in_array(strtoupper($sFieldName), $aFields['uppercase']);
+    }
+
+	public function isIndexExists($sTable, $sIndexName)
+	{
+		$bIndex = false;
+
+        $aIndexes = $this->getAll("SHOW INDEXES FROM `" . $sTable . "`");
+        foreach($aIndexes as $aIndex)
+			if($aIndex['Key_name'] == $sIndexName) {
+				$bIndex = true;
+				break;
+			}
+
+		return $bIndex;
+	}
+
+    public function error($aError)
+    {
+    	$sErrorType = self::$_aErrors[$aError['code']];
+
+    	$bVerbose = isset($aError['verbose']) ? (bool)$aError['verbose'] : $this->_bErrorChecking;
+        if(!$bVerbose) {
+            $this->log($sErrorType . ': ' . $aError['message']);
+            if (!defined('BX_DOL_INSTALL')) // this is needed to display error during installation
+    			return;
+        }
+
+        if((defined('BX_DB_FULL_VISUAL_PROCESSING') && BX_DB_FULL_VISUAL_PROCESSING) || defined('BX_DOL_INSTALL')) {
+            $sOutput = '<div style="border:2px solid red;padding:4px;width:600px;margin:0px auto;">';
+            $sOutput .= '<div style="text-align:center;background-color:red;color:white;font-weight:bold;">Error</div>';
+            $sOutput .= '<div style="text-align:center;">' . $sErrorType . '</div>';
+            if((defined('BX_DB_FULL_DEBUG_MODE') && BX_DB_FULL_DEBUG_MODE) || defined('BX_DOL_INSTALL'))
+				$sOutput .= $this->errorOutput($aError);
+            $sOutput .= '</div>';
+        } 
+
+        if(defined('BX_DB_DO_EMAIL_ERROR_REPORT') && BX_DB_DO_EMAIL_ERROR_REPORT) {
+            $sSiteTitle = $this->getParam('site_title');
+
+            $sMailBody = "Database error in " . $sSiteTitle . "<br /><br /> \n";
+            $sMailBody .= $this->errorOutput($aError);
+            $sMailBody .= "<hr />Auto-report system";
+
+            sendMail($this->getParam('site_email'), "Database error in " . $sSiteTitle, $sMailBody, 0, array(), BX_EMAIL_SYSTEM, 'html', true);
+        }
+
+        bx_log('sys_db', "$sErrorType\n" . 
+            (empty($aError['message']) ? '' : "  Error: {$aError['message']}\n") . 
+            (empty($aError['query']) ? '' : "  Query: {$aError['query']}\n") . 
+            (!function_exists('getLoggedId') || !getLoggedId() ? '' : "  Account ID: " . getLoggedId() . "\n")
+        );
+
+        bx_show_service_unavailable_error_and_exit($sOutput);
+    }
+
+    protected function isParamInCache($sKey)
+    {
+        return is_array(self::$_aParams) && isset(self::$_aParams[$sKey]);
+    }
+
+    public function cacheParams($bForceCacheInvalidate = false, $bForceCacheInvalidateMixed = false)
+    {
+        if ($bForceCacheInvalidate)
+            $this->cacheParamsClear();
+
+        self::$_aParams = $this->fromCache(self::$_sParamsCacheName, 'getPairs', "SELECT `name`, `value` FROM `sys_options`", "name", "value");
+
+        list($sTmplCode, $sTmplName) = BxDolTemplate::retrieveCode();
+        if(!empty($sTmplCode) && !empty($sTmplName)) {
+            $iTmplMix = 0;
+            if(is_array($sTmplCode))
+                list($sTmplCode, $iTmplMix) = $sTmplCode;
+
+            if(empty($iTmplMix))
+                $iTmplMix = (int)$this->getParam($sTmplName . '_default_mix');
+
+            if(!empty($iTmplMix)) {
+                $sCacheNameMixed = self::$_sParamsCacheNameMixed . $sTmplCode .  '_' . $iTmplMix;
+                if($bForceCacheInvalidateMixed)
+                    $this->cacheParamsClear($sCacheNameMixed);
+
+                $aMixed = $this->fromCache($sCacheNameMixed, 'getPairs', "SELECT `option`, `value` FROM `sys_options_mixes2options` WHERE `mix_id`=:mix_id", "option", "value", array(
+                    'mix_id' => $iTmplMix
+                ));
+
+                if(!empty($aMixed) && is_array($aMixed))
+                    self::$_aParams = array_merge(self::$_aParams, $aMixed);
+            }
+        }
+
+        if (empty(self::$_aParams)) {
+            self::$_aParams = array();
+
+            return false;
+        }
+
+        return true;
+    }
+
+    public function cacheParamsClear($sCacheName = '')
+    {
+        if(empty($sCacheName))
+            $sCacheName = self::$_sParamsCacheName;
+
+        return $this->cleanCache($sCacheName);
+    }
+
+    public function isParam($sKey, $bFromCache = true)
+    {
+        if ($bFromCache && $this->isParamInCache($sKey))
+           return true;
+
+        $sQuery = $this->prepare("SELECT `name` FROM `sys_options` WHERE `name` = ? LIMIT 1", $sKey);
+        return $this->getOne($sQuery) == $sKey;
+    }
+
+    public function addParam($sName, $sValue, $iKateg, $sDesc, $sType)
+    {
+        $sQuery = $this->prepare("INSERT INTO `sys_options` SET `category_id` = ?, `name` = ?, `caption` = ?, `value` = ?, `type` = ?", $iKateg, $sName, $sDesc, $sValue, $sType);
+        $this->query($sQuery);
+
+        // renew params cache
+        $this->cacheParams(true);
+    }
+
+    public function getParam($sKey, $bFromCache = true)
+    {
+        if (!$sKey)
+            return false;
+
+        if ($bFromCache && $this->isParamInCache($sKey)) {
+            return self::$_aParams[$sKey];
+        } else {
+            $sQuery = $this->prepare("SELECT `tmo`.`value` AS `value` FROM `sys_options_mixes2options` AS `tmo` INNER JOIN `sys_options_mixes` AS `tm` ON `tmo`.`mix_id`=`tm`.`id` AND `tm`.`active`='1' WHERE `tmo`.`option`=? LIMIT 1", $sKey);
+            $mixedValue = $this->getOne($sQuery);
+            if($mixedValue !== false)
+                return $mixedValue;
+
+            $sQuery = $this->prepare("SELECT `value` FROM `sys_options` WHERE `name` = ? LIMIT 1", $sKey);
+            return $this->getOne($sQuery);
+        }
+    }
+
+    public function setParam($sKey, $mixedValue, $iMixId = 0)
+    {
+    	if(empty($iMixId))
+            $sQuery = $this->prepare("UPDATE `sys_options` SET `value` = ? WHERE `name` = ? LIMIT 1", $mixedValue, $sKey);
+        else
+            $sQuery = $this->prepare("REPLACE INTO `sys_options_mixes2options` SET `option` = ?, `mix_id` = ?, `value` = ?", $sKey, $iMixId, $mixedValue);
+
+        $bResult = (int)$this->query($sQuery) > 0;
+
+        // renew params cache
+        $bResult &= $this->cacheParams(true, !empty($iMixId));
+
+        return $bResult;
+    }
+
+    public function getParamsMix($iId)
+    {
+        return $this->fromCache(self::$_sParamsCacheNameMix . $iId, 'getRow', 'SELECT * FROM `sys_options_mixes` WHERE `id`=:id', array(
+            'id' => $iId
+        ));
+    }
+
+    public function getParamsMixActive($sType)
+    {
+        return $this->getRow("SELECT * FROM `sys_options_mixes` WHERE `type`=:type AND `active`='1'", array(
+            'type' => $sType
+        ));
+    }
+
+    public function getParamsMixes($sType, $mixedPublished = false)
+    {
+        $aBindings = array('type' => $sType);
+
+        $sQuery = "SELECT * FROM `sys_options_mixes` WHERE `type`=:type";
+        if($mixedPublished !== false) {
+            $aBindings['published'] = (int)$mixedPublished;
+
+            $sQuery .= " AND `published`=:published";
+        }
+
+        return $this->getAll($sQuery, $aBindings);
+    }
+
+    public function setTimezone($sTimezone)
+    {
+        $oDate = new DateTime('now', new DateTimeZone($sTimezone));
+        return $this->pdoExec('SET time_zone = "' . $oDate->format('P') . '"') !== false;
+    }
+
+    public function getEncoding()
+    {
+    	$oStatement = $this->pdoQuery('SELECT @@character_set_database');
+    	if($oStatement !== false)
+    		return $this->getOne($oStatement);
+
+    	return false;
+    }
+
+    public function setErrorChecking ($b)
+    {
+        $this->_bErrorChecking = $b;
+    }
+
+    /**
+     * Cache functions.
+     */
+    public function getDbCacheObject ()
+    {
+        if($this->_oDbCacheObject != null)
+			return $this->_oDbCacheObject;
+
+		$sEngine = $this->getParam('sys_db_cache_engine');
+		$this->_oDbCacheObject = bx_instance('BxDolCache'.$sEngine);
+		if(!$this->_oDbCacheObject->isAvailable())
+			$this->_oDbCacheObject = bx_instance('BxDolCacheFile');
+
+		return $this->_oDbCacheObject;
+    }
+
+    public function genDbCacheKey ($sName)
+    {
+        return 'db_' . $sName . '_' . bx_site_hash() . '.php';
+    }
+
+    public function getCache ($sName, $sFunc)
+    {
+        if (!$this->getParam('sys_db_cache_enable'))
+            return false;
+        
+        $oCache = $this->getDbCacheObject ();
+        $sKey = $this->genDbCacheKey($sName);
+        return $oCache->getData($sKey);
+    }
+    
+    public function setCache ($sName, $mData)
+    {
+        if (!$this->getParam('sys_db_cache_enable'))
+            return false;
+        
+        $oCache = $this->getDbCacheObject ();
+        $sKey = $this->genDbCacheKey($sName);
+        $oCache->setData($sKey, $mData);
+    }
+    
+    public function fromCache ($sName, $sFunc)
+    {
+        $aArgs = func_get_args();
+        array_shift ($aArgs); // shift $sName
+        array_shift ($aArgs); // shift $sFunc
+
+        if (!$this->getParam('sys_db_cache_enable'))
+            return call_user_func_array (array ($this, $sFunc), $aArgs); // pass other function parameters as database function parameters
+
+        $oCache = $this->getDbCacheObject ();
+
+        $sKey = $this->genDbCacheKey($sName);
+
+        $mixedRet = $oCache->getData($sKey);
+
+        if ($mixedRet !== null) {
+
+            return $mixedRet;
+
+        } else {
+
+            $mixedRet = call_user_func_array (array ($this, $sFunc), $aArgs); // pass other function parameters as database function parameters
+
+            $oCache->setData($sKey, $mixedRet);
+        }
+
+        return $mixedRet;
+    }
+
+    public function cleanCache ($sName)
+    {
+        $oCache = $this->getDbCacheObject();
+
+        $sKey = $this->genDbCacheKey($sName);
+
+        return $oCache->delData($sKey);
+    }
+
+    public function &fromMemory ($sName, $sFunc)
+    {
+        if(array_key_exists($sName, self::$_aDbCacheData) && !defined('BX_DOL_INSTALL') && !defined('BX_DOL_CRON_EXECUTE'))
+			return self::$_aDbCacheData[$sName];
+
+		$aArgs = func_get_args();
+		array_shift($aArgs); // shift $sName
+		array_shift($aArgs); // shift $sFunc
+		self::$_aDbCacheData[$sName] = call_user_func_array (array ($this, $sFunc), $aArgs); // pass other function parameters as database function parameters
+
+		return self::$_aDbCacheData[$sName];
+    }
+
+    public function cleanMemory ($sName)
+    {
+        if(!isset(self::$_aDbCacheData[$sName])) 
+        	return false;
+
+		unset(self::$_aDbCacheData[$sName]);
+		return true;
+    }
+
+    public function cleanMemoryAll ()
+    {
+        self::$_aDbCacheData = array();
+		return true;
+    }
+
+    /**
+     * It escapes string to pass to mysql query.
+     * Try to use "prepare" function always (@see BxDolDb::prepare), use "escape" only if "prepare" function is not possible at all.
+     * Also consider using "implode_escape" function (@see BxDolDb::implode_escape).
+     *
+     * @param  string  $s string to escape
+     * @return escaped string whcich is ready to pass to SQL query.
+     */
+    public function escape($s)
+    {
+    	try {
+    		$s = self::$_rLink->quote($s);
+    	}
+    	catch (PDOException $oException) {
+    		$oException->errorInfo[self::$_sErrorKey] = array(
+    			'code' => BX_DB_ERR_ESCAPE,
+    			'message' => $oException->getMessage(),
+    			'trace' => $oException->getTrace()
+    		);
+
+    		throw $oException;
+    	}
+
+        return $s;
+    }
+
+    /**
+     * This function is usefull when you need to form array of parameters to pass to IN(...) SQL construction.
+     * Example:
+     * @code
+     * $a = array(2, 4.5, 'apple', 'car');
+     * $s = "SELECT * FROM `t` WHERE `a` IN (" . $oDb->implode_escape($a) . ")";
+     * echo $s; // outputs: SELECT * FROM `t` WHERE `a` IN (2, 4.5, 'apple', 'car')
+     * @endcode
+     *
+     * @param $mixed array or parameters or just one paramter
+     * @return string which is ready to pass to IN(...) SQL construction
+     */
+    public function implode_escape ($mixed)
+    {
+        if (is_array($mixed)) {
+            $s = '';
+            foreach ($mixed as $v)
+                $s .= (is_numeric($v) ? $v : $this->escape($v)) . ',';
+            if ($s)
+                return substr($s, 0, -1);
+            else
+                return 'NULL';
+        }
+
+        return is_numeric($mixed) ? $mixed : ($mixed ? $this->escape($mixed) : 'NULL');
+    }
+
+    /**
+     * @deprecated
+     */
+    public function unescape ($mixed)
+    {
+        if(is_array($mixed)) {
+            foreach($mixed as $k => $v)
+				$mixed[$k] = $this->getOne("SELECT '$v'");
+
+            return $mixed;
+        } 
+        else
+            return $this->getOne("SELECT '$mixed'");
+    }
+
+    /**
+     * Prepare SQL query before execution if some arguments are need to be passed to it.
+     * All parameters marked with question (?) symbol in SQL query are replaced with parameters passed after SQL query parameter.
+     * Parameters are properly excaped and surrounded by qutes if needed.
+     * Example:
+     * @code
+     * $sSql = $oDb->prepare("SELECT `a`, `b` from `t` WHERE `c` = ? and `d` = ?", 12, 'aa');
+     * echo $sSql;// outputs: SELECT `a`, `b` from `t` WHERE `c` = 12 and `d` = 'aa'
+     * $a = $oDb->getAll($sSql);
+     * @endcode
+     *
+     * @param  string $sQuery SQL query, parameters for replacing are marked with ? symbol
+     * @param  mixed  $mixed  any number if parameters to replace, number of parameters whould match number of ? symbols in SQL query
+     * @return PDOStatement object with SQL query ready for execution
+     */
+    public function prepare($sQuery)
+    {
+    	if(!self::$_rLink)
+    		return false;
+
+        if (self::$_bMultuServersMode && (0 != strncasecmp(ltrim($sQuery, " \t\n\r(\0\x0B"), 'SELECT', 6)))
+            $this->setReadOnlyMode(false);
+
+        $aArgs = func_get_args();
+        $sQuery = array_shift($aArgs);
+
+        $oStatement = self::$_rLink->prepare($sQuery);
+
+        $iIndex = 1;
+        foreach($aArgs as $mixedArg) {
+        	if(is_null($mixedArg))
+				$iValueType = PDO::PARAM_NULL;
+            else if(is_numeric($mixedArg) && is_int($mixedArg))
+                $iValueType = PDO::PARAM_INT;
+            else
+                $iValueType = PDO::PARAM_STR;
+
+        	$oStatement->bindValue($iIndex++, $mixedArg, $iValueType);
+        }
+
+        return $oStatement;
+    }
+
+    /**
+     * Prepare SQL query before execution if some arguments are need to be passed to it.
+     * All parameters marked with question (?) symbol in SQL query are replaced with parameters passed after SQL query parameter.
+     * Parameters are properly excaped and surrounded by qutes if needed.
+     * Example:
+     * @code
+     * $sSql = $oDb->prepare("SELECT `a`, `b` from `t` WHERE `c` = ? and `d` = ?", 12, 'aa');
+     * echo $sSql;// outputs: SELECT `a`, `b` from `t` WHERE `c` = 12 and `d` = 'aa'
+     * $a = $oDb->getAll($sSql);
+     * @endcode
+     *
+     * @param  string $sQuery SQL query, parameters for replacing are marked with ? symbol
+     * @param  mixed  $mixed  any number if parameters to replace, number of parameters whould match number of ? symbols in SQL query
+     * @return string with SQL query. 
+     */
+    function prepareAsString($sQuery)
+    {
+        $aArgs = func_get_args();
+        $sQuery = array_shift($aArgs);
+
+        $iPos = 0;
+        foreach ($aArgs as $mixedArg) {
+            if (is_null($mixedArg))
+                $s = 'NULL';
+            elseif (is_numeric($mixedArg))
+                $s = $mixedArg;
+            else
+                $s = $this->escape($mixedArg);
+
+            $i = bx_mb_strpos($sQuery, '?', $iPos);
+            $sQuery = bx_mb_substr_replace($sQuery, $s, $i, 1);
+            $iPos = $i + get_mb_len($s);
+        }
+
+        return $sQuery;
+    }
+
+    /**
+     * Convert array of key => values to SQL query.
+     * Array keys are field names and array values are field values.
+     * @param $a array
+     * @param $sDiv fields separator, by default it is ',', another useful value is ' AND '
+     * @param $sOperator operator for comparision: '=' or 'LIKE'
+     * @param $bWildcardSpaceChars substiture any space and dash characters with single wildcard symbol
+     * @return part of SQL query string
+     */
+    public function arrayToSQL($a, $sDiv = ',', $sOperator = '=', $bWildcardSpaceChars = false)
+    {
+        $aOperators = ['=', 'LIKE', 'like'];
+        $sOperator = in_array($sOperator, $aOperators) ? $sOperator : '=';
+        $s = '';
+        foreach($a as $k => $v) {
+            if ($bWildcardSpaceChars)
+                $v = preg_replace('/[\p{Zs}\p{Cc}\p{Pd}]/', '_', $v);
+            $s .= "`{$k}` {$sOperator} " . $this->escape($v) . $sDiv;
+        }
+        return trim($s, $sDiv);
+    }
+
+    protected function log($s)
+    {
+        if (defined('BX_DIRECTORY_PATH_LOGS')) {
+            $sPath = BX_DIRECTORY_PATH_LOGS;
+        }
+        else {
+            $sDirName = pathinfo(__FILE__, PATHINFO_DIRNAME);
+            $sPath = $sDirName . '/../../logs/';
+        }
+        return file_put_contents($sPath . 'db.err.log', date('Y-m-d H:i:s') . "\t" . $s . "\n", FILE_APPEND);
+    }
+
+    public function executeSQL($sPath, $aReplace = array (), $isBreakOnError = true)
+    {
+        if(!file_exists($sPath) || !($rHandler = fopen($sPath, "r")))
+            return array(array ('query' => "fopen($sPath, 'r')", 'error' => 'file not found or permission denied'));
+
+		self::$_rLink->setAttribute(PDO::ATTR_ERRMODE, BX_DB_MODE_SILENT);
+
+        $sQuery = "";
+        $sDelimiter = ';';
+        $aResult = array();
+        while(!feof($rHandler)) {
+            $sStr = trim(fgets($rHandler));
+
+            if(empty($sStr) || $sStr[0] == "" || $sStr[0] == "#" || ($sStr[0] == "-" && $sStr[1] == "-"))
+                continue;
+
+            //--- Change delimiter ---//
+            if(strpos($sStr, "DELIMITER //") !== false || strpos($sStr, "DELIMITER ;") !== false) {
+                $sDelimiter = trim(str_replace('DELIMITER', '', $sStr));
+                continue;
+            }
+
+            $sQuery .= $sStr;
+
+            //--- Check for multiline query ---//
+            if(substr($sStr, -strlen($sDelimiter)) != $sDelimiter)
+                continue;
+
+            //--- Execute query ---//
+            if ($aReplace)
+                $sQuery = str_replace($aReplace['from'], $aReplace['to'], $sQuery);
+            if($sDelimiter != ';')
+                $sQuery = str_replace($sDelimiter, "", $sQuery);
+
+            if($this->query(trim($sQuery), array(), false) === false) {
+                $aResult[] = array('query' => $sQuery, 'error' => $this->_aError['message']);
+                if ($isBreakOnError)
+                    break;
+            }
+
+            $sQuery = "";
+        }
+        fclose($rHandler);
+
+        self::$_rLink->setAttribute(PDO::ATTR_ERRMODE, $this->_iPdoErrorMode);
+
+        return empty($aResult) ? true : $aResult;
+    }
+
+    protected function executeStatement($oStatement, $aBindings = array(), $bVerbose = null, &$iErrorCode = null)
+    {
+        $bResult = false;
+
+        if (!isset($aBindings[0])) {
+    		foreach($aBindings as $sKey => $mixedValue) {
+	    		if(is_null($mixedValue))
+		    		$oStatement->bindValue(":{$sKey}", $mixedValue, PDO::PARAM_NULL);
+                else if(is_numeric($mixedValue) && is_int($mixedValue))
+                    $oStatement->bindValue(":{$sKey}", $mixedValue, PDO::PARAM_INT);
+                else
+                    $oStatement->bindValue(":{$sKey}", $mixedValue, PDO::PARAM_STR);
+            }
+            $aBindings = array();
+        }
+
+    	switch (self::$_rLink->getAttribute(PDO::ATTR_ERRMODE)) {
+    		case PDO::ERRMODE_SILENT:
+    			$bResult = $this->executeStatementSilent($oStatement, $aBindings, $bVerbose);
+    			break;
+
+    		case PDO::ERRMODE_EXCEPTION:
+    			$bResult = $this->executeStatementException($oStatement, $aBindings, $bVerbose);
+    			break;
+    	}
+
+        if (!$bResult && !is_null($iErrorCode)) {
+            $aError = $oStatement->errorInfo();
+            $iErrorCode = $aError[1];
+        }
+
+    	return $bResult;
+    }
+
+    protected function executeStatementException($oStatement, $aBindings = array(), $bVerbose = null)
+    {
+    	$bResult = false;
+
+    	try {
+			$bResult = $oStatement->execute(!empty($aBindings) && is_array($aBindings) ? $aBindings : null);
+		}
+		catch (PDOException $oException) {
+			$aError = $oStatement->errorInfo();
+
+			$oException->errorInfo[self::$_sErrorKey] = array(
+				'code' => BX_DB_ERR_QUERY_ERROR,
+				'message' => !empty($aError[2]) ? $aError[2] : $oException->getMessage(),
+				'query' => $oStatement->queryString,
+				'trace' => $oException->getTrace(),
+				'verbose' => $bVerbose
+			);
+
+			throw $oException;
+		}
+
+		return $bResult;
+    }
+
+    protected function executeStatementSilent($oStatement, $aBindings = array(), $bVerbose = null)
+    {
+    	$bResult = $oStatement->execute(!empty($aBindings) && is_array($aBindings) ? $aBindings : null);
+    	if($bResult)
+    		return true;
+
+		$aError = $oStatement->errorInfo();
+
+        $aTrace = debug_backtrace();
+        unset($aTrace[0]);
+
+		$this->_aError = array(
+			'code' => BX_DB_ERR_QUERY_ERROR,
+			'message' => !empty($aError[2]) ? $aError[2] : '',
+			'query' => $oStatement->queryString,
+			'trace' => $aTrace,
+			'verbose' => $bVerbose
+		);
+
+		return false;
+    }
+
+	protected function errorOutput($aError)
+    {
+		$aErrorLocation = array();
+
+        if(!empty($aError['query']) && !empty($aError['trace']))
+            foreach($aError['trace'] as $aCall )
+                if(isset($aCall['args']) && is_array($aCall['args']))
+                    foreach($aCall['args'] as $argNum => $argVal)
+                        if((is_string($argVal) && strcmp($argVal, $aError['query']) == 0) || ($argVal instanceof PDOStatement && strcmp($argVal->queryString, $aError['query']) == 0)) {
+                            $aErrorLocation['file'] = isset($aCall['file']) ? $aCall['file'] : (isset($aCall['class']) ? 'class: ' . $aCall['class'] : 'undefined');
+                            $aErrorLocation['line'] = isset($aCall['line']) ? $aCall['line'] : 'undefined';
+                            $aErrorLocation['function'] = $aCall['function'];
+                            $aErrorLocation['arg'] = $argNum;
+                        }
+
+        $sOutput = '';
+        
+        if(!empty($aError['query']))
+            $sOutput .= '<p><b>Query:</b><br />' . bx_process_output($aError['query']) . '</p>';
+
+        if(!empty($aError['message']))
+            $sOutput .= '<p><b>Mysql error:</b><br />' . $aError['message'] . '</p>';
+
+		if(!empty($aErrorLocation))
+			$sOutput .= '<p><b>Location:</b><br />The error was found in <b>' . $aErrorLocation['function'] . '</b> function in the file <b>' . $aErrorLocation['file'] . '</b> at line <b>' . $aErrorLocation['line'] . '</b>.</p>';
+
+        $sOutput .= '<p><b>collation_connection:</b><br />' . $this->getOne("SELECT @@collation_connection") . '</p>';
+        
+		if(!empty($aError['trace'])) {
+            $sBackTrace = print_r($aError['trace'], true);
+            if (defined ('BX_DATABASE_USER') && !is_array(BX_DATABASE_USER))
+                $sBackTrace = str_replace('[_sUser:protected] => ' . BX_DATABASE_USER, '[_sUser:protected] => *****', $sBackTrace);
+            if (defined ('BX_DATABASE_PASS') && !is_array(BX_DATABASE_PASS))
+                $sBackTrace = str_replace('[_sPassword:protected] => ' . BX_DATABASE_PASS, '[_sPassword:protected] => *****', $sBackTrace);
+
+			$sOutput .= '<div><b>Debug backtrace:</b></div><div style="overflow:scroll;height:300px;border:1px solid gray;"><pre>' . htmlspecialchars_adv($sBackTrace) . '</pre></div>';
+		}
+
+		if(!empty(self::$_aParams)) {
+			$sSettings = var_export(self::$_aParams, true);
+
+			$sOutput .= '<div><b>Settings:</b></div><div style="overflow:scroll;height:300px;border:1px solid gray;"><pre>' . htmlspecialchars_adv($sSettings) . '</pre></div>';
+		}
+
+		$sOutput .= '<p><b>Called script:</b><br />' . $_SERVER['PHP_SELF'] . '</p>';
+
+		if(!empty($_REQUEST)) {
+			$sRequest = var_export($_REQUEST, true);
+
+			$sOutput .= '<p><b>Request parameters:</b><br /><pre>' . htmlspecialchars_adv($sRequest) . '</pre></p>';
+		}
+
+        return $sOutput;
+    }
+}
+
+/**
+ * Create the very first instance and initiate connetion to database.
+ */
+BxDolDb::getInstance();
+
+function getParam($sParamName, $bUseCache = true)
+{
+    $oObj = BxDolDb::getInstance();
+    if ($oObj === null)
+        return false;
+       
+     return $oObj->getParam($sParamName, $bUseCache);
+}
+
+function setParam($sParamName, $sParamVal)
+{
+    $oObj = BxDolDb::getInstance();
+    if ($oObj === null)
+        return false;
+    
+    return $oObj->setParam($sParamName, $sParamVal);
+}
+
+/** @} */

+ 467 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolMenu.php

@@ -0,0 +1,467 @@
+<?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
+ * @{
+ */
+
+/**
+ * Menus.
+ *
+ * Menu uses some set of items and the template to display it,
+ * so it is possible to have several menus which uses the same set of items but different templates.
+ *
+ * Menu is any set of some links or actions, for example menu can be links in site's footer or actions in profile view.
+ *
+ *
+ * @section menu_create Creating the Menu object:
+ *
+ * 1. Add record to 'sys_objects_menu' table:
+ *
+ * - object: name of the menu object, in the format: vendor prefix, underscore, module prefix, underscore, internal identifier or nothing; for example: bx_groups_actions - actions menu in group view.
+ * - title: name of the menu, displayed in the studio menu builder.
+ * - set_name: name of items' set.
+ * - module: the module this menu belongs to.
+ * - template_id: the template to use for menu displaying, this is id from 'sys_menu_templates' table.
+ * - deletable: it determines if menu can be deleted from the studio menu builder.
+ * - active: it is possible to disable particular menu, then it will not be displayed.
+ * - override_class_name: user defined class name which is derived from BxTemplMenu.
+ * - override_class_file: the location of the user defined class, leave it empty if class is located in system folders.
+ *
+ * Menu templates are stored in 'sys_menu_templates' table:
+ * - id: template id.
+ * - template: template file.
+ * - title: template title to display in the studio menu builder.
+ * All menu templates iterate through 'bx_repeat:menu_items' and use the following template variables for each menu item: __link__, __target__, __onclick__, __title__, __class_add__.
+ *
+ *
+ * 2. Add menu an empty menu set to 'sys_menu_sets' table (if you want to use new set of items for created menu):
+ * - set_name: the set name.
+ * - module: the module this set belongs to.
+ * - title: name of the set, displayed in studio menu builder.
+ * - deletable: it determines if the set can be deleted from menu builder.
+ *
+ *
+ * 3. Add menu items to the set by adding records to 'sys_menu_items' table:
+ * - set_name: the set name this item belogs to.
+ * - module: the module this item belongs to.
+ * - name: name of the item (not displayed to the end user), unique in the particular set.
+ * - title: menu item title to display to the end user, please note that some templates can still display menu as icons without text titles.
+ * - link: menu item URL.
+ * - onclick: menu item onclick event.
+ * - target: menu item target.
+ * - icon: menu item icon, please note that some templates can still display menu as text without icons.
+ * - addon: display additional data near menu item, only for supported menu templates, this is serialized array of service call parameters: module - module name, method - service method name, params - array of parameters.
+ * - markers: service method to provide additional replacement markers, this is serialized array of service call parameters: module - module name, method - service method name, params - array of parameters.
+ * - 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
+ * - active: it is possible to disable particular menu item, then it will not be displayed.
+ * - order: menu item order in the particular set.
+ *
+ *
+ * 4. Display Menu.
+ * Use the following sample code to display menu:
+ * @code
+ *     $oMenu = BxTemplMenu::getObjectInstance('sample_menu'); // 'sample_menu' is 'object' field from 'sys_objects_menu' table.
+ *     if ($oMenu)
+ *         echo $oMenu->getCode; // display menu
+ * @endcode
+ *
+ * But in most cases you don't need to use above code to display menu,
+ * menu objects are integrated into pages - there is special 'menu' page block type for it.
+ *
+ */
+class BxDolMenu extends BxDolFactory implements iBxDolFactoryObject, iBxDolReplaceable
+{
+    protected static $SEL_MODULE = '';
+    protected static $SEL_NAME = '';
+
+    protected $_bIsApi;
+
+    protected $_bDynamicMode;
+    protected $_bAddNoFollow;
+
+    protected $_sSelModule;
+    protected $_sSelName;
+
+    protected $_sObject;
+    protected $_aObject;
+    protected $_oQuery;
+    protected $_oPermalinks;
+    protected $_aMarkers = array();
+    protected $_bMultilevel = false;
+
+    protected $_sSessionKeyCollapsed;
+
+    /**
+     * Constructor
+     * @param $aObject array of menu options
+     */
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+
+        $this->_bIsApi = bx_is_api();
+
+        $this->_bDynamicMode = false;
+        $this->_bAddNoFollow = getParam('sys_add_nofollow') == 'on';
+
+        $this->_sObject = isset($aObject['object']) ? $aObject['object'] : 'bx-menu-obj-' . time() . rand(0, PHP_INT_MAX);
+        $this->_aObject = $aObject;
+        $this->_oQuery = new BxDolMenuQuery($this->_aObject);
+        $this->_oPermalinks = BxDolPermalinks::getInstance();
+
+        $this->_bMultilevel = !empty($this->_aObject['set_name']) && $this->_oQuery->isSetMultilevel($this->_aObject['set_name']);
+
+        $this->_sSessionKeyCollapsed = 'bx_menu_collapsed_';
+
+        $this->addMarkers([
+            'object' => $this->_sObject
+        ]);
+
+        if(isLogged() && ($oProfile = BxDolProfile::getInstance()) !== false) {
+            $this->addMarkers([
+                'member_id' => $oProfile->id(),
+                'member_display_name' => $oProfile->getDisplayName(),
+                'member_url' => $oProfile->getUrl(),
+                'content_id' => $oProfile->getContentId()
+            ]);
+        }
+    }
+
+    /**
+     * Get menu object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    static public function getObjectInstance($sObject, $oTemplate = false)
+    {
+        $oMenu = false;
+        if (!isset($GLOBALS['bxDolClasses']['BxDolMenu!'.$sObject])) {
+            $aObject = BxDolMenuQuery::getMenuObject($sObject);
+            if (!$aObject || !is_array($aObject) || (int)$aObject['active'] == 0)
+                return false;
+
+            $sClass = 'BxTemplMenu';
+            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']);
+            }
+
+            $oMenu = new $sClass($aObject, $oTemplate);
+            $GLOBALS['bxDolClasses']['BxDolMenu!'.$sObject] = $oMenu;
+        }
+        else
+            $oMenu = $GLOBALS['bxDolClasses']['BxDolMenu!'.$sObject];
+
+        bx_alert('system', 'get_object', 0, false, array(
+            'type' => 'menu',
+            'name' => $sObject,
+            'object' => &$oMenu,
+        ));
+
+        return $oMenu;
+    }
+
+    /**
+     * Set selected menu item globally.
+     * @param $sModule menu item module to set as selected
+     * @param $sName menu item name to set as selected
+     */
+    static public function setSelectedGlobal ($sModule, $sName)
+    {
+        self::$SEL_MODULE = $sModule;
+        self::$SEL_NAME = $sName;
+    }
+
+    /**
+     * Process menu triggers.
+     * Menu triggers allow to automatically add menu items to modules with no different if dependant module was install before or after the module menu item belongs to.
+     * For example module "Notes" adds menu items to all profiles modules (Persons, Organizations, etc)
+     * with no difference if persons module was installed before or after "Notes" module was installed.
+     * @param $sMenuTriggerName trigger name to process, usually specified in module installer class - @see BxBaseModGeneralInstaller
+     * @return always true, always success
+     */
+    static public function processMenuTrigger ($sMenuTriggerName)
+    {
+        // get list of active modules
+        $aModules = BxDolModuleQuery::getInstance()->getModulesBy(array(
+            'type' => 'modules',
+            'active' => 1,
+        ));
+
+        // get list of menu triggers
+        $aMenuItems = BxDolMenuQuery::getMenuTriggers($sMenuTriggerName);
+
+        // check each menu item trigger for all modules
+        foreach ($aMenuItems as $aMenuItem) {
+            foreach ($aModules as $aModule) {
+                if (!BxDolRequest::serviceExists($aModule['name'], 'get_menu_set_name_for_menu_trigger'))
+                    continue;
+
+                $mixedMenuSet = BxDolService::call($aModule['name'], 'get_menu_set_name_for_menu_trigger', array($sMenuTriggerName));
+                if(empty($mixedMenuSet))
+                    continue;
+
+                if(is_string($mixedMenuSet))
+                    $mixedMenuSet = array($mixedMenuSet);
+
+                foreach($mixedMenuSet as $sMenuSet) {
+                    if(empty($sMenuSet))
+                        continue;
+
+                    $aMenuItem['set_name'] = $sMenuSet;
+                    BxDolMenuQuery::addMenuItemToSet($aMenuItem);
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Check if the menu is visible. The menu is visible if at least one menu item is visible.
+     * @return boolean
+     */
+    public function isVisible()
+    {
+        if((int)$this->_aObject['active'] == 0)
+            return false;
+
+    	if(!isset($this->_aObject['menu_items']))
+			$this->_aObject['menu_items'] = $this->_oQuery->getMenuItems();
+
+    	$bVisible = false;
+    	foreach ($this->_aObject['menu_items'] as $a) {
+    		if((isset($a['active']) && !$a['active']) || (isset($a['visible_for_levels']) && !$this->_isVisible($a)))
+				continue;
+			
+			$bVisible = true;
+			break;
+    	}
+
+    	return $bVisible;
+    }
+
+    public function getTemplateId()
+    {
+        return $this->_aObject['template_id'];
+    }
+
+    /**
+     * Get template name with checking for custom template related to exactly this menu object.
+     * @return string with template name.
+     */
+    public function getTemplateName($sName = '')
+    {
+        if(empty($sName))
+            $sName = $this->_aObject['template'];
+
+        $sNameCustom = str_replace('.html', '_' . $this->_sObject . '.html', $sName);
+        return $this->_oTemplate->isHtml($sNameCustom) ? $sNameCustom : $sName;
+    }
+
+    public function setTemplateById ($iTemplateId)
+    {
+        $aTemplate = $this->_oQuery->getMenuTemplateById($iTemplateId);
+        if(empty($aTemplate) || !is_array($aTemplate))
+            return;
+
+        $this->_aObject['template'] = $aTemplate['template'];
+    }
+    
+    /**
+     * Set selected menu item for current menu object only.
+     * @param $sModule menu item module to set as selected
+     * @param $sName menu item name to set as selected
+     */
+    public function setSelected ($sModule, $sName)
+    {
+        $this->_sSelModule = $sModule;
+        $this->_sSelName = $sName;
+    }
+
+    public function setDynamicMode ($bDynamicMode)
+    {
+        $this->_bDynamicMode = $bDynamicMode;
+    }
+
+    /**
+     * Get an arrey of replacable markers.
+     * @return an array with markers
+     */
+    public function getMarkers()
+    {
+        return $this->_aMarkers;
+    }
+    
+    /**
+     * Add replace markers.
+     * @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;
+    }
+
+    /**
+     * Remove marker
+     * @param $s marker key
+     */
+    public function removeMarker ($s) 
+    {
+        unset($this->_aMarkers[$s]);
+    }
+
+    public function performActionSetCollapsed($mixedValue)
+    {
+        $this->_setCollapsed($this->_sObject, (int)$mixedValue);
+    }
+
+    public function performActionSetCollapsedSubmenu($sMenuItem, $mixedValue)
+    {
+        $this->_setCollapsed($this->_sObject . '_' . $sMenuItem, (int)$mixedValue);
+    }
+
+    public function getUserChoiceCollapsed($sObject = '')
+    {
+        $iProfile = bx_get_logged_profile_id();
+        if(!$iProfile)
+            return false;
+
+        if(!$sObject)
+            $sObject = $this->_sObject;
+
+        $sSessionKey = $this->_sSessionKeyCollapsed . $iProfile;
+        $aCollapsed = BxDolSession::getInstance()->getValue($sSessionKey);
+        if(!isset($aCollapsed[$sObject]))
+            return false;
+
+        return (int)$aCollapsed[$sObject];
+    }
+
+    public function getUserChoiceCollapsedSubmenu($mixedItem, $sObject = '')
+    {
+        if(!$sObject)
+            $sObject = $this->_sObject;
+
+        if(is_array($mixedItem) && isset($mixedItem['name']))
+            $sObject .= '_' . $mixedItem['name'];
+        else if(is_string($mixedItem))
+            $sObject .= '_' . $mixedItem;
+
+        return $this->getUserChoiceCollapsed($sObject);
+    }
+
+    protected function _setCollapsed($sName, $mixedValue)
+    {
+        $iProfile = bx_get_logged_profile_id();
+        if(!$iProfile)
+            return;
+
+        $oSession = BxDolSession::getInstance();
+        $sSessionKey = $this->_sSessionKeyCollapsed . $iProfile;
+
+        $aCollapsed = $oSession->getValue($sSessionKey);
+        if(!is_array($aCollapsed))
+            $aCollapsed = [];
+
+        $aCollapsed[$sName] = $mixedValue;
+        $oSession->setValue($sSessionKey, $aCollapsed);
+    }
+
+    /**
+     * Check if menu items is selected.
+     * @param $a menu item array
+     * @return boolean
+     */
+    protected function _isSelected ($a)
+    {
+        if ($this->_sSelModule || $this->_sSelName)
+            return (!isset($a['module']) || $a['module'] == $this->_sSelModule) && (isset($a['name']) && $a['name'] == $this->_sSelName) ? true : false;
+        return (!isset($a['module']) || $a['module'] == self::$SEL_MODULE) && (isset($a['name']) && $a['name'] == self::$SEL_NAME) ? true : false;
+    }
+
+    /**
+     * Check if menu items is visible.
+     * @param $a menu item array
+     * @return boolean
+     */
+    protected function _isVisible ($a)
+    {
+        if($this->_bIsApi && isset($a['hidden_on']) && ((1 << 3) & (int)$a['hidden_on']))
+            return false;
+
+        if(isset($a['visible_for_levels']) && !BxDolAcl::getInstance()->isMemberLevelInSet($a['visible_for_levels']))
+            return false;
+
+        if(!empty($a['visibility_custom']) && !BxDolService::callSerialized($a['visibility_custom'], $this->_aMarkers))
+            return false;
+
+        if($this->_iPageType && !empty($a['hidden_on_pt']) && ((1 << ($this->_iPageType - 2)) & (int)$a['hidden_on_pt']))
+            return false;
+
+        return true;
+    }
+    
+    protected function _getVisibilityClass($a)
+    {
+        $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'
+        );
+        
+        $aHiddenOnCol = array(
+            pow(2, 1) => 'bx-def-thin-col-hide',
+            pow(2, 2) => 'bx-def-half-col-hide',
+            pow(2, 3) => 'bx-def-wide-col-hide',
+            pow(2, 4) => 'bx-def-full-col-hide'
+        );
+        
+        $sHiddenOnCssClasses = '';
+        if(!empty($a['hidden_on']))
+            foreach($aHiddenOn as $iHiddenOn => $sClass)
+                if((int)$a['hidden_on'] & $iHiddenOn)
+                    $sHiddenOnCssClasses .= ' ' . $sClass;
+        
+        
+        if(!empty($a['hidden_on_col'])){    
+            foreach($aHiddenOnCol as $iHiddenOn => $sClass)
+                if((int)$a['hidden_on_col'] & $iHiddenOn)
+                    $sHiddenOnCssClasses .= ' ' . $sClass;
+        }
+        
+        return $sHiddenOnCssClasses;
+    }
+
+    /**
+     * Replace provided markers in menu item array, curently markers are replaced in title, link and onclick fields.
+     * @param $a menu item array
+     * @return array where markes are replaced with real values
+     */
+    protected function _replaceMarkers ($a)
+    {
+        if (empty($this->_aMarkers))
+            return $a;
+        $aReplacebleFields = array ('title', 'link', 'onclick');
+        foreach ($aReplacebleFields as $sField)
+            if (isset($a[$sField]))
+                $a[$sField] = bx_replace_markers($a[$sField], $this->_aMarkers);
+        return $a;
+    }
+
+}
+
+/** @} */

+ 451 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolObject.php

@@ -0,0 +1,451 @@
+<?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_OBJECT_ERR_NOT_AVAILABLE', 1);
+define('BX_DOL_OBJECT_ERR_ACCESS_DENIED', 2);
+define('BX_DOL_OBJECT_ERR_DUPLICATE', 3);
+define('BX_DOL_OBJECT_ERR_WRONG_DATE', 4);
+define('BX_DOL_OBJECT_ERR_CANNOT_PERFORM', 5);
+
+/**
+ * Base class for all "Object" classes.
+ * Child classes usually represents high level programming constructions to generate ready 'objects' functionality, like Comments, Votings, Forms.
+ */
+class BxDolObject extends BxDolFactory implements iBxDolReplaceable
+{
+    protected $_oTemplate = null;
+    protected $_oQuery = null;
+
+    protected $_bApi = false;
+
+    protected $_iId = 0; ///< item id the action to be performed with
+    protected $_sSystem = ''; ///< current system name
+    protected $_aSystem = array(); ///< current system array
+
+    protected $_aMarkers = array ();
+
+    protected $_sTmplContentElementBlock = '';
+    protected $_sTmplContentElementInline = '';
+    protected $_sTmplContentDoAction;
+    protected $_sTmplContentDoActionLabel = '';
+    protected $_sTmplContentCounter = '';
+    protected $_sTmplContentCounterLabel = '';
+
+    protected function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
+    {
+        parent::__construct();
+
+        $aSystems = $this->getSystems();
+        if(!isset($aSystems[$sSystem]))
+            return;
+
+        $this->_sSystem = $sSystem;
+        $this->_aSystem = $aSystems[$sSystem];
+
+        if ($oTemplate)
+            $this->_oTemplate = $oTemplate;
+        else
+            $this->_oTemplate = BxDolTemplate::getInstance();
+
+        if(!$this->isEnabled())
+            return;
+
+        if($iInit)
+            $this->init($iId);
+    }
+
+    public static function &getSystems()
+    {
+        $aResult = [];
+
+        return $aResult;
+    }
+
+    static public function pruning()
+    {
+        $iResults = 0;
+
+        $sClass = get_called_class();
+        $sMethod = 'getSystems';
+        if(!method_exists($sClass, $sMethod))
+            return $iResults;
+
+        $aSystems = $sClass::$sMethod();
+        foreach($aSystems as $aSystem) {
+            if(empty($aSystem['pruning']))
+                continue;
+
+            $oObject = $sClass::getObjectInstance($aSystem['name'], 0, false);
+            if(!$oObject || !$oObject->isEnabled())
+                continue;
+
+            $iResults += $oObject->_oQuery->pruningByDate($aSystem['pruning']);
+        }
+
+        return $iResults;
+    }
+
+    public function init($iId)
+    {
+        if(!$this->isEnabled())
+            return false;
+
+        if(empty($this->_iId) && $iId)
+            $this->setId($iId);
+
+		$this->addMarkers(array(
+            'object_id' => $this->getId(),
+            'user_id' => $this->_getAuthorId()
+        ));
+
+        return true;
+    }
+
+    public function getSystemId()
+    {
+        return $this->_aSystem['id'];
+    }
+
+    public function getSystemName()
+    {
+        return $this->_sSystem;
+    }
+
+    public function getSystemInfo()
+    {
+        return $this->_aSystem;
+    }
+
+    public function getId()
+    {
+        return $this->_iId;
+    }
+
+    public function setId($iId)
+    {
+        if($iId == $this->getId())
+            return;
+
+        $this->_iId = $iId;
+    }
+
+    public function isEnabled ()
+    {
+        return $this->_aSystem && (int)$this->_aSystem['is_on'] == 1;
+    }
+
+    public function isPerformed($iObjectId, $iAuthorId, $iAuthorIp = 0)
+    {
+        return $this->_oQuery->isPerformed($iObjectId, $iAuthorId);
+    }
+
+    public function getVote($iObjectId = 0, $bForceGet = false)
+    {
+        return $this->_getVote($iObjectId, $bForceGet);
+    }
+
+    public function getTrack($iObjectId, $iAuthorId)
+    {
+        return $this->_getTrack($iObjectId, $iAuthorId);
+    }
+
+    /**
+     * Interface functions for outer usage
+     */
+    public function getConditions($sMainTable, $sMainField)
+    {
+        if(!$this->isEnabled())
+            return array();
+
+        $sTable = isset($this->_aSystem['table_main']) ? $this->_aSystem['table_main'] : '';
+        if(empty($sTable) || empty($sMainTable) || empty($sMainField))
+            return array();
+
+        return array(
+            'join' => array (
+                'objects_' . $this->_sSystem => array(
+                    'type' => 'INNER',
+                    'table' => $sTable,
+                    'mainTable' => $sMainTable,
+                    'mainField' => $sMainField,
+                    'onField' => 'object_id',
+                    'joinFields' => array('count'),
+                ),
+            ),
+        );
+    }
+
+    public function getConditionsTrack($sMainTable, $sMainField, $iAuthorId = 0)
+    {
+        if(!$this->isEnabled())
+            return array();
+
+        $sTableTrack = isset($this->_aSystem['table_track']) ? $this->_aSystem['table_track'] : '';
+        if(empty($sTableTrack) || empty($sMainTable) || empty($sMainField))
+            return array();
+
+        return array(
+            'restriction' => array (
+                'objects_' . $this->_sSystem . '_author' => array(
+                    'value' => $iAuthorId,
+                    'field' => 'author_id',
+                    'operator' => '=',
+                    'table' => $sTableTrack,
+                ),
+            ),
+            'join' => array (
+                'objects_' . $this->_sSystem => array(
+                    'type' => 'INNER',
+                    'table' => $sTableTrack,
+                    'mainTable' => $sMainTable,
+                    'mainField' => $sMainField,
+                    'onField' => 'object_id',
+                    'joinFields' => array('author_id'),
+                ),
+            ),
+
+        );
+    }
+    
+    public function getSqlParts($sMainTable, $sMainField)
+    {
+        if(!$this->isEnabled())
+            return array();
+
+        return $this->_oQuery->getSqlParts($sMainTable, $sMainField);
+    }
+
+    public function getSqlPartsTrack($sMainTable, $sMainField, $iAuthorId = 0)
+    {
+        if(!$this->isEnabled())
+            return array();
+
+        return $this->_oQuery->getSqlPartsTrack($sMainTable, $sMainField, $iAuthorId);
+    }
+
+    public function getSqlPartsTrackAuthor($sMainTable, $sMainField, $iObjectId = 0)
+    {
+        if(!$this->isEnabled())
+            return array();
+
+        return $this->_oQuery->getSqlPartsTrackAuthor($sMainTable, $sMainField, $iObjectId);
+    }
+
+    public function addMarkers($aMarkers)
+    {
+        if(empty($aMarkers) || !is_array($aMarkers))
+			return false;
+
+        $this->_aMarkers = array_merge($this->_aMarkers, $aMarkers);
+        return true;
+    }
+
+    public function getElementAPI($aParams = [])
+    {
+        if(!($this->_bApi = bx_is_api()))
+            return;
+
+        //TODO: Implement for Objects like Views, Votes, etc.
+        $sClass = get_class($this);
+        return [
+            'display_type' => 'element', // 'element_action', 'element_counter'
+            'name' => md5($sClass),
+            'title' => get_class($this)
+        ];
+    }
+
+    /**
+     * Database functions
+     */
+    public function getQueryObject ()
+    {
+        return $this->_oQuery;
+    }
+
+	/**
+     * Permissions functions
+     */
+	public function checkAction ($sAction, $isPerformAction = false)
+    {
+        $iId = $this->_getAuthorId();
+        $a = checkActionModule($iId, $sAction, 'system', $isPerformAction);
+        return $a[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    public function checkActionErrorMsg ($sAction)
+    {
+        $iId = $this->_getAuthorId();
+        $a = checkActionModule($iId, $sAction, 'system');
+        return $a[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED ? $a[CHECK_ACTION_MESSAGE] : '';
+    }
+
+    /**
+     * Actions' response functions
+     */
+    public function onObjectDelete($iObjectId = 0)
+    {
+        $this->_oQuery->deleteObjectEntries($iObjectId ? $iObjectId : $this->getId());
+    }
+
+    /**
+     * Internal functions
+     */
+    protected function _getAuthorId ()
+    {
+        return isMember() ? bx_get_logged_profile_id() : 0;
+    }
+
+    protected function _getAuthorPassword ()
+    {
+        return getLoggedPassword();
+    }
+
+    protected function _getAuthorIp ()
+    {
+        return getVisitorIP();
+    }
+
+    protected function _getAuthorInfo($iAuthorId = 0)
+    {
+        $oProfile = $this->_getAuthorObject($iAuthorId);
+
+        return array(
+            $oProfile->getDisplayName(),
+            $oProfile->getUrl(),
+            $oProfile->getThumb(),
+            $oProfile->getUnit(),
+            $oProfile->getUnit(0, array('template' => 'unit_wo_info'))
+        );
+    }
+
+    protected function _getAuthorObject($iAuthorId = 0)
+    {
+    	if($iAuthorId == 0)
+    		return BxDolProfileUndefined::getInstance();
+
+        $oProfile = BxDolProfile::getInstance($iAuthorId);
+        if(!$oProfile)
+			$oProfile = BxDolProfileUndefined::getInstance();
+
+        return $oProfile;
+    }
+
+    /**
+     * Update Trigger table using data which is automatically gotten from object's internal table.
+     */
+    protected function _trigger()
+    {
+        if(!$this->_aSystem['trigger_table'])
+            return false;
+
+        $iId = $this->getId();
+        if(!$iId)
+            return false;
+
+        return $this->_oQuery->updateTriggerTable($iId);
+    }
+
+    /**
+     * Update (increment/decrement) Trigger table using provided value.
+     */
+    protected function _triggerValue($iValue)
+    {
+        if(!$this->_aSystem['trigger_table'])
+            return false;
+
+        $iId = $this->getId();
+        if(!$iId)
+            return false;
+
+        return $this->_oQuery->updateTriggerTableValue($iId, $iValue);
+    }
+
+    /**
+     * 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);
+    }
+
+    protected function _prepareParamsData($aParams)
+    {
+        $aParams = array_merge([
+            'sSystem' => $this->getSystemName(),
+            'iObjId' => $this->getId(),
+            'iAuthorId' => $this->_getAuthorId(),
+            'sRootUrl' => BX_DOL_URL_ROOT,
+        ], $aParams);
+
+        foreach($aParams as $sKey => $mixedValue)
+            if(is_bool($mixedValue))
+                $aParams[$sKey] = (int)$mixedValue;
+
+        return $aParams;
+    }
+
+    protected function _getRequestParamsData($aKeys = array())
+    {
+        $sParams = bx_get('params');
+        if($sParams === false)
+            return [];
+
+        $aParams = [];
+        parse_str(bx_process_input($sParams), $aParams);
+
+        return $aParams;
+    }
+
+    protected function _prepareRequestParamsData($aParams, $aParamsAdd = array())
+    {
+        $aRequestParams = array_intersect_key($aParams, $this->_aElementDefaults);
+        $aRequestParams = array_merge($aRequestParams, $aParamsAdd);
+
+        foreach($aRequestParams as $sKey => $mixedValue)
+            if(is_bool($mixedValue))
+                $aRequestParams[$sKey] = (int)$mixedValue;
+
+        return $aRequestParams;
+    }
+
+    protected function _getTmplContentElementBlock()
+    {
+        return $this->_sTmplContentElementBlock;
+    }
+
+    protected function _getTmplContentElementInline()
+    {
+        return $this->_sTmplContentElementInline;
+    }
+
+    protected function _getTmplContentDoAction()
+    {
+        return $this->_sTmplContentDoAction;
+    }
+
+    protected function _getTmplContentDoActionLabel()
+    {
+        return $this->_sTmplContentDoActionLabel;
+    }
+
+    protected function _getTmplContentCounter()
+    {
+        return $this->_sTmplContentCounter;
+    }
+
+    protected function _getTmplContentCounterLabel()
+    {
+        return $this->_sTmplContentCounterLabel;
+    }
+}
+
+/** @} */

+ 643 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolProfile.php

@@ -0,0 +1,643 @@
+<?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_PROFILE_ACTION_AUTO', 0); ///< automatic action without any checking
+define('BX_PROFILE_ACTION_MANUAL', 1); ///< manual action performed by human
+define('BX_PROFILE_ACTION_ROBOT', 2); ///< action peformed by some robot based on some conditions
+define('BX_PROFILE_ACTION_EXTERNAL', 2); ///< action peformed by external service, like join using OAuth 
+
+class BxDolProfile extends BxDolFactory implements iBxDolProfile
+{
+    protected $_iProfileID;
+    protected $_aProfile;
+    protected $_oQuery;
+
+    /**
+     * Constructor
+     */
+    protected function __construct ($iProfileId)
+    {
+        $iProfileId = (int)$iProfileId;
+        $sClass = get_class($this) . '_' . $iProfileId;
+        if (isset($GLOBALS['bxDolClasses'][$sClass]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+
+        $this->_iProfileID = $iProfileId; // since constructor is protected $iProfileId is always valid
+        $this->_oQuery = BxDolProfileQuery::getInstance();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        $sClass = get_class($this) . '_' . $this->_iProfileID;
+        if (isset($GLOBALS['bxDolClasses'][$sClass]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of Account Profile by account id
+     */
+    public static function getInstanceAccountProfile($iAccountId = false, $bClearCache = false)
+    {
+        if (!$iAccountId)
+            $iAccountId = getLoggedId();
+        $oQuery = BxDolProfileQuery::getInstance();
+        $aProfile = $oQuery->getProfileByContentTypeAccount($iAccountId, 'system', $iAccountId);
+        if (!$aProfile)
+            return false;
+        return self::getInstance($aProfile['id'], $bClearCache);
+    }
+
+    /**
+     * Get singleton instance of Profile by account id, content id and type
+     */
+    public static function getInstanceByContentTypeAccount($iContent, $sType, $iAccountId = false)
+    {
+        if (!$iAccountId)
+            $iAccountId = getLoggedId();
+        $oQuery = BxDolProfileQuery::getInstance();
+        $aProfile = $oQuery->getProfileByContentTypeAccount($iContent, $sType, $iAccountId);
+        if (!$aProfile)
+            return false;
+        return self::getInstance($aProfile['id']);
+    }
+
+    /**
+     * Get singleton instance of Profile by content id and type
+     */
+    public static function getInstanceByContentAndType($iContent, $sType, $bClearCache = false)
+    {
+        $oQuery = BxDolProfileQuery::getInstance();
+        $aProfile = $oQuery->getProfileByContentAndType($iContent, $sType, $bClearCache);
+        if (!$aProfile)
+            return false;
+        return self::getInstance($aProfile['id']);
+    }
+
+    /**
+     * Get singleton instance of Profile by Account id (currently active profile is returned)
+     */
+    public static function getInstanceByAccount($iAccountId = false, $bClearCache = false)
+    {
+        $oQuery = BxDolProfileQuery::getInstance();
+        $mixedProfileId = $oQuery->getCurrentProfileByAccount($iAccountId, $bClearCache);
+
+        return self::getInstance($mixedProfileId);
+    }
+
+    /**
+     * Get singleton instance of Profile by profile id, if profile isn't found it returns instance of BxDolProfileAnonymous or BxDolProfileUndefined
+     */
+    public static function getInstanceMagic($mixedProfileId = false, $bClearCache = false)
+    {
+        if ($mixedProfileId < 0)
+            return BxDolProfileAnonymous::getInstance($mixedProfileId);
+
+        if (0 === $mixedProfileId || !($oProfile = self::getInstance($mixedProfileId, $bClearCache)))
+            return BxDolProfileUndefined::getInstance();
+        
+        return $oProfile;
+    }
+
+    /**
+     * Get singleton instance of Profile by profile id
+     */
+    public static function getInstance($mixedProfileId = false, $bClearCache = false)
+    {
+        $oQuery = BxDolProfileQuery::getInstance();
+
+        if (!$mixedProfileId)
+            $mixedProfileId = $oQuery->getCurrentProfileByAccount(getLoggedId(), $bClearCache);
+
+        $aProfileInfo = $oQuery->getInfoById($mixedProfileId);
+        if (empty($aProfileInfo['id']) || !BxDolModuleDb::getInstance()->isEnabledByName($aProfileInfo['type']))
+            return false;
+
+        $sClass = __CLASS__ . '_' . $aProfileInfo['id'];
+        if (!isset($GLOBALS['bxDolClasses'][$sClass]))
+            $GLOBALS['bxDolClasses'][$sClass] = new BxDolProfile($aProfileInfo['id']);
+
+        return $GLOBALS['bxDolClasses'][$sClass];
+    }
+
+    public static function getData($mixedProfileId = false, $aParams = [])
+    {
+        $sDisplayType = 'unit';
+        if(isset($aParams['display_type']))
+            $sDisplayType = $aParams['display_type'];
+
+        if(!($mixedProfileId instanceof BxDolProfile))
+            $oProfile = BxDolProfile::getInstanceMagic($mixedProfileId);
+        else
+            $oProfile = $mixedProfileId;
+
+        return [
+            'id' => $oProfile->id(),
+            'display_type' =>  $sDisplayType,
+            'display_name' => $oProfile->getDisplayName(),
+            'url' => bx_relative_url($oProfile->getUrl()),
+            'url_avatar' => $oProfile->getAvatar(),
+        ];
+    }
+    
+    /**
+     * Get profile id
+     */
+    public function id()
+    {
+        $aProfile = $this->getInfo($this->_iProfileID);
+        return isset($aProfile['id']) ? $aProfile['id'] : false;
+    }
+
+    /**
+     * Get account id associated with the profile
+     */
+    public function getAccountId()
+    {
+        $aInfo = $this->getInfo();
+        return $aInfo['account_id'];
+    }
+
+    /**
+     * Get account object associated with the profile
+     */
+    public function getAccountObject()
+    {
+        return BxDolAccount::getInstance($this->getAccountId());
+    }
+
+    /**
+     * Get content id associated with the profile
+     */
+    public function getContentId()
+    {
+        $aInfo = $this->getInfo();
+        return $aInfo['content_id'];
+    }
+
+    /**
+     * Check if profile status is active
+     */
+    public function isActive($iProfileId = false)
+    {
+        if($this->getStatus($iProfileId) != BX_PROFILE_STATUS_ACTIVE)
+            return false;
+
+        return (($oAccount = $this->getAccountObject()) !== false && $oAccount->isConfirmed()) || !getParam('sys_account_hide_unconfirmed_accounts');
+    }
+
+    /**
+     * Is profile online
+     */
+    public function isOnline($iProfileId = false)
+    {
+        $iProfileId = (int)$iProfileId ? $iProfileId : $this->_iProfileID;
+        return $this->_oQuery->isOnline($iProfileId);
+    }
+
+    /**
+     * Is profile can 'Act as Profile'
+     */
+    public function isActAsProfile($iProfileId = false)
+    {
+        $aInfo = $this->_oQuery->getInfoById((int)$iProfileId ? $iProfileId : $this->_iProfileID);
+        return BxDolService::call($aInfo['type'], 'act_as_profile');
+    }
+
+    /**
+     * Get profile status
+     */
+    public function getStatus($iProfileId = false)
+    {
+        $aInfo = $this->_oQuery->getInfoById((int)$iProfileId ? $iProfileId : $this->_iProfileID);
+        return $aInfo['status'];
+    }
+
+    /**
+     * Get profile module name
+     */
+    public function getModule($iProfileId = false)
+    {
+        $aInfo = $this->_oQuery->getInfoById((int)$iProfileId ? $iProfileId : $this->_iProfileID);
+        return $aInfo['type'];
+    }
+
+    /**
+     * Get profile info
+     */
+    public function getInfo($iProfileId = 0)
+    {
+        if ($iProfileId && $iProfileId != $this->_iProfileID)
+            return $this->_oQuery->getInfoById((int)$iProfileId ? $iProfileId : $this->_iProfileID);
+
+        if ($this->_aProfile)
+            return $this->_aProfile;
+        
+        $this->_aProfile = $this->_oQuery->getInfoById((int)$iProfileId ? $iProfileId : $this->_iProfileID);
+        return $this->_aProfile;
+    }
+
+    /**
+     * Validate profile id.
+     * @param $s - profile id
+     * @return profile id or false if profile was not found
+     */
+    static public function getID($s)
+    {
+        $iId = BxDolProfileQuery::getInstance()->getIdById((int)$s);
+        return $iId ? $iId : false;
+    }
+
+    /**
+     * Get name to display in thumbnail
+     */
+    public function getDisplayName($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        $sDisplayName = BxDolService::call($aInfo['type'], 'profile_name', array($aInfo['content_id']));
+        bx_alert('profile', 'profile_name', $iProfileId, 0, array('info' => $aInfo, 'display_name' => &$sDisplayName));
+        return $sDisplayName;
+    }
+
+    /**
+     * Get profile url
+     */
+    public function getUrl($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_url', array($aInfo['content_id']));
+    }
+
+    /**
+     * Get profile unit
+     */
+    public function getUnit($iProfileId = 0, $aParams = array())
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_unit', array($aInfo['content_id'], $aParams));
+    }
+    
+    /**
+     * Get badges
+     */
+    public function getBadges($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'get_badges', array($aInfo['content_id'], false, true));
+    }
+
+	/**
+     * Check whether a profile has real image uploaded by user.
+     */
+    public function hasImage($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'has_image', array($aInfo['content_id']));
+    }
+
+    /**
+     * Get picture url
+     */
+    public function getPicture($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_picture', array($aInfo['content_id']));
+    }
+
+    /**
+     * Get avatar url
+     */
+    public function getAvatar($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_avatar', array($aInfo['content_id']));
+    }
+
+    /**
+     * Get big (2x) avatar url
+     */
+    public function getAvatarBig($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_avatar_big', array($aInfo['content_id']));
+    }
+
+    /**
+     * Get cover url
+     */
+    public function getCover($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_cover', array($aInfo['content_id']));
+    }
+
+	/**
+     * Get unit cover url
+     */
+    public function getUnitCover($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_unit_cover', array($aInfo['content_id']));
+    }
+
+    /**
+     * Get thumbnail url
+     */
+    public function getThumb($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_thumb', array($aInfo['content_id']));
+    }
+
+    /**
+     * Get icon url
+     */
+    public function getIcon($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_icon', array($aInfo['content_id']));
+    }
+
+    /**
+     * Get module icon
+     */
+    public function getIconModule($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'module_icon');
+    }
+
+    /**
+     * get profile edit page url
+     */
+    public function getEditUrl($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_edit_url', array($aInfo['content_id']));
+    }
+
+    /**
+     * @see iBxDolProfile::checkAllowedProfileView
+     */
+    public function checkAllowedProfileView($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'check_allowed_profile_view', array($aInfo['content_id']));
+    }
+
+    /**
+     * @see iBxDolProfile::checkAllowedProfileContact
+     */
+    public function checkAllowedProfileContact($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'check_allowed_profile_contact', array($aInfo['content_id']));
+    }
+    
+    
+    /**
+     * @see iBxDolProfile::checkAllowedPostInProfile
+     */
+    public function checkAllowedPostInProfile($iProfileId = 0, $sPostModule = '')
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'check_allowed_post_in_profile', array($aInfo['content_id'], $sPostModule));
+    }
+    
+    /**
+     * Delete profile.
+     * @param $ID - optional profile id to delete
+     * @param $bDeleteWithContent - delete profile with all its contents
+     * @param $bForceDelete - force deletetion is case of account profile deletion
+     * @return false on error, or true on success
+     */
+    function delete($ID = false, $bDeleteWithContent = false, $bForceDelete = false)
+    {
+        $ID = (int)$ID;
+        if (!$ID)
+            $ID = $this->_iProfileID;
+
+        $aProfileInfo = $this->_oQuery->getInfoById($ID);
+        if (!$aProfileInfo)
+            return false;
+
+        // delete system profiles (accounts) is not allowed, instead - delete whole account
+        if (!$bForceDelete && 'system' == $aProfileInfo['type'])
+            return false;
+
+        // delete actual profile
+        if ($sErrorMsg = BxDolService::call($aProfileInfo['type'] , 'delete_entity_service', array($aProfileInfo['content_id'], $bDeleteWithContent)))
+            return false;
+
+        // switch profile context if deleted profile is active profile context
+        $oAccount = BxDolAccount::getInstance ($aProfileInfo['account_id']);
+        $aAccountInfo = $oAccount->getInfo();
+        if (!$bForceDelete && $ID == $aAccountInfo['profile_id'])
+            $oAccount->updateProfileContextAuto($ID);
+
+        // create system event before deletion
+        $isStopDeletion = false;
+        bx_alert('profile', 'before_delete', $ID, 0, array('delete_with_content' => $bDeleteWithContent, 'stop_deletion' => &$isStopDeletion, 'type' => $aProfileInfo['type']));
+        if ($isStopDeletion)
+            return false;
+
+        // delete associated content
+        if($bDeleteWithContent) {
+	        BxDolCmts::onAuthorDelete($ID);
+
+	        BxDolReport::onAuthorDelete($ID);
+
+	        BxDolVote::onAuthorDelete($ID);
+
+	        BxDolFavorite::onAuthorDelete($ID);
+
+	        BxDolView::onAuthorDelete($ID);
+        }
+
+        // delete connections
+        $oConn = BxDolConnection::getObjectInstance('sys_profiles_friends');
+        $oConn->onDeleteInitiatorAndContent($ID);
+
+        $oConn = BxDolConnection::getObjectInstance('sys_profiles_subscriptions');
+        $oConn->onDeleteInitiatorAndContent($ID);
+
+        // delete profile's acl levels
+        BxDolAcl::getInstance()->onProfileDelete($ID);
+
+        // delete SEO links
+        BxDolPage::deleteSeoLinkByParam ('profile_id', $ID);
+
+        // delete profile
+        if (!$this->_oQuery->delete($ID))
+            return false;
+
+        // create system event
+        bx_alert('profile', 'delete', $ID, 0, array('delete_with_content' => $bDeleteWithContent, 'type' => $aProfileInfo['type']));
+
+        // unset class instance to prevent creating the instance again
+        $this->_iProfileID = 0;
+        $sClass = get_class($this) . '_' . $ID;
+        unset($GLOBALS['bxDolClasses'][$sClass]);
+
+        return true;
+    }
+
+    /**
+     * Insert account and content id association. Also if currect profile id is not defined - it updates current profile id in account.
+     * @param $iAccountId account id
+     * @param $iContentId content id
+     * @param $sStatus profile status
+     * @param $sType profile content type
+     * @return inserted profile's id
+     */
+    static public function add ($iAction, $iAccountId, $iContentId, $sStatus, $sType = 'system')
+    {
+        $oQuery = BxDolProfileQuery::getInstance();
+        if (!($iProfileId = $oQuery->insertProfile ($iAccountId, $iContentId, $sStatus, $sType)))
+            return false;
+        bx_alert('profile', 'add', $iProfileId, 0, array('module' => $sType, 'content' => $iContentId, 'account' => $iAccountId, 'status' => $sStatus, 'action' => $iAction, 'profile_id' => &$iProfileId));
+        return $iProfileId;
+    }
+
+    /**
+     * Change profile status to 'Active'
+     */
+    public function activate($iAction, $iProfileId = 0, $bSendEmailNotification = true)
+    {
+        $sStatus = $this->getStatus($iProfileId);
+        return $this->changeStatus(BX_PROFILE_STATUS_ACTIVE, BX_PROFILE_STATUS_PENDING == $sStatus ? 'approve' : 'activate', $iAction, $iProfileId, $bSendEmailNotification);
+    }
+
+    /**
+     * Change profile status from 'Pending' to the next level - 'Active'
+     */
+    public function approve($iAction, $iProfileId = 0, $bSendEmailNotification = true)
+    {
+        return $this->changeStatus(BX_PROFILE_STATUS_ACTIVE, 'approve', $iAction, $iProfileId, $bSendEmailNotification);
+    }
+
+    /**
+     * Change profile status to 'Pending'
+     */
+    public function disapprove($iAction, $iProfileId = 0, $bSendEmailNotification = true)
+    {
+        return $this->changeStatus(BX_PROFILE_STATUS_PENDING, 'disapprove', $iAction, $iProfileId, $bSendEmailNotification);
+    }
+
+    public function doAudit($sAction, $aData = array())
+    {
+        bx_audit(
+            $this->getContentId(), 
+            $this->getModule(), 
+            $sAction,  
+            array('content_title' => $this->getDisplayName(), 'data' => $aData)
+        );  
+    }
+    
+    /**
+     * Change profile status to 'Suspended'
+     */
+    public function suspend($iAction, $iProfileId = 0, $bSendEmailNotification = true)
+    {
+        if (!$iProfileId)
+            $iProfileId = $this->_iProfileID;
+        
+        //moderators shouldn't be able to suspend other moderators and admins
+        if (BxDolAcl::getInstance()->isMemberLevelInSet(array(MEMBERSHIP_ID_MODERATOR), bx_get_logged_profile_id()) && BxDolAcl::getInstance()->isMemberLevelInSet(array(MEMBERSHIP_ID_MODERATOR, MEMBERSHIP_ID_ADMINISTRATOR), $iProfileId))
+            return false;
+        
+        return $this->changeStatus(BX_PROFILE_STATUS_SUSPENDED, 'suspend', $iAction, $iProfileId, $bSendEmailNotification);
+    }
+
+    protected function changeStatus($sStatus, $sAlertActionName, $iAction, $iProfileId = 0, $bSendEmailNotification = true)
+    {
+        if (!$iProfileId)
+            $iProfileId = $this->_iProfileID;
+
+        // get account and profile objects
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        $oAccount = $oProfile->getAccountObject();
+        if (!$oProfile || !$oAccount)
+            return false;
+
+        // change status
+        if (!$this->_oQuery->changeStatus($iProfileId, $sStatus))
+            return false;
+
+        $this->_aProfile = array();
+
+        // alert about status changing
+        bx_alert('profile', $sAlertActionName, $iProfileId, false, array('action' => $iAction, 'status' => &$sStatus, 'send_email_notification' => &$bSendEmailNotification));
+        
+        $this->doAudit('_sys_audit_action_set_status_' . $sStatus);
+        
+        // send email to member about status change
+        if ($bSendEmailNotification)
+            sendMailTemplate('t_ChangeStatus' . ucfirst($sStatus), $oAccount->id(), $iProfileId, array('status' => $sStatus), BX_EMAIL_SYSTEM);
+
+        return true;
+    }
+
+    public static function getSwitchToProfileRedirectUrl($iProfileId)
+    {
+        return BxDolPermalinks::getInstance()->permalink('page.php?i=account-profile-switcher', [
+            'switch_to_profile' => $iProfileId, 
+            'redirect' => getParam('sys_account_switch_to_profile_redirect')
+        ]);
+    }
+
+    /**
+     * Display informer message if it is possible to switch to this profile
+     */
+    public function checkSwitchToProfile($oTemplate = null, $iViewerAccountId = false, $iViewerProfileId = false)
+    {
+        if (false === $iViewerAccountId)
+            $iViewerAccountId = getLoggedId();
+        if (false === $iViewerProfileId)
+            $iViewerProfileId = bx_get_logged_profile_id();
+
+        if (!$iViewerAccountId || !$iViewerProfileId)
+            return;
+
+        $aCheck = checkActionModule($iViewerProfileId, 'switch to any profile', 'system', false);
+        $bAllowSwitchToAnyProfile = $aCheck[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED;
+        
+        $iSwitchToAccountId = $this->getAccountId();
+        $iSwitchToProfileId = $this->id();
+        $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 ||  $iViewerProfileId == $iSwitchToProfileId)
+            return;
+
+        $oInformer = BxDolInformer::getInstance($oTemplate);
+        if($oInformer)
+            $oInformer->add('sys-switch-profile-context', _t('_sys_txt_account_profile_context_change' . ($iSwitchToAccountId != $iViewerAccountId ? '_to_another' : '') . '_suggestion', self::getSwitchToProfileRedirectUrl($this->id())), BX_INFORMER_INFO);
+    }
+
+    /**
+     * Add permament messages.
+     */
+    public function addInformerPermanentMessages ($oInformer)
+    {
+    	$aInfo = $this->getInfo();
+    	$aProfiles = $this->_oQuery->getProfilesByAccount($aInfo['account_id']);
+
+        if ($aInfo['type'] == 'system' && count($aProfiles) == 1) {
+            $sUrl = BxDolPermalinks::getInstance()->permalink('page.php?i=account-profile-switcher');
+            $oInformer->add('sys-account-profile-system', _t('_sys_txt_account_profile_system', $sUrl), BX_INFORMER_ALERT);
+        }
+    }
+}
+
+/** @} */

+ 180 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolPush.php

@@ -0,0 +1,180 @@
+<?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 BxDolPush extends BxDolFactory implements iBxDolSingleton
+{
+    protected $_sAppId;
+    protected $_sRestApi;
+
+    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->_sAppId = getParam('sys_push_app_id');
+        $this->_sRestApi = getParam('sys_push_rest_api');
+    }
+
+    /**
+     * 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 BxDolPush();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+    
+    /**
+     * Get tags to send to PUSH server
+     * @param $iProfileId - profile ID
+     * @return array of tags
+     */
+    public static function getTags($iProfileId = false)
+    {
+        if (false === $iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        $oAccount = $oProfile ? $oProfile->getAccountObject() : null;
+        if (!$oProfile || !$oAccount)
+            return false;
+
+        $sEmail = $oAccount->getEmail();
+        $a = array (
+            'user' => $iProfileId,
+            'user_hash' => encryptUserId($iProfileId),
+            'real_name' => $oProfile->getDisplayName(),
+            'email' => $sEmail,
+            'email_hash' => $sEmail ? hash_hmac('sha256', $sEmail, getParam('sys_push_app_id')) : '',
+        );
+
+        bx_alert('system', 'push_tags', $iProfileId, $iProfileId, array('tags' => &$a));
+
+        return $a;
+    }
+
+    /**
+     * @param $a - array to fill with notification counter per module
+     * @return total number of notifications
+     */
+    public static function getNotificationsCount($iProfileId = 0, &$aBubbles = null)
+    {    
+        $iMemberIdCookie = null;
+        $bLoggedMemberGlobals = null;
+        if ($iProfileId && $iProfileId != bx_get_logged_profile_id()) {
+            if (getLoggedId())
+                $iMemberIdCookie = getLoggedId();
+            if (!empty($GLOBALS['logged']['member']))
+                $bLoggedMemberGlobals = $GLOBALS['logged']['member'];
+            $oProfile = BxDolProfile::getInstance($iProfileId);
+            $_COOKIE['memberID'] = $oProfile ? $oProfile->getAccountId() : 0;
+            $GLOBALS['logged']['member'] = $oProfile ? true : false;
+        }
+    
+        $aMenusObjects = array('sys_account_notifications', 'sys_toolbar_member');
+        foreach ($aMenusObjects as $sMenuObject) {
+            if ($iProfileId && $iProfileId != bx_get_logged_profile_id())
+                unset($GLOBALS['bxDolClasses']['BxDolMenu!sys_account_notifications']);
+            $oMenu = BxDolMenu::getObjectInstance($sMenuObject);
+            if ($iProfileId && $iProfileId != bx_get_logged_profile_id())
+                unset($GLOBALS['bxDolClasses']['BxDolMenu!sys_account_notifications']);
+
+            $bSave = $oMenu->setDisplayAddons(true);
+            $a = $oMenu->getMenuItems();
+            $iBubbles = 0;
+            foreach ($a as $r) {
+                if (!$r['bx_if:addon']['condition'])
+                    continue;
+                if (null !== $aBubbles)
+                    $aBubbles[$r['name']] = $r['bx_if:addon']['content']['addon'];
+                $iBubbles += $r['bx_if:addon']['content']['addon'];
+            }
+        }
+
+        if ($iProfileId && $iProfileId != bx_get_logged_profile_id()) {
+            if (null === $iMemberIdCookie)
+                unset($_COOKIE['memberID']);
+            else
+                $_COOKIE['memberID'] = $iMemberIdCookie;
+
+            if (null === $bLoggedMemberGlobals)
+                unset($GLOBALS['logged']['member']);
+            else
+                $GLOBALS['logged']['member'] = $bLoggedMemberGlobals;
+        }
+
+        return $iBubbles;
+    }
+
+    public function send($iProfileId, $aMessage, $bAddToQueue = false)
+    {
+        if(empty($this->_sAppId) || empty($this->_sRestApi))
+            return false;
+
+        if($bAddToQueue && BxDolQueuePush::getInstance()->add($iProfileId, $aMessage))
+            return true;        
+    
+		$aFields = array(
+			'app_id' => $this->_sAppId,
+			'filters' => array(
+		        array('field' => 'tag', 'key' => 'user_hash', 'relation' => '=', 'value' => encryptUserId($iProfileId))
+            ),
+			'contents' => !empty($aMessage['contents']) && is_array($aMessage['contents']) ? $aMessage['contents'] : array(),
+			'headings' => !empty($aMessage['headings']) && is_array($aMessage['headings']) ? $aMessage['headings'] : array(),
+            'web_url' => !empty($aMessage['url']) ? $aMessage['url'] : '',
+            'data' => array('url' => !empty($aMessage['url']) ? $aMessage['url'] : ''),
+            'chrome_web_icon' => !empty($aMessage['icon']) ? $aMessage['icon'] : '',
+		);
+
+        if ('on' == getParam('bx_nexus_option_push_notifications_count')) {
+            $iBadgeCount = $this->getNotificationsCount($iProfileId);
+            $aFields['ios_badgeType'] = 'SetTo';
+            $aFields['ios_badgeCount'] = $iBadgeCount;
+        }
+
+		$oChannel = curl_init();
+		curl_setopt($oChannel, CURLOPT_URL, "https://onesignal.com/api/v1/notifications");
+		curl_setopt($oChannel, CURLOPT_HTTPHEADER, array(
+			'Content-Type: application/json; charset=utf-8',
+			'Authorization: Basic ' . $this->_sRestApi
+		));
+		curl_setopt($oChannel, CURLOPT_RETURNTRANSFER, true);
+		curl_setopt($oChannel, CURLOPT_HEADER, false);
+		curl_setopt($oChannel, CURLOPT_POST, true);
+		curl_setopt($oChannel, CURLOPT_POSTFIELDS, json_encode($aFields));
+		curl_setopt($oChannel, CURLOPT_SSL_VERIFYPEER, false);
+
+		$sResult = curl_exec($oChannel);
+		curl_close($oChannel);
+        
+        $oResult = @json_decode($sResult, true);
+        if(isset($oResult['errors'])){
+            foreach($oResult['errors'] as $sError) {  
+                bx_log('sys_push', $sError . " Message:" . json_encode($aMessage));
+            }
+        }
+
+		return $sResult;
+    }
+}
+
+/** @} */

+ 308 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolRelation.php

@@ -0,0 +1,308 @@
+<?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 BxDolRelation extends BxDolConnection
+{
+    protected static $_sParamEnabled = 'sys_relations_enable';
+    protected static $_sParamTypes = 'sys_relations';
+
+    protected $_sParamDivider;
+    protected $_sPreList;
+
+    protected function __construct($aObject)
+    {
+        parent::__construct($aObject);
+
+        $this->_oQuery = new BxDolRelationQuery($aObject);
+
+        $this->_sParamDivider = '_';
+        $this->_sPreList = 'sys_relations';
+    }
+
+    /**
+     * Whether the Relations feature is enabled globaly or not.
+     * @return boolean
+     */
+    public static function isEnabled()
+    {
+        return getParam(self::$_sParamEnabled) == 'on';
+    }
+
+    /**
+     * Add new relation.
+     * @param $mixedContent content to make relation with or an array with content and relation type
+     * @return array
+     */
+    public function actionAdd($mixedContent = 0, $iInitiator = false)
+    {
+        if(empty($mixedContent))
+            $mixedContent = bx_process_input($_POST['id'], BX_DATA_INT);
+
+        $iContent = 0;
+        $iRelation = 0;
+        if(is_array($mixedContent)) {
+            $iContent = (int)$mixedContent['content'];
+            $iRelation = (int)$mixedContent['relation'];
+        }
+        else
+            $iContent = (int)$mixedContent;
+
+        $iInitiator = $iInitiator ? (int)$iInitiator : (int)bx_get_logged_profile_id();
+
+        $aResult = parent::actionAdd($iContent, $iInitiator);
+        if(empty($iRelation) || (isset($aResult['err']) && $aResult['err'] !== false))
+            return $aResult;
+
+        $this->_oQuery->updateConnection($iInitiator, $iContent, array(
+            'relation' => $iRelation
+        ));
+
+        return $aResult;
+    }
+    
+    /**
+     * Confirm relation request without creation of retroactive relation.
+     * @param $iContent content to make relation with
+     * @return array
+     */
+    public function actionConfirm($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(), 'confirmConnection', '_sys_conn_err_connection_does_not_exists');
+    }
+
+    /**
+     * Remove relation without removing a retroactive relation. 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);
+
+        return $this->_action($iInitiator ? $iInitiator : bx_get_logged_profile_id(), $iContent, 'removeConnection', '_sys_conn_err_connection_does_not_exists');
+    }
+
+    /**
+     * 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 && !empty($aParams['relation']))
+            $this->_oQuery->updateConnection($iInitiator, $iContent, [
+                'relation' => (int)$aParams['relation']
+            ]);
+
+        return $bResult;
+    }
+
+    /**
+     * Confirm relation request without creation of retroactive relation.
+     * @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 confirmConnection($iInitiator, $iContent)
+    {
+        $iMutual = 1;
+        if(!$this->_oQuery->updateConnectionMutual((int)$iInitiator, (int)$iContent, $iMutual))
+            return false;
+
+        bx_alert($this->_sObject, 'connection_confirmed', 0, bx_get_logged_profile_id(), array(
+            'initiator' => (int)$iInitiator,
+            'content' => (int)$iContent,
+            'mutual' => (int)$iMutual,
+            'object' => $this,
+        ));
+
+        return true;
+    }
+
+    /**
+     * Compound function, which calls getCommonContentExt, getConnectedContentExt or getConnectedInitiatorsExt 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 getConnectionsAsArrayExt($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->getCommonContentExt($iId1, $iId2, $isMutual, $iStart, $iLimit, $iOrder);
+
+        if (BX_CONNECTIONS_CONTENT_TYPE_INITIATORS == $sContentType)
+            $sMethod = 'getConnectedInitiatorsExt';
+        else
+            $sMethod = 'getConnectedContentExt';
+
+        return $this->$sMethod($iId1, $isMutual, $iStart, $iLimit, $iOrder);
+    }
+
+    /**
+     * Get common content (full info) between two initiators
+     * @param $iInitiator1 one initiator
+     * @param $iInitiator2 second initiator
+     * @param $isMutual get mutual connections only
+     * @return array of available connections
+     */
+    public function getCommonContentExt($iInitiator1, $iInitiator2, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        return $this->_oQuery->getCommonContentExt($iInitiator1, $iInitiator2, $isMutual, $iStart, $iLimit, $iOrder);
+    }
+
+    /**
+     * Get connected initiators (full info)
+     * @param $iContent content of the connection
+     * @param $isMutual get mutual connections only
+     * @return array of available connections
+     */
+    public function getConnectedInitiatorsExt($iContent, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        return $this->_oQuery->getConnectedInitiatorsExt($iContent, $isMutual, $iStart, $iLimit, $iOrder);
+    }
+
+    /**
+     * Get connected content (full info)
+     * @param $iInitiator initiator of the connection
+     * @param $isMutual get mutual connections only
+     * @return array of available connections
+     */
+    public function getConnectedContentExt($iInitiator, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        return $this->_oQuery->getConnectedContentExt($iInitiator, $isMutual, $iStart, $iLimit, $iOrder);
+    }
+
+    /**
+     * 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(!$this->isRelationAvailable($iInitiator, $iContent))
+            return _t('_sys_txt_access_denied');
+
+        $mixedResult = $this->checkAllowedConnectCustom($iInitiator, $iContent, $isPerformAction, $isMutual, $isInvertResult, $isSwap, $isCheckExists);
+        if($mixedResult !== CHECK_ACTION_RESULT_ALLOWED)
+            return $mixedResult;
+
+        return parent::checkAllowedConnect($iInitiator, $iContent, $isPerformAction, $isMutual, $isInvertResult, $isSwap, $isCheckExists);
+    }
+
+    /**
+     * Custom check action method which can be overwritten.
+     * Currently only friends can establish relations.
+     */
+    public function checkAllowedConnectCustom($iInitiator, $iContent, $isPerformAction = false, $isMutual = false, $isInvertResult = false, $isSwap = false, $isCheckExists = true)
+    {
+        if(!BxDolConnection::getObjectInstance('sys_profiles_friends')->isConnected($iInitiator, $iContent, true))
+            return _t('_sys_txt_access_denied');
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    public function checkAllowedConfirmConnection($iInitiator, $iContent, $isPerformAction = false, $isMutual = false, $isInvertResult = false, $isSwap = false, $isCheckExists = true)
+    {
+        return $this->checkAllowedConnect($iInitiator, $iContent, $isPerformAction, $isMutual, $isInvertResult, $isSwap, false);
+    }
+
+    public function isRelationAvailableFromProfile($sModule)
+    {
+        $sModule .= $this->_sParamDivider;
+
+        $aTypes = $this->getRelationTypes();
+        foreach($aTypes as $sType)
+            if(substr($sType, 0, strlen($sModule)) == $sModule)
+                return true;
+
+        return false;
+    }
+
+    public function isRelationAvailableWithProfile($sModule)
+    {
+        $sModule = $this->_sParamDivider . $sModule;
+
+        $aTypes = $this->getRelationTypes();
+        foreach($aTypes as $sType)
+            if(substr($sType, -strlen($sModule)) == $sModule)
+                return true;
+
+        return false;
+    }
+    
+    public function isRelationAvailableBetweenProfiles($sModuleInitiator, $sModuleContent)
+    {
+        $aTypes = $this->getRelationTypes();
+        if(in_array($sModuleInitiator . $this->_sParamDivider . $sModuleContent, $aTypes))
+            return true;
+
+        return false;
+    }
+
+    public function isRelationAvailable($iInitiator, $iContent)
+    {
+        $oInitiator = BxDolProfile::getInstance($iInitiator);
+        $oContent = BxDolProfile::getInstance($iContent);
+        if(!$oInitiator || !$oContent)
+            return false;
+
+        return $this->isRelationAvailableBetweenProfiles($oInitiator->getModule(), $oContent->getModule());
+    }
+
+    public function getRelations($iInitiator, $iContent, &$aSuggestions = array())
+    {
+        $aRelations = BxDolFormQuery::getDataItems($this->_sPreList, false, BX_DATA_VALUES_ALL);
+
+        bx_alert($this->_sObject, 'get_relations', 0, bx_get_logged_profile_id(), array(
+            'initiator' => (int)$iInitiator,
+            'content' => (int)$iContent,
+            'pre_list' => $this->_sPreList,
+            'relations' => &$aRelations
+        ));
+
+        if($this->isConnected($iContent, $iInitiator)) {
+            $iRelation = $this->getRelation($iContent, $iInitiator);
+            if(!empty($iRelation) && !empty($aRelations[$iRelation]['Data']))
+                $aSuggestions = unserialize($aRelations[$iRelation]['Data']);
+        }
+
+        return $aRelations;
+    }
+
+    public function getRelation($iInitiator, $iContent)
+    {
+        $aConnection = $this->_oQuery->getConnection ($iInitiator, $iContent);
+        if(empty($aConnection) || !is_array($aConnection))
+            return 0;
+
+        return (int)$aConnection['relation'];
+    }
+
+    public function getRelationTranslation($iValue, $sUseValues = BX_DATA_VALUES_DEFAULT)
+    {
+        $aRelations = BxDolFormQuery::getDataItems($this->_sPreList, false, $sUseValues);
+
+        return !empty($aRelations[$iValue]) ? $aRelations[$iValue] : _t('_uknown');
+    }
+
+    public function getRelationTypes()
+    {
+        $sParam = getParam(self::$_sParamTypes);
+        if(empty($sParam))
+            return array();
+
+        return explode(',', $sParam);
+    }
+}

+ 1140 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolSearch.php

@@ -0,0 +1,1140 @@
+<?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 $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->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)
+                $sCode[$sKey] = $oEx->getSearchData();
+            else
+                $sCode .= $this->_bRawProcessing ? $oEx->processingRaw() : $oEx->processing();
+        }
+
+        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 $_bSingleSearch = true;
+    protected $_bLiveSearch = false;
+    protected $_sMetaType = '';
+    protected $_sMode = '';
+    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 ()
+    {
+        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 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 (defined('BX_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(MsgBox(_t($this->sDisplayEmptyMsgKey ? $this->sDisplayEmptyMsgKey : '_Empty'))) : '';
+        }
+        return $sCode;
+    }
+
+    function processingAPI () 
+    {
+        $sUnitType = 'content';
+        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 ($this->sUnitViewDefault == 'showcase' || $this->sUnitViewDefault == 'gallery')
+            $sUnit = 'card';
+        
+        return [
+            'module' => $this->oModule->getName(),
+            'unit' => 'general-' . $sUnitType . '-' . $sUnit,
+            'request_url' => '/api.php?r=' . $this->oModule->getName() . '/browse/&params[]=',
+            'data' => $this->decodeData($this->getSearchData()),
+            'params' => [
+                'per_page' => $this->aCurrent['paginate']['perPage'],
+                'start' => $this->aCurrent['paginate']['start'],
+                'type' => $this->_sMode,
+            ],
+        ];
+    }
+
+    function decodeData ($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;
+    }
+
+    /**
+     * 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 ()
+    {
+        $this->aPseud = $this->_getPseud();
+        $this->setConditionParams();
+        if ($this->aCurrent['paginate']['num'] > 0) {
+            $aData = $this->getSearchDataByParams();
+            return $aData;
+        }
+        return array();
+    }
+
+    /**
+     * 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']}`=`$sOn`.`{$aValue['mainField']}`");
+                $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 = '')
+    {
+        $aSql = array('ownFields'=>'', 'joinFields'=>'', 'order'=>'');
+
+        // searchFields
+        foreach ($this->aCurrent['ownFields'] as $sValue)
+            $aSql['ownFields'] .= $this->setFieldUnit($sValue, $this->aCurrent['table']);
+
+        // joinFields & join
+        $aJoins = $this->getJoins();
+        if (!empty($aJoins)) {
+            $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 (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 '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));
+    }
+
+    /**
+     * 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 ()
+    {
+        $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 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;
+
+        $this->aCurrent['paginate']['start'] = isset($this->aCurrent['paginate']['forceStart'])
+            ? (int)$this->aCurrent['paginate']['forceStart']
+            : (empty($_GET['start']) ? 0 : (int)$_GET['start']);
+
+        if ($this->aCurrent['paginate']['start'] < 0)
+            $this->aCurrent['paginate']['start'] = 0;
+    }
+
+    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);
+    }
+}
+
+/** @} */

+ 216 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolSession.php

@@ -0,0 +1,216 @@
+<?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
+ * @{
+ */
+
+if (!defined('BX_DOL_SESSION_LIFETIME'))
+    define('BX_DOL_SESSION_LIFETIME', 7*24*60*60);
+if (!defined('BX_DOL_SESSION_SKIP_UPDATE'))
+    define('BX_DOL_SESSION_SKIP_UPDATE', 30);
+define('BX_DOL_SESSION_COOKIE', 'memberSession');
+
+class BxDolSession extends BxDolFactory implements iBxDolSingleton
+{
+    protected $oDb;
+    protected $sId;
+    protected $iUserId;
+    protected $aData;
+
+    protected $bAutoLogout;	//--- Auto logout with broken session
+
+    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 = new BxDolSessionQuery();
+        $this->sId = '';
+        $this->iUserId = 0;
+        $this->aData = array();
+
+        $this->bAutoLogout = false; // when changing to 'true', it's better to increase BX_DOL_SESSION_LIFETIME to 1 month or so
+        if (defined('BX_SESSION_AUTO_LOGOUT'))
+            $this->bAutoLogout = BX_SESSION_AUTO_LOGOUT;
+    }
+
+    /**
+     * 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 BxDolSession();
+
+        if(!$GLOBALS['bxDolClasses'][__CLASS__]->getId())
+            $GLOBALS['bxDolClasses'][__CLASS__]->start();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    function start()
+    {
+        if (defined('BX_DOL_CRON_EXECUTE') || defined('BX_MANIFEST'))
+            return true;
+
+        if (getParam('sys_session_auth')) {
+            $this->exists($this->sId);
+        }
+        else {
+            if ($this->exists($this->sId)) {
+                if ($this->iUserId == getLoggedId())
+                    return true;
+                $this->destroy(false);
+            }
+        }
+
+		/**
+		 * Force logout a logged in user if his session wasn't found and required to be automatically recreated.
+		 * It's needed to avoid problems when different users are logged in at the same time under one account. 
+		 */
+		if($this->bAutoLogout && isLogged())
+			bx_logout();
+
+		// try to restore user's old session
+		if (!getParam('sys_session_auth') && isLogged() && defined('BX_DOL_SESSION_RESTORATION') && constant('BX_DOL_SESSION_RESTORATION')) {
+		    $this->sId = $this->oDb->getOldSession(getLoggedId());
+		    if ($this->sId)
+		        $this->exists($this->sId); // it exists for sure but required for initializing some data there
+        }
+
+		// if an old session has not been found then generate a new one
+		if (!$this->sId)
+            $this->sId = genRndPwd(32, true);
+
+        $iTime = time() + BX_DOL_SESSION_LIFETIME;
+        if (getParam('sys_session_auth') && $this->isValue('remember_me') && !$this->getValue('remember_me'))
+            $iTime = 0;
+        bx_setcookie(BX_DOL_SESSION_COOKIE, $this->sId, $iTime, 'auto', '', 'auto', true);
+
+        $this->save();
+        return true;
+    }
+
+    function destroy($bDeleteCookies = true)
+    {
+        if ($bDeleteCookies) {
+            bx_setcookie(BX_DOL_SESSION_COOKIE, '', time() - 86400, 'auto', '', 'auto', true);
+            unset($_COOKIE[BX_DOL_SESSION_COOKIE]);
+        }
+
+        $this->oDb->delete($this->sId);
+
+        $this->sId = '';
+        $this->iUserId = 0;
+        $this->aData = array();
+    }
+
+    function exists($sId = '')
+    {
+        if(empty($sId) && isset($_COOKIE[BX_DOL_SESSION_COOKIE]))
+            $sId = bx_process_input($_COOKIE[BX_DOL_SESSION_COOKIE]);
+
+        $mixedSession = $this->oDb->exists($sId);
+        if($mixedSession === false) 
+        	return false;
+
+		$this->sId = $mixedSession['id'];
+		$this->iUserId = (int)$mixedSession['user_id'];
+		$this->aData = unserialize($mixedSession['data']);
+
+		$this->oDb->update($this->sId);		//--- update session's time
+		return true;
+    }
+
+    function getId()
+    {
+        return $this->sId;
+    }
+
+    function setUserId($iUserId)
+    {
+        $this->iUserId = $iUserId;
+        $this->save();
+    }
+
+    function getUserId()
+    {
+        return $this->iUserId;
+    }
+
+    function setValue($sKey, $mixedValue)
+    {
+        if(empty($this->sId))
+            $this->start();
+
+        $this->aData[$sKey] = $mixedValue;
+        $this->save();
+    }
+
+    function unsetValue($sKey)
+    {
+        if(empty($this->sId))
+            $this->start();
+
+        unset($this->aData[$sKey]);
+
+        $this->save();
+    }
+
+	function isValue($sKey)
+    {
+        if(empty($this->sId))
+            $this->start();
+
+        return isset($this->aData[$sKey]);
+    }
+
+    function getValue($sKey)
+    {
+        if(empty($this->sId))
+            $this->start();
+
+        return isset($this->aData[$sKey]) ? $this->aData[$sKey] : false;
+    }
+
+    function getUnsetValue($sKey)
+    {
+        $mixedValue = $this->getValue($sKey);
+        if($mixedValue !== false)
+            $this->unsetValue($sKey);
+
+        return $mixedValue;
+    }
+
+    public function maintenance()
+    {
+        return $this->oDb->deleteExpired();
+    }
+
+    protected function save()
+    {
+        if($this->iUserId == 0)
+            $this->iUserId = getLoggedId();
+
+        $this->oDb->save($this->sId, array(
+            'user_id' => $this->iUserId,
+            'data' => serialize($this->aData)
+        ));
+    }
+}
+
+/** @} */

+ 418 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolStorageQuery.php

@@ -0,0 +1,418 @@
+<?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
+ * @{
+ */
+
+/**
+ * Database queries for File storage class.
+ * @see BxDolStorage
+ */
+class BxDolStorageQuery extends BxDolDb
+{
+    protected $_aObject;
+    protected $_sTableFiles;
+
+    public function __construct($aObject)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+        $this->_sTableFiles = '`' . $aObject['table_files'] . '`';
+    }
+
+    static public function getStorageObject ($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_storage` WHERE `object` = ?", $sObject);
+        
+        $aTmp = $oDb->getCache('sys_objects_storage_' . $sObject, 'getRow', $sQuery);
+        if ($aTmp)
+            return $aTmp;
+        
+        $aRow = $oDb->getRow($sQuery);
+        if (isset($aRow['params'])){
+            $mParams = unserialize($aRow['params']);
+            if ($oDb->isFieldExists($aRow['table_files'], 'dimensions')){
+                $aDim =  ['fields' => ['dimensions' => 'getFileDimensions']];
+                if (is_array($mParams)){
+                    $mParams = array_merge($mParams, $aDim);
+                }
+                else{
+                    $mParams = $aDim;
+                }
+            }
+
+            $aRow['params'] = serialize($mParams);
+        }
+        $oDb->setCache('sys_objects_storage_' . $sObject, $aRow);
+        return $aRow;
+    }
+
+    static public function getStorageObjects ()
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = "SELECT * FROM `sys_objects_storage`";
+        return $oDb->getAll($sQuery);
+    }
+
+    static public function getAllGhosts ($aParams)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = "SELECT * FROM `sys_storage_ghosts` WHERE 2>1 ";
+        if (isset($aParams['profile_id']))
+            $sQuery .= " AND profile_id = :profile_id";
+        if (isset($aParams['object']))
+            $sQuery .= " AND object = :object";
+        if (isset($aParams['content_id']))
+            $sQuery .= " AND content_id = :content_id";
+        if (isset($aParams['sort_field'])){
+			$sQuery .= " ORDER BY `" . $aParams['sort_field'] . "` " . (isset($aParams['sort_direction']) ? $aParams['sort_direction'] : '');
+			unset($aParams['sort_field']);
+			unset($aParams['sort_direction']);
+		}
+        return $oDb->getAll($sQuery, $aParams);
+    }
+    
+    public function changeStorageEngine ($sEngine)
+    {
+        $sQuery = $this->prepare("UPDATE `sys_objects_storage` SET `engine` = ? WHERE `object` = ?", $sEngine, $this->_aObject['object']);
+        return $this->query($sQuery);
+    }
+
+    public function getMimeTypeByExt($sExt)
+    {
+        $sQuery = $this->prepare("SELECT `mime_type` FROM `sys_storage_mime_types` WHERE `ext` = ?", $sExt);
+        return $this->getOne($sQuery);
+    }
+
+    public function getIconByExt($sExt)
+    {
+        $sQuery = $this->prepare("SELECT `icon` FROM `sys_storage_mime_types` WHERE `ext` = ?", $sExt);
+        return $this->getOne($sQuery);
+    }
+
+    public function getIconFontByExt($sExt)
+    {
+        $sQuery = $this->prepare("SELECT `icon_font` FROM `sys_storage_mime_types` WHERE `ext` = ?", $sExt);
+        return $this->getOne($sQuery);
+    }
+    
+    public function clearAllMimeTypes()
+    {
+        return $this->query("TRUNCATE TABLE `sys_storage_mime_types`");
+    }
+
+    public function addMimeType($sMimeType, $sExt, $sIcon = '', $sIconFont = '')
+    {
+        $sQuery = $this->prepare("INSERT IGNORE INTO `sys_storage_mime_types` SET `ext` = ?, `mime_type` = ?, `icon` = ?, `icon_font` = ?", $sExt, $sMimeType, $sIcon, $sIconFont);
+        return $this->query($sQuery);
+    }
+
+    public function getStorageObjectQuota()
+    {
+        return $this->_aObject;
+    }
+
+    public function updateStorageObjectQuota($iSize, $iNumber = 1)
+    {
+        $iTime = time();
+        $sQuery = $this->prepare("
+            UPDATE `sys_objects_storage`
+            SET `current_size` = `current_size` + ?, `current_number` = `current_number` + (?), `ts` = ?
+            WHERE `object` = ?",
+            $iSize, $iNumber, $iTime, $this->_aObject['object']
+        );
+        if ($this->query($sQuery)) {
+            $this->_aObject = $this->getStorageObject($this->_aObject['object']);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public function getUserQuota($iProfileId)
+    {
+        return BxDolProfileQuery::getInstance()->getProfileQuota($iProfileId);
+    }
+
+    public function updateUserQuota($iProfileId, $iSize, $iNumber = 1)
+    {
+        return BxDolProfileQuery::getInstance()->updateProfileQuota($iProfileId, $iSize, $iNumber);
+    }
+
+    public function addFile($iProfileId, $sLocalId, $sPath, $aFileName, $sMimeType, $sExt, $iSize, $iTime, $isPrivate, $aAdditionalFields = array())
+    {
+        $aBind = array('profile_id' => $iProfileId, 'local_id' => $sLocalId, 'path' => $sPath, 'file_name' => $aFileName, 'mime_type' => $sMimeType, 'ext' => $sExt, 'size' => $iSize, 'time' => $iTime, 'private' => $isPrivate ? 1 : 0);
+
+        $sQuery = "INSERT INTO " . $this->_sTableFiles . " SET
+            `profile_id` = :profile_id, `remote_id` = :local_id, `path` = :path, `file_name` = :file_name, `mime_type` = :mime_type, `ext` = :ext, `size` = :size, `added` = :time, `modified` = :time, `private` = :private";
+
+        if ($aAdditionalFields && is_array($aAdditionalFields)) {
+            foreach ($aAdditionalFields as $sField => $sValue) {
+                $aBind[$sField] = $sValue;
+                $sQuery .= ", `$sField` = :$sField";
+            }
+        }
+
+        return $this->query($sQuery, $aBind);
+    }
+
+    public function modifyFilePrivate($iFileId, $isPrivate)
+    {
+        return $this->modifyCustomField($iFileId, 'private', $isPrivate ? 1 : 0);
+    }
+
+    public function modifyCustomField($iFileId, $sField, $sValue, $isUpdateModifiedField = true)
+    {
+    	$aBindings = array(
+    		$sField => $sValue,
+    		'id' => $iFileId
+    	);
+    	
+        $sAdd = '';
+        if ($isUpdateModifiedField) {
+        	$aBindings['modified'] = time();
+
+            $sAdd = ", `modified` = :modified";
+        }
+
+        $sQuery = "UPDATE " . $this->_sTableFiles . " SET `{$sField}` = :{$sField} {$sAdd} WHERE `id` = :id";
+        return $this->query($sQuery, $aBindings);
+    }
+
+    public function deleteFile($iFileId)
+    {
+        // delete queued record for the file
+        $sQuery = $this->prepare("DELETE FROM `sys_storage_deletions` WHERE `object` = ? AND `file_id` = ?", $this->_aObject['object'], $iFileId);
+        $this->query($sQuery);
+
+        // delete file record
+        $sQuery = $this->prepare("DELETE FROM " . $this->_sTableFiles . " WHERE `id` = ?", $iFileId);
+        if (!$this->query($sQuery))
+            return false;
+
+        // delete any file traces in ghosts table
+        $sQuery = $this->prepare("DELETE FROM `sys_storage_ghosts` WHERE `object` = ? AND `id` = ?", $this->_aObject['object'], $iFileId);
+        $this->query($sQuery);
+
+        return true;
+    }
+
+    public function getFileByFileName($sValue)
+    {
+        return $this->_getFileBy('`file_name`', $sValue);
+    }
+
+    public function getFileById($sValue)
+    {
+        return $this->_getFileBy('`id`', $sValue);
+    }
+
+    public function getFileByRemoteId($sValue)
+    {
+        return $this->_getFileBy('`remote_id`', $sValue);
+    }
+
+    protected function _getFileBy($sField, $sValue)
+    {
+        $sQuery = $this->prepare("SELECT * FROM " . $this->_sTableFiles . " WHERE " . $sField . " = ?", $sValue);
+        return $this->getRow($sQuery);
+    }
+
+    public function isTokenValid($iFileId, $sToken)
+    {
+        $iTime = time();
+        $sQuery = $this->prepare("SELECT `created` FROM `sys_storage_tokens` WHERE `id` = ? AND `object` = ? AND `hash` = ? AND `created` > ?", $iFileId, $this->_aObject['object'], $sToken, $iTime - $this->_aObject['token_life']);
+        return $this->getOne($sQuery) ? true : false;
+    }
+
+    public function genToken($iFileId)
+    {
+        $iTime = time();
+        $sToken = md5($iTime . mt_rand() . BX_DOL_SECRET);
+        $sQuery = $this->prepare("INSERT INTO `sys_storage_tokens` SET `id` = ?, `object` = ?, `hash` = ?, `created` = ?", $iFileId, $this->_aObject['object'], $sToken, $iTime);
+        if ($this->query($sQuery))
+            return $sToken;
+        else
+            return false;
+    }
+
+    public function insertGhosts($mixedFileIds, $iProfileId, $iContentId = 0)
+    {
+        $iTime = time();
+        if (!is_array($mixedFileIds))
+            $mixedFileIds = array($mixedFileIds);
+
+        $iCount = 0;
+        foreach ($mixedFileIds as $iFileId) {
+            $sQuery = $this->prepare("INSERT INTO `sys_storage_ghosts`
+                SET `id` = ?, `object` = ?, `profile_id` = ?, `content_id` = ?, `created` = ?
+                ON DUPLICATE KEY UPDATE `profile_id` = ?, `content_id` = ?, `created` = ?", $iFileId, $this->_aObject['object'], $iProfileId, $iContentId, $iTime, $iProfileId, $iContentId, $iTime);
+            $iCount += $this->query($sQuery);
+        }
+        return $iCount;
+    }
+
+    public function updateGhostOrder($iProfileId, $iContentId, $iFileId, $iOrder)
+    {
+        $aBindings = array(
+            'object' => $this->_aObject['object'],
+            'content_id' => $iContentId,
+            'id' => $iFileId,
+            'order' => $iOrder,
+        );
+
+        $sWhereClause = " AND `object`=:object AND `content_id`=:content_id AND `id`=:id";
+
+        if(!empty($iProfileId)) {
+            $aBindings['profile_id'] = $iProfileId;
+            
+            $sWhereClause .= " AND `profile_id`=:profile_id";
+        }
+
+        return $this->query("UPDATE `sys_storage_ghosts` SET `order`=:order WHERE 1" . $sWhereClause, $aBindings) !== false;
+    }
+
+    public function updateGhostsContentId($mixedFileIds, $iProfileId, $iContentId, $aProfiles = array(), $isAdmin = false)
+    {
+        $aBindings = array(
+            'content' => $iContentId,
+    		'profile' => $iProfileId,
+    		'object' => $this->_aObject['object']
+        );
+
+        $sSetAddon = '';
+        $sWhere = " AND `profile_id` = :profile ";
+        if ($aProfiles || $isAdmin) {
+            $sSetAddon = ', `profile_id` = :profile ';
+            $sWhere = $isAdmin ? "" : " AND `profile_id` IN(" . $this->implode_escape($aProfiles) . ")";
+        }
+        
+        $sQuery = $this->prepare("UPDATE `sys_storage_ghosts` SET `content_id` = :content $sSetAddon WHERE `object` = :object $sWhere AND `id` IN (" . $this->implode_escape($mixedFileIds) . ")");
+        return $this->res($sQuery, $aBindings);
+    }
+
+    public function deleteGhosts($mixedFileIds, $iProfileId, $iContentId = false)
+    {
+    	$aBindings = array(
+    		'profile_id' => $iProfileId,
+    		'object' => $this->_aObject['object']
+    	);
+        $sQuery = "DELETE FROM `sys_storage_ghosts` WHERE `profile_id` = :profile_id AND `object` = :object AND `id` IN (" . $this->implode_escape($mixedFileIds) . ")";
+        if (false !== $iContentId) {
+        	$aBindings['content_id'] = $iContentId;
+
+            $sQuery .= " AND `content_id` = :content_id";
+        }
+        $iCount = $this->query($sQuery, $aBindings);
+        if ($iCount)
+            $this->query("OPTIMIZE TABLE `sys_storage_ghosts`");
+        return $iCount;
+    }
+
+    public function getGhosts($mixedProfileId, $iContentId = false, $isAdmin = false)
+    {
+        return $this->getFiles($mixedProfileId, true, $iContentId, $isAdmin);
+    }
+
+    public function getGhost($iFileId)
+    {
+        $sQuery = $this->prepare("SELECT * FROM `sys_storage_ghosts` WHERE `object` = ? AND `id` = ?", $this->_aObject['object'], $iFileId);
+        return $this->getRow($sQuery);
+    }
+
+    public function getFiles($mixedProfileId, $isGhostsOnly = false, $iContentId = false, $isAdmin = false)
+    {
+        $aBindings = array();        
+
+        $sWhere = '';
+        $sJoin = '';
+        $sOrder = '';
+        if ($isGhostsOnly) {
+            $aBindings['object'] = $this->_aObject['object'];
+
+            $sOnProfile = '';
+            if ($isAdmin && $iContentId) {
+                // don't check profile id for admins, so admin can edit any entry
+            }
+            elseif (is_array($mixedProfileId) && $mixedProfileId) {
+                $sOnProfile = " AND `g`.`profile_id` IN(" . $this->implode_escape($mixedProfileId) . ") ";
+            } 
+            elseif (!is_array($mixedProfileId) && false !== $mixedProfileId) {
+                $aBindings['profile_id'] = $mixedProfileId;
+                
+                $sOnProfile = " AND `g`.`profile_id` = :profile_id ";                
+            }
+            
+            $sJoin .= " INNER JOIN `sys_storage_ghosts` AS `g` ON (`f`.`id` = `g`.`id` AND `g`.`object` = :object " . $sOnProfile;
+            if (false !== $iContentId) {
+                $aBindings['content_id'] = $iContentId;
+
+                $sJoin .= " AND `g`.`content_id` = :content_id";
+            }
+
+            $sJoin .= ')';
+
+            $sOrder = ' ORDER BY `g`.`order` ASC, `g`.`created` DESC';
+        } 
+        else {
+            if (is_array($mixedProfileId) && $mixedProfileId) {
+                $sWhere = " WHERE `f`.`profile_id` IN(" . $this->implode_escape($mixedProfileId) . ") ";
+            }
+            elseif (!is_array($mixedProfileId) && false !== $mixedProfileId) {
+                $aBindings['profile_id'] = $mixedProfileId;
+
+                $sWhere = " WHERE `f`.`profile_id` = :profile_id ";
+            }
+        }
+
+        $sQuery = "SELECT `f`.* FROM " . $this->_sTableFiles . " AS `f` " . $sJoin . $sWhere . $sOrder;
+        return $this->getAll($sQuery, $aBindings);
+    }
+
+    public function getFilesAll($iStart, $iPerPage)
+    {
+        $sQuery = $this->prepare("SELECT * FROM " . $this->_sTableFiles . " LIMIT ?, ?", (int)$iStart, (int)$iPerPage);
+        return $this->getAll($sQuery);
+    }
+
+    public function prune()
+    {
+        $iTime = time();
+        $sQuery = $this->prepare("DELETE FROM `sys_storage_tokens` WHERE `object` = ? AND `created` < ?", $this->_aObject['object'], $iTime - $this->_aObject['token_life']);
+        $iCount = $this->query($sQuery);
+        if ($iCount)
+            $this->query("OPTIMIZE TABLE `sys_storage_tokens`");
+        return $iCount;
+    }
+
+    public function queueFilesForDeletion ($a)
+    {
+        $iTime = time();
+        $iAdded = 0;
+        foreach ($a as $iFileId) {
+            $sQuery = $this->prepare("INSERT IGNORE INTO `sys_storage_deletions` SET `object` = ?, `file_id` = ?, `requested` = ?", $this->_aObject['object'], (int)$iFileId, $iTime);
+            $iAdded += ($this->query($sQuery) ? 1 : 0);
+        }
+        return $iAdded;
+    }
+
+    public static function getQueuedFilesForDeletion ($iLimit = 1000)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT `object`, `file_id` FROM `sys_storage_deletions` ORDER BY `requested` ASC LIMIT ?", $iLimit);
+        return $oDb->getAll($sQuery);
+    }
+
+    public static function isQueuedFilesForDeletion ($sPrefix)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT COUNT(*) FROM `sys_storage_deletions` WHERE `object` LIKE ?", $sPrefix . '%');
+        return $oDb->getOne($sQuery);
+    }
+}
+
+/** @} */

+ 3548 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolTemplate.php

@@ -0,0 +1,3548 @@
+<?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
+ * @{
+ */
+
+ini_set('pcre.backtrack_limit', 1000000);
+
+define('BX_DOL_TEMPLATE_INJECTIONS_CACHE', 'sys_injections.inc');
+
+define('BX_DOL_TEMPLATE_CHECK_IN_BOTH', 'both');
+define('BX_DOL_TEMPLATE_CHECK_IN_BASE', 'base');
+define('BX_DOL_TEMPLATE_CHECK_IN_TMPL', 'tmpl');
+
+define('BX_DOL_COLOR_BG', 'bg');
+define('BX_DOL_COLOR_FT', 'ft');
+
+define('BX_DOL_PAGE_WIDTH', '1024px');
+
+/**
+ * Page display levels.
+ * Note. Both levels may refer to the same HTML templates. 
+ * 
+ * 'System' level. It uses page's NameIndex and
+ * is mainly used for pages which aren't registered 
+ * in 'sys_objects_page' table. Also it's used 
+ * in 'Injections' engine.  
+ */
+define('BX_PAGE_DEFAULT', 0); ///< default, regular page
+define('BX_PAGE_CLEAR', 2); ///< clear page, without any headers and footers
+define('BX_PAGE_EMBED', 22); ///< page used for embeds
+define('BX_PAGE_POPUP', 44); ///< popup page, without any headers and footers
+define('BX_PAGE_TRANSITION', 150); ///< transition page with redirect to display some msg, like 'please wait', without headers footers
+
+/**
+ * 'Builder based' level. It uses page's Type 
+ * from BxDolPage class and is used for pages 
+ * which are registered in 'sys_objects_page' table.
+ * Changeable in Studio -> Pages Builder -> Settings. 
+ */
+define('BX_PAGE_TYPE_DEFAULT', 1); ///< default, depends on the settins
+define('BX_PAGE_TYPE_DEFAULT_WO_HF', 2); ///< clear page, without any headers and footers
+define('BX_PAGE_TYPE_STANDARD', 3); ///< regular page divided on columns
+define('BX_PAGE_TYPE_APPLICATION', 4); ///< regular page divided on columns with left vertical menu(s) column
+
+/**
+ * Template engine.
+ *
+ * An object of the class allows to:
+ *  1. Manage HTML templates.
+ *  2. Get URL/path for any template image/icon.
+ *  3. Attach CSS/JavaScript files to the output.
+ *  4. Add some content to any template key using Injection engine.
+ *
+ *
+ * Avalable constructions.
+ *  1. &lt;bx_include_auto:template_name.html /&gt; - the content of the file would be inserted. File would be taken from current template if it existes there, and from base directory otherwise.
+ *  2. &lt;bx_include_base:template_name.html /&gt; - the content of the file would be inserted. File would be taken from base directory.
+ *  3. &lt;bx_include_tmpl:template_name.html /&gt; - the content of the file would be inserted. File would be taken from tmpl_xxx directory.
+ *  4. &lt;bx_url_root /&gt; - the value of BX_DOL_URL_ROOT variable will be inserted.
+ *  5. &lt;bx_url_admin /&gt; - the value of BX_DOL_URL_ADMIN variable will be inserted.
+ *  6. &lt;bx_text:_language_key /&gt; - _language_key will be translated using language file(function _t()) and inserted.
+       &lt;bx_text_js:_language_key /&gt; - _language_key will be translated using language file(function _t()) and inserted, use it to insert text into js string.
+       &lt;bx_text_attribute:_language_key /&gt; - _language_key will be translated using language file(function _t()) and inserted, use it to insert text into html attribute.
+ *  7. &lt;bx_image_url:image_file_name /&gt; - image with 'image_file_name' file name will be searched in the images folder of current template.
+ *     If it's not found, then it will be searched in the images folder of base template. On success full URL will be inserted, otherwise an empty string.
+ *  8. &lt;bx_icon_url:icon_file_name /&gt; - the same with &lt;bx_image_url:image_file_name /&gt;, but icons will be searched in the images/icons/ folders.
+ *  9. &lt;bx_injection:injection_name /&gt; - will be replaced with injections registered with the page and injection_name in the `sys_injections`/`sys_injections_admin`/ tables.
+ *  10. &lt;bx_if:tag_name&gt;some_HTML&lt;/bx_if:tag_name&gt; - will be replaced with provided content if the condition is true, and with empty string otherwise.
+ *  11. &lt;bx_repeat:cycle_name&gt;some_HTML&lt;/bx_repeat:cycle_name&gt; - an inner HTML content will be repeated in accordance with received data.
+ *
+ *
+ * Related classes:
+ *  BxDolTemplateAdmin - for processing admin templates.
+ *  Template classes in modules - for processing modiles' templates.
+ *
+ *
+ * Global variables:
+ *  oSysTemplate - is used for template processing in user part.
+ *  oAdmTemplate - is used for template processing in admin part.
+ *
+ *
+ * Add injection:
+ *  1. Register it in the `sys_injections` table or `sys_injections_admin` table for admin panel.
+ *  2. Clear injections cache(sys_injections.inc and sys_injections_admin.inc in cache folder).
+ *
+ *
+ * Predefined template keys to add injections:
+ *  1. injection_head - add injections in the &lt;head&gt; tag.
+ *  2. injection_body - add ingection(attribute) in the &lt;body&gt; tag.
+ *  3. injection_header - add injection inside the &lt;body&gt; tag at the very beginning.
+ *  4. injection_logo_before - add injection at the left of the main logo(inside logo's DIV).
+ *  5. injection_logo_after - add injection at the right of the main logo(inside logo's DIV).
+ *  6. injection_between_logo_top_menu - add injection between logo and top menu.
+ *  7. injection_top_menu_before - add injection at the left of the top menu(inside top menu's DIV).
+ *  8. injection_top_menu_after - add injection at the right of the top menu(inside top menu's DIV).
+ *  13. injection_content_before - add injection just before main content(inside content's DIV).
+ *  14. injection_content_after - add injection just after main content(inside content's DIV).
+ *  15. injection_between_content_footer - add injection between content and footer.
+ *  16. injection_footer_before - add injection at the left of the footer(inside footer's DIV).
+ *  17. injection_footer_after - add injection at the right of the footer(inside footer's DIV).
+ *  18. injection_footer - add injection inside the &lt;body&gt; tag at the very end.
+ *
+ *
+ * Example of usage:
+ * @code
+ *  $oSysTemplate = BxDolTemplate::getInstance();
+ *
+ *  $oSysTemplate->addCss(array('test1.css', 'test2.css'));
+ *  $oSysTemplate->addJs(array('test1.js', 'test2.js'));
+ *  $oSysTemplate->parseHtmlByName('messageBox.html', array(
+ *    'id' => $iId,
+ *     'msgText' => $sText,
+ *     'bx_if:timer' => array(
+ *        'condition' => $iTimer > 0,
+ *        'content' => array(
+ *           'id' => $iId,
+ *           'time' => 1000 * $iTimer,
+ *           'on_close' => $sOnClose,
+ *        )
+ *     ),
+ *     'bx_if:timer' => array(
+ *        array(
+ *           'name' => $sName,
+ *           'title' => $sTitle
+ *        ),
+ *        array(
+ *           'name' => $sName,
+ *           'title' => $sTitle
+ *        )
+ *     )
+ *  ));
+ * @endcode
+ *
+ *
+ * Memberships/ACL:
+ * Doesn't depend on user's membership.
+ *
+ *
+ * Alerts:
+ * no alerts available
+ *
+ */
+class BxDolTemplate extends BxDolFactory implements iBxDolSingleton
+{
+    protected static $_sColorClassPrefix = 'col-';
+    protected static $_sColorClassPrefixBg = 'bg-col-';
+    protected static $_aColors = array(
+    	'red1' => array(216, 9, 96), 
+    	'red1-dark' => array(194, 7, 86), 
+    	'red2' => array(231, 68, 30), 
+    	'red2-dark' => array(207, 60, 25), 
+    	'red3' => array(243, 143, 0), 
+    	'red3-dark' => array(218, 128, 0), 
+    	'green1' => array(96, 174, 0), 
+    	'green1-dark' => array(86, 156, 0), 
+    	'green2' => array(209, 211, 0), 
+    	'green2-dark' => array(186, 188, 0), 
+    	'green3' => array(48, 116, 36), 
+    	'green3-dark' => array(43, 104, 32), 
+    	'blue1' => array(10, 61, 143), 
+    	'blue1-dark' => array(9, 54, 128), 
+    	'blue2' => array(0, 164, 165), 
+    	'blue2-dark' => array(0, 146, 148), 
+    	'blue3' => array(0, 160, 206), 
+    	'blue3-dark' => array(0, 143, 184), 
+    	'gray' => array(97, 97, 97), 
+    	'gray-dark' => array(87, 87, 87)
+    );
+
+    protected static $_aImages;
+    protected static $_sImagesCacheKey;
+    protected static $_iImagesCacheTTL;
+
+    /**
+     * Main fields
+     */
+    protected $_sName;
+    protected $_sPrefix;
+    protected $_sRootPath;
+    protected $_sRootUrl;
+    protected $_sSubPath;
+    protected $_sInjectionsTable;
+    protected $_sInjectionsCache;
+    protected $_sCode;
+    protected $_sCodeKey;
+    protected $_iMix;
+    protected $_sMixKey;
+    protected $_sKeyWrapperHtml;
+    protected $_sFolderHtml;
+    protected $_sFolderCss;
+    protected $_sFolderImages;
+    protected $_sFolderIcons;
+    protected $_aTemplates;
+
+    protected $_aLocations;
+    protected $_aLocationsJs;
+
+    /**
+     * Cache related fields
+     */
+    protected $_bCacheEnable;
+    protected $_sCacheFolderUrl;
+    protected $_sCachePublicFolderUrl;
+    protected $_sCachePublicFolderPath;
+    protected $_sCacheFilePrefix;
+    protected $_aCacheExceptions;
+
+    protected $_bImagesInline;
+    protected $_iImagesMaxSize;
+
+    protected $_bCssLess;
+    protected $_bCssCache;
+    protected $_bCssMinify;
+    protected $_bCssArchive;
+    protected $_sCssLessPrefix;
+    protected $_sCssCachePrefix;
+
+    protected $_bJsLess;
+    protected $_bJsCache;
+    protected $_bJsMinify;
+    protected $_bJsArchive;
+    protected $_sJsCachePrefix;
+
+    protected $aPage;
+    protected $aPageContent;
+    protected $aPageSnapshot = array();
+
+    protected $_oTemplateConfig;
+    protected $_oTemplateFunctions;
+
+    /**
+     * Constructor
+     */
+    protected function __construct($sRootPath = BX_DIRECTORY_PATH_ROOT, $sRootUrl = BX_DOL_URL_ROOT)
+    {
+        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->_sPrefix = 'BxDolTemplate';
+
+        $this->_sRootPath = $sRootPath;
+        $this->_sRootUrl = $sRootUrl;
+        $this->_sInjectionsTable = 'sys_injections';
+        $this->_sInjectionsCache = BX_DOL_TEMPLATE_INJECTIONS_CACHE;
+
+        $this->_sCodeKey = BX_DOL_TEMPLATE_CODE_KEY;
+        $this->_sMixKey = BX_DOL_TEMPLATE_MIX_KEY;
+        list(
+            $this->_sCode, 
+            $this->_sName, 
+            $this->_sSubPath
+        ) = self::retrieveCode($this->_sCodeKey, $this->_sMixKey, $this->_sRootPath);
+
+        $this->_iMix = 0;
+        if(is_array($this->_sCode))
+            list($this->_sCode, $this->_iMix) = $this->_sCode;
+
+        if(!$this->_sSubPath)
+            $this->_sSubPath = 'boonex/' . BX_DOL_TEMPLATE_DEFAULT_CODE . '/';
+
+        if(!file_exists(BX_DIRECTORY_PATH_MODULES . $this->_sSubPath)) // just for 8.0.0-A6 upgrade
+            $this->_sSubPath = 'boonex/uni/';
+
+        if(isset($_GET[$this->_sCodeKey])) {
+            if(BxDolPermalinks::getInstance()->redirectIfNecessary(array($this->_sCodeKey)))
+                exit;
+        }
+
+        $this->_sKeyWrapperHtml = '__';
+        $this->_sFolderHtml = '';
+        $this->_sFolderCss = 'css/';
+        $this->_sFolderImages = 'images/';
+        $this->_sFolderIcons = 'images/icons/';
+        $this->_aTemplates = array('html_tags', 'menu_item_addon', 'menu_item_addon_small', 'menu_item_addon_middle');
+
+        $this->addLocation('system', $this->_sRootPath, $this->_sRootUrl);
+
+        $this->addLocationJs('system_inc_js', BX_DIRECTORY_PATH_INC . 'js/' , BX_DOL_URL_ROOT . 'inc/js/');
+        $this->addLocationJs('system_inc_js_classes', BX_DIRECTORY_PATH_INC . 'js/classes/' , BX_DOL_URL_ROOT . 'inc/js/classes/');
+        $this->addLocationJs('system_plugins_public', BX_DIRECTORY_PATH_PLUGINS_PUBLIC, BX_DOL_URL_PLUGINS_PUBLIC);
+
+        $this->_bCacheEnable = !defined('BX_DOL_CRON_EXECUTE') && getParam('sys_template_cache_enable') == 'on';
+        $this->_sCacheFolderUrl = '';
+        $this->_sCachePublicFolderUrl = BX_DOL_URL_CACHE_PUBLIC;
+        $this->_sCachePublicFolderPath = BX_DIRECTORY_PATH_CACHE_PUBLIC;
+        $this->_sCacheFilePrefix = "bx_templ_";
+        $this->_aCacheExceptions = ['menu_icon.html'];
+
+        $this->_bImagesInline = getParam('sys_template_cache_image_enable') == 'on';
+        $this->_iImagesMaxSize = (int)getParam('sys_template_cache_image_max_size') * 1024;
+
+        $bArchive = getParam('sys_template_cache_compress_enable') == 'on';
+
+        $this->_bCssLess = true; //--- Less cannot be disabled for CSS.
+        $this->_bCssCache = !defined('BX_DOL_CRON_EXECUTE') && getParam('sys_template_cache_css_enable') == 'on';
+        $this->_bCssMinify = $this->_bCssCache && getParam('sys_template_cache_minify_css_enable') == 'on';
+        $this->_bCssArchive = $this->_bCssCache && $bArchive;
+        $this->_sCssLessPrefix = $this->_sCacheFilePrefix . 'less_';
+        $this->_sCssCachePrefix = $this->_sCacheFilePrefix . 'css_';
+
+        $this->_bJsLess = false; //--- Less language isn't available for JS at all.
+        $this->_bJsCache = !defined('BX_DOL_CRON_EXECUTE') && getParam('sys_template_cache_js_enable') == 'on';
+        $this->_bJsMinify = $this->_bJsCache && getParam('sys_template_cache_minify_js_enable') == 'on';
+        $this->_bJsArchive = $this->_bJsCache && $bArchive;
+        $this->_sJsCachePrefix = $this->_sCacheFilePrefix . 'js_';
+
+        $this->aPage = array();
+        $this->aPageContent = array();
+    }
+
+    /**
+     * 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 BxDolTemplate();
+            $GLOBALS['bxDolClasses'][__CLASS__]->init();
+        }
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    /**
+     * Retrieve template code and check whether it's associated with active template or not.
+     *
+     * @param string $sCodeKey template's code key.
+     * @param string $sMixKey template's mix key.
+     * @param string $sRootPath path to root directory.
+     */
+    public static function retrieveCode($sCodeKey = BX_DOL_TEMPLATE_CODE_KEY, $sMixKey = BX_DOL_TEMPLATE_MIX_KEY, $sRootPath = BX_DIRECTORY_PATH_ROOT)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $fCheckCode = function($sCode, $bSetCookie) use($sCodeKey, $sRootPath) {
+            if(empty($sCode) || !preg_match('/^[A-Za-z0-9_-]+$/', $sCode))
+                return false;
+
+            $aModule = BxDolModuleQuery::getInstance()->getModuleByUri($sCode);
+            if(empty($aModule) || !is_array($aModule) || (int)$aModule['enabled'] != 1 || !file_exists(BX_DIRECTORY_PATH_MODULES . $aModule['path'] . 'data/template/'))
+                return false;
+
+            $oConfig = new BxDolModuleConfig($aModule);
+
+            $aResult = array(
+                $oConfig->getUri(), //--- Template module's URI is used as template Code. 
+                $oConfig->getName(),
+                $oConfig->getDirectory()
+            );
+
+            if(!$bSetCookie || bx_get('preview'))
+                return $aResult;
+
+            bx_setcookie($sCodeKey, $sCode, time() + 60*60*24*365);
+
+            return $aResult;
+        };
+
+        $fCheckMix = function($aResult, $iMix, $bSetCookie) use($sMixKey, $sRootPath, $oDb) {
+            list($sCode, $sName) = $aResult;
+            if(empty($sName) || empty($iMix))
+                return false;
+
+            $aMix = $oDb->getParamsMix($iMix);
+            if(empty($aMix) || !is_array($aMix) || $aMix['type'] != $sName)
+                return false;
+
+            if(!$bSetCookie)
+                return $iMix;
+
+            bx_setcookie($sMixKey, $iMix, time() + 60*60*24*365);
+
+            return $iMix;
+        };
+
+        $sCode = getParam('template');
+        if(empty($sCode))
+            $sCode = BX_DOL_TEMPLATE_DEFAULT_CODE;
+        $aResult = $fCheckCode($sCode, false);
+
+        //--- Check selected template in COOKIE(the lowest priority) ---//
+        $sCode = !empty($_COOKIE[$sCodeKey]) ? $_COOKIE[$sCodeKey] : '';
+        $aResultCheck = $fCheckCode($sCode, false);
+        if($aResultCheck !== false)
+            $aResult = $aResultCheck;
+
+        //--- Check selected template in GET(the highest priority) ---//
+        $sCode = !empty($_GET[$sCodeKey]) ? $_GET[$sCodeKey] : '';
+        $aResultCheck = $fCheckCode($sCode, true);
+        if($aResultCheck !== false)
+            $aResult = $aResultCheck;
+
+        if($aResult === false) 
+            return $aResult;
+
+        if(!is_array($aResult[0]))
+            $aResult[0] = array($aResult[0]);
+
+        $iMixDefault = !empty($aResult[1]) ? (int)getParam($aResult[1] . '_default_mix') : 0;
+
+        //--- Check selected mix in COOKIE(the lowest priority) ---//
+        $iMix = !empty($_COOKIE[$sMixKey]) ? (int)$_COOKIE[$sMixKey] : 0;
+        $iResultCheck = $fCheckMix($aResult, $iMix, false);
+        if($iResultCheck !== false) {
+            $aMix = $oDb->getParamsMix($iMix);
+            if((int)$aMix['published'] == 0 && $iMix != $iMixDefault) {
+                $aUrl = parse_url(BX_DOL_URL_ROOT);
+                $sPath = isset($aUrl['path']) && !empty($aUrl['path']) ? $aUrl['path'] : '/';
+
+                setcookie($sMixKey, '', time() - 96 * 3600, $sPath);
+                unset($_COOKIE[$sMixKey]);
+            }
+            else
+                $aResult[0][1] = $iResultCheck;
+        }
+
+        //--- Check selected mix in GET(the highest priority) ---//
+        $iMix = !empty($_GET[$sMixKey]) ? (int)$_GET[$sMixKey] : 0;
+        $iResultCheck = $fCheckMix($aResult, $iMix, true);
+        if($iResultCheck !== false)
+            $aResult[0][1] = $iResultCheck;
+
+        //--- Get default mix for currently selected template ---//
+        if(empty($aResult[0][1]) && !empty($iMixDefault)) {
+            $iResultCheck = $fCheckMix($aResult, $iMixDefault, false);
+            if($iResultCheck !== false)
+                $aResult[0][1] = $iResultCheck;
+        }
+
+        if(is_array($aResult[0]) && count($aResult[0]) == 1)
+           $aResult[0] = $aResult[0][0];
+
+        return $aResult;
+    }
+
+    public function getIncludedUrls($sType)
+    {
+        if (!isset($this->aPage[$sType]))
+            return array();
+        $a = array();
+        foreach ($this->aPage[$sType] as $r)
+            $a[] = $r['url'];
+        return $a;
+    }
+
+    /**
+     * Remember current state of aPage variable with all css, js, etc
+     */
+    public function collectingStart()
+    {
+        $this->aPageSnapshot = $this->aPage;
+    }
+
+    public function collectingInject($aCss, $aJs)
+    {
+        $a = array('css' => 'aCss', 'js' => 'aJs');
+        foreach ($a as $s => $sVar) {
+            if (empty($$sVar))
+                continue;
+            $sKey = $s . '_compiled';
+            foreach ($$sVar as $r)
+                $this->aPage[$sKey][] = $r;
+        }
+    }
+
+    /**
+     * Get difference for non-system css and js files from previously remembered state as ready HTML code, 
+     * additionally filter out css and js from $aExcludeCss and $aExcludeJs arrays
+     */
+    public function collectingEndGetCode($aExcludeCss = array(), $aExcludeJs = array(), $sFormat = 'html')
+    {
+        if (!is_array($aExcludeCss))
+            $aExcludeCss = [];
+        if (!is_array($aExcludeJs))
+            $aExcludeJs = [];
+
+        $aPageSave = $this->aPage; // save current state to restore later
+
+        // filter funcs
+        $fFilterCss = function ($a) use ($aExcludeCss) {
+            if (in_array($a['url'], $aExcludeCss))
+                return false;
+            if (isset($this->aPageSnapshot['css_compiled']))
+                foreach ($this->aPageSnapshot['css_compiled'] as $r)
+                    if ($r['url'] == $a['url'])
+                        return false;
+            return true;
+        };
+        $fFilterJs = function ($a) use ($aExcludeJs) {
+            if (in_array($a['url'], $aExcludeJs))
+                return false;
+            if (isset($this->aPageSnapshot['js_compiled']))
+                foreach ($this->aPageSnapshot['js_compiled'] as $r)
+                    if ($r['url'] == $a['url'])
+                        return false;
+            return true;
+        };    
+
+        // diff aPageSnapshot and aPage and output only newly added css/js
+        $this->aPage['css_compiled'] = array_filter($this->aPage['css_compiled'], $fFilterCss);
+        $this->aPage['js_compiled'] = array_filter($this->aPage['js_compiled'], $fFilterJs);
+
+        // return js/css
+        $mixedRet = '';
+        if ('html' == $sFormat) {
+            $mixedRet .= $this->includeFiles('css');
+            $mixedRet .= $this->includeFiles('js');
+        }
+        else {
+            $mixedRet = array(
+                'css' => $this->aPage['css_compiled'],
+                'js' => $this->aPage['js_compiled'],
+            );
+        }
+
+        // restore original state
+        $this->aPageSnapshot = array();
+        $this->aPage = $aPageSave; 
+
+        return $mixedRet;
+    }
+
+    public function getClassName()
+    {
+        return get_class($this);
+    }
+
+    public static function getColorPalette()
+    {
+        $aResult = self::$_aColors;
+
+        bx_alert('system', 'get_color_palette', 0, false, array(
+            'override_result' => &$aResult
+        ));
+
+        if($aResult != self::$_aColors) {
+            $oTemplate = self::getInstance();
+            foreach($aResult as $sName => $aRgb) {
+                $sRgb = 'rgb(' . trim(implode(', ', $aRgb)) . ') !important';
+
+                $oTemplate->addCssStyle('.' . self::$_sColorClassPrefix . $sName, array(
+                    'color' => $sRgb
+                ));
+                $oTemplate->addCssStyle('.' . self::$_sColorClassPrefixBg . $sName, array(
+                    'background-color' => $sRgb
+                ));
+            }
+        }
+
+        return $aResult;
+    }
+
+    public static function getColorCode($mixedName = false, $fOpacity = false)
+    {
+        $aPalette = self::getColorPalette();
+        $aClasses = array_keys($aPalette);
+
+        if($mixedName === false || (is_string($mixedName) && !is_numeric($mixedName) && !in_array($mixedName, $aClasses)))
+            $mixedName = $aClasses[rand(0, count($aClasses) - 1)];
+        else if(is_numeric($mixedName))
+            $mixedName = $aClasses[(int)$mixedName % count($aClasses)];
+
+        $aColor = $aPalette[$mixedName];
+        if($fOpacity !== false && is_numeric($fOpacity))
+            $aColor[] = $fOpacity;
+
+        return $aColor;
+    }
+
+    public static function getColorClass($sType = BX_DOL_COLOR_FT, $sName = '')
+    {
+        $aClasses = array_keys(self::getColorPalette());
+
+        if(empty($sName) || !in_array($sName, $aClasses))
+            $sName = $aClasses[rand(0, count($aClasses) - 1)];
+
+        $sPrefix = '';
+        switch ($sType) {
+            case BX_DOL_COLOR_FT:
+                $sPrefix = self::$_sColorClassPrefix;
+                break;
+
+            case BX_DOL_COLOR_BG:
+                $sPrefix = self::$_sColorClassPrefixBg;
+                break;
+        }
+
+        return $sPrefix . $sName;
+    }
+
+    /**
+     * Load templates.
+     */
+    function loadTemplates()
+    {
+        $aResult = array();
+        foreach($this->_aTemplates as $sName)
+            $aResult[$sName] = $this->getHtml($sName . '.html');
+        $this->_aTemplates = $aResult;
+    }
+    /**
+     * Initialize template engine.
+     * Note. The method is executed with the system, you shouldn't execute it in your subclasses.
+     */
+    function init()
+    {
+        $this->loadTemplates();
+
+        //--- Load page elements related static variables ---//
+        $this->aPage = array(
+            'name_index' => BX_PAGE_DEFAULT,
+            'type' => BX_PAGE_TYPE_DEFAULT,
+            'url' => '',
+            'header' => '',
+            'header_text' => '',
+            'keywords' => array(),
+            'location' => array(),
+            'description'  => '',
+            'robots' => '',
+            'base' => ['href' =>  BX_DOL_URL_ROOT],
+            'css_name' => array(),
+            'css_compiled' => array(),
+            'css_system' => array(),
+            'css_async' => array(),
+            'js_name' => array(),
+            'js_compiled' => array(),
+            'js_system' => array(),
+            'js_options' => array(),
+            'js_translations' => array(),
+            'js_images' => array(),
+            'injections' => array()
+        );
+
+        //--- Load default CSS, JS, etc ---//
+        BxDolPreloader::getInstance()->perform($this);
+
+        //--- Load injection's cache ---//
+        if (getParam('sys_db_cache_enable')) {
+            $oDb = BxDolDb::getInstance();
+            $oCache = $oDb->getDbCacheObject();
+            $sCacheKey = $oDb->genDbCacheKey($this->_sInjectionsCache);
+
+            $aInjections = $oCache->getData($sCacheKey);
+            if ($aInjections === null) {
+                $aInjections = $this->getInjectionsData();
+                $oCache->setData ($sCacheKey, $aInjections);
+            }
+        } 
+        else
+            $aInjections = $this->getInjectionsData();
+
+        $this->aPage['injections'] = $aInjections;
+
+        //--- Load images/icons cache ---//
+        $this->initImages();
+
+        bx_import('BxTemplConfig'); // TODO: for some reason autoloader isn't working here....
+        $this->_oTemplateConfig = BxTemplConfig::getInstance();
+
+        bx_import('BxTemplFunctions');
+        $this->_oTemplateFunctions = BxTemplFunctions::getInstance($this);
+
+        $this->addJsOption('sys_fixed_header');
+        $this->addJsOption('sys_confirmation_before_redirect');
+    }
+    
+    protected function initImages()
+    {
+        self::$_iImagesCacheTTL = 86400;
+        self::$_sImagesCacheKey = 'db_layout_images_' . $this->_sCode .  '_' . bx_site_hash('images') . '.php';
+        self::$_aImages = BxDolDb::getInstance()->getDbCacheObject()->getData(self::$_sImagesCacheKey);
+        if(!self::$_aImages)
+            self::$_aImages = [];
+
+        bx_alert('system', 'get_layout_images', 0, false, [
+            'code' => $this->_sCode,
+            'override_result' => &self::$_aImages
+        ]);
+    }
+
+    protected function saveImages()
+    {
+        if(!self::$_iImagesCacheTTL)
+            $this->initImages();
+
+        BxDolDb::getInstance()->getDbCacheObject()->setData(self::$_sImagesCacheKey, self::$_aImages, self::$_iImagesCacheTTL);
+    }
+
+    protected function getInjectionsData ()
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $aInjections = $oDb->getAll("SELECT `page_index`, `name`, `key`, `type`, `data`, `replace` FROM `" . $this->_sInjectionsTable . "` WHERE `active`='1'");
+        if (!$aInjections)
+            return array();
+
+        foreach ($aInjections as $aInjection)
+            $aInjections['page_' . $aInjection['page_index']][$aInjection['key']][] = $aInjection;
+
+        return $aInjections;
+    }
+
+    /**
+     * Set page name index
+     * @param int $i name index
+     */
+    function setPageNameIndex($i)
+    {
+        $this->aPage['name_index'] = $i;
+    }
+
+    /**
+     * Get page name index
+     * @return int $i name index
+     */
+    function getPageNameIndex()
+    {
+        return isset($this->aPage['name_index']) ? (int)$this->aPage['name_index'] : 0;
+    }
+
+	/**
+     * Set page type
+     * @param int $i page type
+     */
+    function setPageType($i)
+    {
+        $this->aPage['type'] = $i;
+    }
+    
+    /**
+     * Set page url
+     * @param string $s page url
+     */
+    function setPageUrl($s)
+    {
+        $this->aPage['url'] = $s;
+    }
+
+    /**
+     * Get page type
+     * @return int $i page type
+     */
+    function getPageType()
+    {
+        $iType = BX_PAGE_TYPE_DEFAULT;
+        if(isset($this->aPage['type']))
+            $iType = (int)$this->aPage['type'];
+
+        if($iType == BX_PAGE_TYPE_DEFAULT) 
+            $iType = (int)getParam('sys_pt_default_' . (isLogged() ? 'member' : 'visitor'));
+
+        return $iType;
+    }
+
+    /**
+     * Set page header
+     * @param string $s page header
+     */
+    function setPageHeader($s)
+    {
+        $this->aPage['header'] = $s;
+    }
+
+    /**
+     * Get page header
+     * @return string $s page header
+     */
+    function getPageHeader()
+    {
+        return $this->aPage['header'];
+    }
+
+    /**
+     * Set page params. Available page params are: name_index, header
+     * @param array $a page params
+     */
+    function setPageParams($a)
+    {
+        if (!empty($this->aPage))
+            $this->aPage = array_merge($this->aPage, $a);
+        else
+            $this->aPage = $a;
+    }
+
+    /**
+     * Get page params.
+     * @return array $a page params
+     */
+    function getPageParams()
+    {
+        return $this->aPage;
+    }
+
+    /**
+     * Set page description.
+     *
+     * @param string $sDescription necessary page description.
+     */
+    function setPageDescription($sDescription)
+    {
+        $this->aPage['description'] = $sDescription;
+    }
+
+    /**
+     * Set page meta robots.
+     *
+     * @param string $s page meta robots.
+     */
+    function setPageMetaRobots($s)
+    {
+        $this->aPage['robots'] = $s;
+    }
+
+    /**
+     * Set page injections.
+     *
+     * @param array $aInjections name => value injections.
+     */
+    function setPageInjections($aInjections)
+    {
+        if(empty($aInjections) || !is_array($aInjections))
+            return;
+
+        foreach($aInjections as $sName => $sValue)
+            $this->addInjection('injection_' . $sName, 'text', $sValue);
+    }
+    
+    /**
+     * Set page content for some variable.
+     * @param string $sVar     name of content variable
+     * @param string $sContent content for $sVar variable
+     * @param int    $iIndex   optional page index, default is index which was set before with @see setPageNameIndex function, or 0
+     */
+    function setPageContent($sVar, $sContent, $iIndex = false)
+    {
+        $i = false !== $iIndex ? $iIndex : $this->getPageNameIndex();
+        $this->aPageContent[$i][$sVar] = $sContent;
+    }
+
+    /**
+     * Get page content for some variable.
+     * @param  string $sVar   name of content variable
+     * @param  int    $iIndex optional page index, default is index which was set before with @see setPageNameIndex function, or 0
+     * @return string page content for some variable or for the whole page.
+     */
+    function getPageContent($sVar = false, $iIndex = false)
+    {
+        $i = false !== $iIndex ? $iIndex : $this->getPageNameIndex();
+        return false !== $sVar ? $this->aPageContent[$i][$sVar] : $this->aPageContent[$i];
+    }
+
+    /**
+     * Get currently active template name.
+     *
+     * @return string template's name.
+     */
+    function getName()
+    {
+        return $this->_sName;
+    }
+    
+    /**
+     * Get currently active template name.
+     *
+     * @return string template's name.
+     */
+    function getCssClassName()
+    {
+        return str_replace('_', '-', $this->_sName);
+    }
+
+    /**
+     * Get currently active template code.
+     *
+     * @return string template's code.
+     */
+    function getCode()
+    {
+        return $this->_sCode;
+    }
+    
+    /**
+     * Get embed code.
+     *
+     * @return string embed's code.
+     */
+    function getEmbed($sContent)
+    {
+        if ($sContent == ''){
+            header('Content-Security-Policy: frame-ancestors ' . getParam('sys_csp_frame_ancestors')) ;
+            $this->displayPageNotFound('', BX_PAGE_EMBED);
+            exit;
+        }
+        
+        $this->addJs(['inc/js/|embed.js']);
+        $this->addCss(['embed.css']);
+        $this->aPage['base']['target'] = '_blank';
+        $this->setPageNameIndex (BX_PAGE_EMBED);
+        $this->setPageContent('page_main_code', '<div class="bx-embed">' . $sContent . '</div>');
+        $this->getPageCode();
+    }
+
+    /**
+     * Get code key.
+     *
+     * @return string template's code key.
+     */
+    function getCodeKey()
+    {
+        return $this->_sCodeKey;
+    }
+
+    /**
+     * Get currently active template mix.
+     *
+     * @return integer template's mix.
+     */
+    function getMix()
+    {
+        return $this->_iMix;
+    }
+
+    /**
+     * Get currently active template path.
+     *
+     * @return string template's path.
+     */
+    function getPath()
+    {
+        return $this->_sSubPath;
+    }
+
+    /**
+     * Set page title.
+     * @deprecated use setPageHeader
+     *
+     * @param string $sTitle necessary page title.
+     */
+    function setPageTitle($sTitle)
+    {
+        $this->setPageHeader($sTitle);
+    }
+
+    /**
+     * Set page's main box title.
+     * @deprecated use setPageParams
+     *
+     * @param string $sTitle necessary page's main box title.
+     */
+    function setPageMainBoxTitle($sTitle)
+    {
+        $this->setPageParams(array('header_text' => $sTitle));
+    }
+
+    /**
+     * Check whether location exists or not.
+     * @param string $sKey - location's unique key.
+     */
+    function isLocation($sKey)
+    {
+        return isset($this->_aLocations[$sKey]);
+    }
+
+    /**
+     * Get a lis of all added locations.
+     * @return array with locations.
+     */
+    function getLocations()
+    {
+        return $this->_aLocations;
+    }
+
+    /**
+     * Add location in array of locations.
+     * Note. Location is the path/url to folder where 'templates' folder is stored.
+     *
+     * @param string $sKey          - location's unique key.
+     * @param string $sLocationPath - location's path. For modules: '[path_to_script]/modules/[vendor_name]/[module_name]/'
+     * @param string $sLocationUrl  - location's url. For modules: '[url_to_script]/modules/[vendor_name]/[module_name]/'
+     */
+    function addLocation($sKey, $sLocationPath, $sLocationUrl)
+    {
+        $this->_aLocations[$sKey] = array(
+            'path' => $sLocationPath,
+            'url' => $sLocationUrl,
+        );
+
+        return $sKey;
+    }
+    /**
+     * Add dynamic location.
+     *
+     * @param  string   $sLocationPath - location's path. For modules: '[path_to_script]/modules/[vendor_name]/[module_name]/'
+     * @param  string   $sLocationUrl  - location's url. For modules: '[url_to_script]/modules/[vendor_name]/[module_name]/'
+     * @return location key. Is needed to remove the location.
+     */
+    function addDynamicLocation($sLocationPath, $sLocationUrl)
+    {
+        $sLocationKey = time() . mt_rand();
+        $this->addLocation($sLocationKey, $sLocationPath, $sLocationUrl);
+
+        return $sLocationKey;
+    }
+    /**
+     * Remove location from array of locations.
+     * Note. Location is the path/url to folder where templates are stored.
+     *
+     * @param string $sKey - location's    unique key.
+     */
+    function removeLocation($sKey)
+    {
+        if(isset($this->_aLocations[$sKey]))
+           unset($this->_aLocations[$sKey]);
+    }
+	/**
+     * Check whether JS location exists or not.
+     * @param string $sKey - JS location's unique key.
+     */
+    function isLocationJs($sKey)
+    {
+        return isset($this->_aLocationsJs[$sKey]);
+    }
+    /**
+     * Add JS location in array of JS locations.
+     * Note. Location is the path/url to folder where JS files are stored.
+     *
+     * @param string $sKey          - location's    unique key.
+     * @param string $sLocationPath - location's path. For modules: '[path_to_script]/modules/[vendor_name]/[module_name]/js/'
+     * @param string $sLocationUrl  - location's url. For modules: '[url_to_script]/modules/[vendor_name]/[module_name]/js/'
+     */
+    function addLocationJs($sKey, $sLocationPath, $sLocationUrl)
+    {
+        $this->_aLocationsJs[$sKey] = array(
+            'path' => $sLocationPath,
+            'url' => $sLocationUrl
+        );
+
+        return $sKey;
+    }
+    /**
+     * Add dynamic JS location.
+     *
+     * @param  string   $sLocationPath - location's path. For modules: '[path_to_script]/modules/[vendor_name]/[module_name]/'
+     * @param  string   $sLocationUrl  - location's url. For modules: '[url_to_script]/modules/[vendor_name]/[module_name]/'
+     * @return location key. Is needed to remove the location.
+     */
+    function addDynamicLocationJs($sLocationPath, $sLocationUrl)
+    {
+        $sLocationKey = time() . mt_rand();
+        $this->addLocationJs($sLocationKey, $sLocationPath, $sLocationUrl);
+
+        return $sLocationKey;
+    }
+    /**
+     * Remove JS location from array of locations.
+     * Note. Location is the path/url to folder where templates are stored.
+     *
+     * @param string $sKey - JS location's    unique key.
+     */
+    function removeLocationJs($sKey)
+    {
+        if(isset($this->_aLocationsJs[$sKey]))
+           unset($this->_aLocationsJs[$sKey]);
+    }
+    /**
+     * Add Option in JS output.
+     *
+     * @param mixed $mixedName option's name or an array of options' names.
+     */
+    function addJsOption($mixedName)
+    {
+        if(is_string($mixedName))
+            $mixedName = array($mixedName);
+
+        foreach($mixedName as $sName)
+            $this->aPage['js_options'][$sName] = getParam($sName);
+    }
+    /**
+     * Add language translation for key in JS output.
+     *
+     * @param mixed $mixedKey language key or an array of keys.
+     */
+    function addJsTranslation($mixedKey, $bDynamic = false)
+    {
+        if(is_string($mixedKey))
+            $mixedKey = array($mixedKey);
+
+        foreach($mixedKey as $sKey)
+            $this->aPage['js_translations'][$sKey] = _t($sKey, '{0}', '{1}');
+
+        return $bDynamic ? $this->_processJsTranslations() : '';
+    }
+    /**
+     * Add image in JS output.
+     *
+     * @param array $aImages an array of image descriptors.
+     *                       The descriptor is a key/value pear in the array of descriptors.
+     */
+    function addJsImage($aImages)
+    {
+        if(!is_array($aImages))
+            return;
+
+        foreach($aImages as $sKey => $sFile) {
+            $sUrl = $this->getImageUrl($sFile);
+            if(empty($sUrl))
+                continue;
+
+            $this->aPage['js_images'][$sKey] = $sUrl;
+        }
+    }
+    /**
+     * Add icon in JS output.
+     *
+     * @param array $aIcons an array of icons descriptors.
+     *                      The descriptor is a key/value pear in the array of descriptors.
+     */
+    function addJsIcon($aIcons)
+    {
+        if(!is_array($aIcons))
+            return;
+
+        foreach($aIcons as $sKey => $sFile) {
+            $sUrl = $this->getIconUrl($sFile);
+            if(empty($sUrl))
+                continue;
+
+            $this->aPage['js_images'][$sKey] = $sUrl;
+        }
+    }
+	/**
+	 * Add CSS style.
+	 *
+	 * @param string $sName CSS class name.
+	 * @param string $sContent CSS class styles.
+	 */
+	function addCssStyle($sName, $sContent)
+	{
+		$this->aPage['css_styles'][$sName] = $sContent;
+	}
+    /**
+     * Set page keywords.
+     *
+     * @param mixed  $mixedKeywords necessary page keywords(string - single keyword, array - an array of keywords).
+     * @param string $sDevider      - string devider.
+     */
+    function addPageKeywords($mixedKeywords, $sDevider = ',')
+    {
+        if(is_string($mixedKeywords))
+            $mixedKeywords = strpos($mixedKeywords, $sDevider) !== false ? explode($sDevider, $mixedKeywords) : array($mixedKeywords);
+
+        foreach($mixedKeywords as $iKey => $sValue)
+            $mixedKeywords[$iKey] = trim($sValue);
+
+        $this->aPage['keywords'] = isset($this->aPage['keywords']) && is_array($this->aPage['keywords']) ? array_merge($this->aPage['keywords'], $mixedKeywords) : $mixedKeywords;
+    }
+    /**
+     * Set page locatoin coordinates.
+     *
+     * @param $fLat latitude
+     * @param $fLng longitude
+     */
+    function addPageMetaLocation($fLat, $fLng, $sCountryCode)
+    {
+        $this->aPage['location'] = array('lat' => $fLat, 'lng' => $fLng, 'country' => $sCountryCode);
+    }
+    /**
+     * Set page meta image.
+     *
+     * @param $sImageUrl meta image url
+     */
+    function addPageMetaImage($sImageUrl)
+    {
+        $this->aPage['image'] = $sImageUrl;
+    }
+    /**
+     * Set page rss link.
+     *
+     * @param $sTitle - rss feed title
+     * @param $sUrl - rss feed URL
+     */
+    function addPageRssLink($sTitle, $sUrl)
+    {
+        if (!isset($this->aPage['rss']))
+            $this->aPage['rss'] = array('title' => $sTitle, 'url' => $sUrl);
+        else
+            $this->aPage['rss'] = false;
+    }
+    /**
+     * Returns page meta info, like meta keyword, meta description, location, etc
+     */
+    function getMetaInfo()
+    {
+        $sRet = '';
+
+        $oPage = BxDolPage::getObjectInstanceByURI();
+        $bPage = $oPage !== false;
+
+        // general meta tags
+        if (!empty($this->aPage['keywords']) && is_array($this->aPage['keywords']))
+            $sRet .= '<meta name="keywords" content="' . bx_html_attribute(implode(',', $this->aPage['keywords'])) . '" />';
+
+        $sDescription = '';
+        if(!empty($this->aPage['description']) && is_string($this->aPage['description']))
+            $sDescription = $this->aPage['description'];
+        if(!$sDescription && $bPage)
+            $sDescription = $oPage->getMetaDescription();
+        $bDescription = !empty($sDescription);
+
+        if ($bDescription)
+            $sRet .= '<meta name="description" content="' . bx_html_attribute($sDescription) . '" />';
+
+        // location
+        if (!empty($this->aPage['location']) && isset($this->aPage['location']['lat']) && isset($this->aPage['location']['lng']) && isset($this->aPage['location']['country']))
+            $sRet .= '
+                <meta name="ICBM" content="' . $this->aPage['location']['lat'] . ';' . $this->aPage['location']['lng'] . '" />
+                <meta name="geo.position" content="' . $this->aPage['location']['lat'] . ';' . $this->aPage['location']['lng'] . '" />
+                <meta name="geo.region" content="' . bx_html_attribute($this->aPage['location']['country']) . '" />';
+
+        //set meta[image] value
+        if(empty($this->aPage['image'])) {
+            // use cover image if exists
+            if($bPage && ($aCover = $oPage->getPageCoverImage()))
+                $this->aPage['image'] = BxDolCover::getInstance($this)->getCoverImageUrl($aCover);
+
+            // use system Apple/Android icons if exists
+            if(empty($this->aPage['image'])) {
+                $sImgUrl = '';
+                $iImgSquare = 0;
+                $oImgStorage = BxDolStorage::getObjectInstance(BX_DOL_STORAGE_OBJ_IMAGES);
+                foreach(['icon_apple', 'icon_android', 'icon_android_splash'] as $sIcon) {
+                    $iIcon = (int)getParam('sys_site_' . $sIcon);
+                    if(!$iIcon)
+                        continue;
+
+                    $sUrl = $oImgStorage->getFileUrlById($iIcon);
+                    if(!$sUrl)
+                        continue;
+
+                    if(($aSize = BxDolImageResize::getImageSize($sUrl)) !== false && ($iSquare = (int)$aSize['w'] * (int)$aSize['h']) > $iImgSquare) {
+                        $sImgUrl = $sUrl;
+                        $iImgSquare = $iSquare;
+                    }
+                }
+
+                if(!empty($sImgUrl))
+                    $this->aPage['image'] = $sImgUrl;
+            }
+        }
+
+        // facebook / twitter
+        $bPageImage = !empty($this->aPage['image']);
+        $sRet .= '<meta name="twitter:card" content="' . ($bPageImage ? 'summary_large_image' : 'summary') . '" />';
+        if ($bPageImage)
+            $sRet .= '<meta property="og:image" content="' . $this->aPage['image'] . '" />';
+        $sRet .= '<meta property="og:title" content="' . (isset($this->aPage['header']) ? bx_html_attribute(strip_tags($this->aPage['header'])) : '') . '" />';
+        $sRet .= '<meta property="og:description" content="' . ($bDescription ? bx_html_attribute($sDescription) : '') . '" />';
+
+        // Smart App Banner
+        if (getParam('smart_app_banner') && false === strpos($_SERVER['HTTP_USER_AGENT'], 'UNAMobileApp')) {
+            if ($sAppIdIOS = getParam('smart_app_banner_ios_app_id'))
+                $sRet .= '<meta name="apple-itunes-app" content="app-id=' . $sAppIdIOS . '" />';
+        }
+
+        // RSS
+        $oFunctions = BxTemplFunctions::getInstance();
+        $sRet .= $oFunctions->getManifests();
+        $sRet .= $oFunctions->getMetaIcons();
+        
+        if (!empty($this->aPage['rss']) && !empty($this->aPage['rss']['url']))
+            $sRet .= '<link rel="alternate" type="application/rss+xml" title="' . bx_html_attribute($this->aPage['rss']['title'], BX_ESCAPE_STR_QUOTE) . '" href="' . $this->aPage['rss']['url'] . '" />';
+        
+        $sRet .= "<link rel=\"alternate\" type=\"application/json+oembed\" href=\"" . BX_DOL_URL_ROOT ."em.php?url=" . urlencode($_SERVER["REQUEST_URI"]) . "&format=json\" title=\"". (isset($this->aPage['header']) ? bx_html_attribute(strip_tags($this->aPage['header'])) : '') . "\" />";
+        
+        if (!empty($this->aPage['url'])){
+            $sRet .= '<link rel="canonical" href="' . bx_absolute_url(BxDolPermalinks::getInstance()->permalink($this->aPage['url'])) . '" />';
+        }
+        
+        return $sRet;
+    }
+    /**
+     * Get template, which was loaded earlier.
+     * @see method this->loadTemplates and field this->_aTemplates
+     *
+     * @param  string $sName - template name.
+     * @return string template's content.
+     */
+    public function getTemplate($sName)
+    {
+        return $this->_aTemplates[$sName];
+    }
+
+    /**
+     * Get template functions object.
+     * @see BxBaseFunctions
+     */
+    public function getTemplateFunctions()
+    {
+        return $this->_oTemplateFunctions;
+    }
+
+    /**
+     * Get image MIME type.
+     * 
+     * @param string $sExtension - image file's extension.
+     * @return string with MIME type. 
+     */
+    function getImageMimeType($sExtension)
+    {
+    	$sExtension = strtolower($sExtension);
+    
+    	$sResult = '';
+    	switch($sExtension) {
+    		case 'svg':
+    			$sResult = 'svg+xml';
+    			break;
+    
+    		default:
+    			$sResult = $sExtension;
+    	}
+    
+    	return 'data:image/' . $sResult;
+    }
+
+    /**
+     * Get icon template in dependence of a value, provided in $mixedId.
+     * 
+     * @param  mixed $mixedId numeric id from Storage, string with template's file name or string with font icon.
+     */
+	public function getIcon($mixedId, $aParams = array())
+    {
+        return $this->_getImage('icon', $mixedId, $aParams);
+    }
+
+    /**
+     * Get image template in dependence of a value, provided in $mixedId.
+     * 
+     * @param  mixed $mixedId numeric id from Storage, string with template's file name or string with font icon.
+     */
+    public function getImage($mixedId, $aParams = array())
+    {
+        return $this->_getImage('image', $mixedId, $aParams);
+    }
+
+	protected function _getImage($sType, $mixedId, $aParams = array())
+    {
+        $sUrl = "";
+        $aType2Method = array('image' => 'getImageUrl', 'icon' => 'getIconUrl');
+
+        //--- Check in System Storage.
+        if(is_numeric($mixedId) && (int)$mixedId > 0) {
+        	$sStorage = BX_DOL_STORAGE_OBJ_IMAGES;
+        	if(!empty($aParams['storage'])) {
+        		$sStorage = $aParams['storage'];
+        		unset($aParams['storage']);
+        	}
+
+            if(($sResult = BxDolStorage::getObjectInstance($sStorage)->getFileUrlById((int)$mixedId)) !== false)
+                $sUrl = $sResult;
+        }
+
+        //--- Check in template folders.
+        if($sUrl == "" && is_string($mixedId) && strpos($mixedId, '.') !== false)
+            $sUrl = $this->{$aType2Method[$sType]}($mixedId);
+
+        if($sUrl != "")
+            return $this->parseImage($sUrl, array(
+                'class' => isset($aParams['class']) && !empty($aParams['class']) ? $aParams['class'] : '',
+            	'alt' => isset($aParams['alt']) && !empty($aParams['alt']) ? $aParams['alt'] : ''
+            ));
+
+        //--- Use iconic font.
+        return $this->parseIcon($mixedId, $aParams);
+    }
+
+    /**
+     * Get full URL for the icon.
+     *
+     * @param  string $sName    icon's file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string full URL.
+     */
+    function getIconUrl($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        $sContent = "";
+        if(($sContent = $this->_getInlineData('icon', $sName, $sCheckIn)) !== false)
+            return $sContent;
+
+        return $this->_getAbsoluteLocation('url', $this->_sFolderIcons, $sName, $sCheckIn);
+    }
+    /**
+     * Get absolute Path for the icon.
+     *
+     * @param  string $sName    - icon's file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string absolute path.
+     */
+    function getIconPath($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        return $this->_getAbsoluteLocation('path', $this->_sFolderIcons, $sName, $sCheckIn);
+    }
+
+    /**
+     * Get image/icon by name automatically. Cache item description:
+     * [
+     *      'v' => value (name, url or source),
+     *      'c' => classes list divided with comma (,)
+     *      't' => parse type (ic - icon, im - image, sc - source)
+     * ]
+     * Cached images can be overwritten by listening 'system' - 'get_layout_images' alert.
+     * 
+     * @param string $sName unique name. The following format can be used: name|classes. 
+     * Where name and classes can consists of multiple parts (divided with comma (,) in HTML variant). For example, 
+     *     in PHP: $this->getImageAuto('far star|class1 class2');
+     *     in HTML: <bx_image_auto:far,star|class1,class2 />
+     * @param boolean $bWrapped wrap in HTML tag or not.
+     * @param string $sCheckIn where the content would be searched(base, template, both)
+     * @return string icon/image value (name, url or source) or final HTML code.
+     */
+    function getImageAuto($sName, $bWrapped = true, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        $sDivParts = '|';
+        $sDivItems = ',';
+
+        $sKey = md5($sName);
+
+        $sClasses = '';
+        if(strpos($sName, $sDivParts) !== false)
+            list($sName, $sClasses) = explode($sDivParts, $sName);
+
+        if(strpos($sName, $sDivItems) !== false)
+            $sName = implode(' ', explode($sDivItems, $sName));
+
+        if(!isset(self::$_aImages[$sKey])) {
+            $aResult = [
+                'v' => $sName,
+                't' => 'ic',
+                'c' => $sClasses
+            ];
+
+            $sUrl = '';
+            foreach(['Image', 'Icon'] as $sType) 
+                foreach(['svg', 'png', 'jpg', 'gif'] as $sExt)
+                    if(($sUrl = $this->{'get' . $sType . 'Url'}($sName . '.' . $sExt, $sCheckIn)) != '') {
+                        $aResult = [
+                            'v' => $sUrl,
+                            't' => 'im',
+                            'c' => $sClasses
+                        ];
+                        break 2;
+                    }
+
+            self::$_aImages[$sKey] = $aResult;
+
+            $this->saveImages();
+        }
+
+        if(!self::$_aImages[$sKey]['v'])
+            return '';
+
+        if(!$bWrapped || self::$_aImages[$sKey]['t'] == 'sc')
+            return self::$_aImages[$sKey]['v'];       
+
+        $aAttrs = [];
+        if(self::$_aImages[$sKey]['c'] != '')
+            $aAttrs['class'] = implode(' ', explode($sDivItems, self::$_aImages[$sKey]['c']));
+
+        $aType2Method = ['ic' => 'parseIcon', 'im' => 'parseImage'];
+        return $this->{$aType2Method[self::$_aImages[$sKey]['t']]}(self::$_aImages[$sKey]['v'], $aAttrs);
+    }
+
+    /**
+     * Get full URL for the image.
+     *
+     * @param  string $sName    - images's file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string full URL.
+     */
+    function getImageUrl($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        $sContent = "";
+        if(($sContent = $this->_getInlineData('image', $sName, $sCheckIn)) !== false)
+            return $sContent;
+
+        return $this->_getAbsoluteLocation('url', $this->_sFolderImages, $sName, $sCheckIn);
+    }
+    /**
+     * Get absolute Path for the image.
+     *
+     * @param  string $sName    - image's file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string absolute path.
+     */
+    function getImagePath($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        return $this->_getAbsoluteLocation('path', $this->_sFolderImages, $sName, $sCheckIn);
+    }
+    /**
+     * Get full URL of CSS file.
+     *
+     * @param  string $sName    - CSS file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string full URL.
+     */
+    function getCssUrl($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        return $this->_getAbsoluteLocation('url', $this->_sFolderCss, $sName, $sCheckIn);
+    }
+    /**
+     * Get full Path of CSS file.
+     *
+     * @param  string $sName    - CSS file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string full URL.
+     */
+    function getCssPath($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        return $this->_getAbsoluteLocation('path', $this->_sFolderCss, $sName, $sCheckIn);
+    }
+    /**
+     * Get full URL of JS file.
+     *
+     * @param  string $sName    - JS file name.
+     * @return string full URL.
+     */
+    function getJsUrl($sName)
+    {
+        return $this->_getAbsoluteLocationJs('url', $sName);
+    }
+    /**
+     * Get full Path of JS file.
+     *
+     * @param  string $sName    - JS file name.
+     * @return string full URL.
+     */
+    function getJsPath($sName)
+    {
+        return $this->_getAbsoluteLocationJs('path', $sName);
+    }
+    /**
+     * Get full URL of Template (HTML) file.
+     *
+     * @param  string $sName    - Template file name.
+     * @return string full URL.
+     */
+    function getTemplateUrl($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        return $this->_getAbsoluteLocation('url', $this->_sFolderHtml, $sName, $sCheckIn);
+    }
+    /**
+     * Get full Path of JS file.
+     *
+     * @param  string $sName    - JS file name.
+     * @return string full URL.
+     */
+    function getTemplatePath($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        return $this->_getAbsoluteLocation('path', $this->_sFolderHtml, $sName, $sCheckIn);
+    }
+
+    /**
+     * Get menu.
+     * @param $s menu object name
+     * @return html or empty string
+     */
+    function getMenu ($s)
+    {
+        $oMenu = BxDolMenu::getObjectInstance($s);
+        
+        if($s == 'sys_site_submenu'){
+            $oPage = BxDolPage::getObjectInstanceByURI();
+            if ($oPage && $oPage->getSubMenu() == 'disabled'){
+                return;
+            }
+        }
+        return $oMenu ? $oMenu->getCode () : '';
+    }
+
+    /**
+     * Check whether HTML file exists or not.
+     *
+     * @param  string $sName    - HTML file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return boolean result of operation.
+     */
+    function isHtml($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        return $this->_getAbsoluteLocation('path', $this->_sFolderHtml, $sName, $sCheckIn) != '';
+    }
+
+    /**
+     * Get content of HTML file.
+     *
+     * @param  string $sName    - HTML file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string full content of the file and false on failure.
+     */
+    function getHtml($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        $sAbsolutePath = $this->_getAbsoluteLocation('path', $this->_sFolderHtml, $sName, $sCheckIn);
+        return !empty($sAbsolutePath) ? trim(file_get_contents($sAbsolutePath)) : false;
+    }
+
+    /**
+     * Parse HTML template. Search for the template with accordance to it's file name.
+     *
+     * @see allows to use cache.
+     *
+     * @param  string $sName               - HTML file name.
+     * @param  array  $aVariables          - key/value pairs. key should be the same as template's key, but without prefix and postfix.
+     * @param  mixed  $mixedKeyWrapperHtml - key wrapper(string value if left and right parts are the same, array(left, right) otherwise).
+     * @param  string $sCheckIn            where the content would be searched(base, template, both)
+     * @return string the result of operation.
+     */
+    function parseHtmlByName($sName, $aVariables, $mixedKeyWrapperHtml = null, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->beginTemplate($sName, $sRand = time().rand());
+
+        if (($sContent = $this->getCached($sName, $aVariables, $mixedKeyWrapperHtml, $sCheckIn)) !== false) {
+            if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->endTemplate($sName, $sRand, $sContent, true);
+            return $sContent;
+        }
+
+        $sRet = '';
+        if (($sContent = $this->getHtml($sName, $sCheckIn)) !== false)
+            $sRet = $this->_parseContent($sContent, $aVariables, $mixedKeyWrapperHtml);
+
+        if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->endTemplate($sName, $sRand, $sRet, false);
+
+        return $sRet;
+    }
+    /**
+     * Parse HTML template.
+     *
+     * @see Doesn't allow to use cache.
+     *
+     * @param  string $sContent            - HTML file content.
+     * @param  array  $aVariables          - key/value pairs. key should be the same as template's key, but without prefix and postfix.
+     * @param  mixed  $mixedKeyWrapperHtml - key wrapper(string value if left and right parts are the same, array(left, right) otherwise).
+     * @return string the result of operation.
+     */
+    function parseHtmlByContent($sContent, $aVariables, $mixedKeyWrapperHtml = null)
+    {
+        if(empty($sContent))
+            return "";
+
+        return $this->_parseContent($sContent, $aVariables, $mixedKeyWrapperHtml);
+    }
+    /**
+     * Parse earlier loaded HTML template.
+     *
+     * @see Doesn't allow to use cache.
+     *
+     * @param  string $sName      - template name.
+     * @param  array  $aVariables - key/value pairs. Key should be the same as template's key, excluding prefix and postfix.
+     * @return string the result of operation.
+     * @see $this->_aTemplates
+     */
+    function parseHtmlByTemplateName($sName, $aVariables, $mixedKeyWrapperHtml = null)
+    {
+        if(!isset($this->_aTemplates[$sName]) || empty($this->_aTemplates[$sName]))
+            return "";
+
+        return $this->_parseContent($this->_aTemplates[$sName], $aVariables, $mixedKeyWrapperHtml);
+    }
+    /**
+     * Parse page HTML template. Search for the page's template with accordance to it's file name.
+     *
+     * @see allows to use cache.
+     *
+     * @param  string $sName      - HTML file name.
+     * @param  array  $aVariables - key/value pairs. key should be the same as template's key, but without prefix and postfix.
+     * @return string the result of operation.
+     */
+    function parsePageByName($sName, $aVariables)
+    {
+        if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->beginPage($sName);
+
+        $sContent = $this->parseHtmlByName($sName, $aVariables, $this->_sKeyWrapperHtml, BX_DOL_TEMPLATE_CHECK_IN_BOTH);
+        if(empty($sContent)) {
+            $aType = BxDolPageQuery::getPageType($this->getPageType());
+            if(!empty($aType) && is_array($aType))
+                $sContent = $this->parseHtmlByName($aType['template'], $aVariables, $this->_sKeyWrapperHtml, BX_DOL_TEMPLATE_CHECK_IN_BOTH);
+        }
+        if(empty($sContent))
+            $sContent = $this->parseHtmlByName('default.html', $aVariables, $this->_sKeyWrapperHtml, BX_DOL_TEMPLATE_CHECK_IN_BOTH);
+
+        //---Process injection at the very last ---//
+        $oTemplate = &$this;
+        $sContent = preg_replace_callback("'<bx_injection:([^\s]+) />'s", function($aMatches) use($oTemplate) {
+            return $oTemplate->processInjection($oTemplate->getPageNameIndex(), $aMatches[1]);
+        }, $sContent);
+
+        //--- Add CSS and JS at the very last ---//
+        if(strpos($sContent, '<bx_include_css_styles />') !== false) {
+            $aStyles = array(
+                'display' => 'none !important'
+            );
+
+            if(isLogged())
+                $this->addCssStyle('.bx-hide-when-logged-in', $aStyles);
+            else
+                $this->addCssStyle('.bx-hide-when-logged-out', $aStyles);
+
+            $sContent = str_replace('<bx_include_css_styles />', $this->includeCssStyles(), $sContent);
+        }
+
+        if(strpos($sContent , '<bx_include_css_system />') !== false) {
+            $sContent = str_replace('<bx_include_css_system />', $this->includeFiles('css', true), $sContent);
+        }
+
+        if(strpos($sContent , '<bx_include_css />') !== false) {
+            if (!empty($this->aPage['css_name']))
+                $this->addCss($this->aPage['css_name']);
+            $sContent = str_replace('<bx_include_css />', $this->includeFiles('css'), $sContent);
+        }
+        
+        if(strpos($sContent , '<bx_include_js_system />') !== false) {
+            $sContent = str_replace('<bx_include_js_system />', $this->includeFiles('js', true), $sContent);
+        }
+
+        if(strpos($sContent , '<bx_include_js />') !== false) {
+            if (!empty($this->aPage['js_name']))
+                $this->addJs($this->aPage['js_name']);
+            $sContent = str_replace('<bx_include_js />', $this->includeFiles('js') . $this->includeCssAsync(), $sContent);
+        }
+
+        if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->endPage($sContent);
+
+        return $sContent;
+    }
+    /**
+     * Parse system keys.
+     *
+     * @param  string $sKey key
+     * @return string value associated with the key.
+     */
+    function parseSystemKey($sKey, $mixedKeyWrapperHtml = null, $bProcessInjection = true)
+    {
+        $aKeyWrappers = $this->_getKeyWrappers($mixedKeyWrapperHtml);
+
+        $sRet = '';
+        switch( $sKey ) {
+            case 'page_charset':
+                $sRet = 'UTF-8';
+                break;
+            case 'page_robots':
+                if(!empty($this->aPage['robots']) && is_string($this->aPage['robots']))
+                    $sRet = '<meta name="robots" content="' . bx_html_attribute($this->aPage['robots']) . '" />';
+                break;
+            case 'meta_info':
+                $sRet = $this->getMetaInfo();
+                break;
+            case 'page_header':
+                if(isset($this->aPage['header']))
+                    $sRet = bx_process_output(strip_tags($this->aPage['header']));
+                break;
+            case 'page_header_text':
+                if(isset($this->aPage['header_text']))
+                    $sRet = bx_process_output($this->aPage['header_text']);
+                break;
+            case 'page_width':
+                if (false === strpos($this->_oTemplateConfig->aLessConfig['bx-page-width'], 'px'))
+                    $sRet = BX_DOL_PAGE_WIDTH;
+                else
+                    $sRet = $this->_oTemplateConfig->aLessConfig['bx-page-width'];
+                break;
+            case 'system_injection_head':
+                $sRet = $this->_oTemplateFunctions->getInjectionHead();
+                break;
+            case 'system_injection_header':
+                $sRet = $this->_oTemplateFunctions->getInjectionHeader();
+                break;
+            case 'system_injection_footer':
+                $sRet = $this->_oTemplateFunctions->getInjectionFooter();
+                break;
+            case 'lang':
+                $sRet = bx_lang_name();
+                break;
+            case 'lang_direction':
+                $sRet = bx_lang_direction();
+                break;
+            case 'lang_country':
+                if (!($sRet = BxDolLanguages::getInstance()->getLangCountryCode()))
+                    $sRet = bx_lang_name();
+                break;                
+            case 'main_logo':
+                $sRet = BxTemplFunctions::getInstance()->getMainLogo();
+                break;
+            case 'informer':
+                $oInformer = BxDolInformer::getInstance($this);
+                $sRet = $oInformer ? $oInformer->display() : '';
+                break;
+            case 'cover':
+            	$oCover = BxDolCover::getInstance($this);
+
+            	$bCover = $oCover->isEnabled();
+            	if($bCover && ($oPage = BxDolPage::getObjectInstanceByURI()) !== false) {
+                    $bCover = $oPage->isPageCover();
+                    if($bCover && !$oCover->isCover()) {
+                        $aCover = $oPage->getPageCoverImage();
+                        if(!empty($aCover))
+                            $oCover->setCoverImageUrl($aCover);
+                    }
+            	}
+
+                $sRet = $bCover ? $oCover->display() : $oCover->displayEmpty();
+                break;
+            case 'site_submenu_class':
+                $oMenu = BxDolMenu::getObjectInstance('sys_site_submenu');
+                if($oMenu)
+                    $sRet = $oMenu->getClass();
+                break;
+            case 'site_submenu_hidden':
+                $sClass = 'bx-menu-main-bar-hidden';
+
+                $oPage = BxDolPage::getObjectInstanceByURI();
+                if($oPage !== false && !$oPage->isVisiblePageSubmenu()) {
+                    $sRet = $sClass;
+                    break;
+                }
+
+                $oMenu = BxDolMenu::getObjectInstance('sys_site_submenu');
+                if($oMenu !== false && !$oMenu->isVisible()) {
+                    $sRet = $sClass;
+                    break;
+                }
+
+                break;
+            case 'dol_images':
+                $sRet = $this->_processJsImages();
+                break;
+            case 'dol_lang':
+                $sRet = $this->_processJsTranslations();
+                break;
+            case 'dol_options':
+                $sRet = $this->_processJsOptions();
+                break;
+            case 'copyright':
+                $sRet = _t( '_copyright', date('Y') ) . getVersionComment();
+                break;
+            case 'copyright_attr':
+                $sRet = bx_html_attribute(_t('_copyright', date('Y')));
+                break;
+            case 'extra_js':
+                $sRet = empty($this->aPage['extra_js']) ? '' : $this->aPage['extra_js'];
+                break;
+            case 'is_profile_page':
+                $sRet = (defined('BX_PROFILE_PAGE')) ? 'true' : 'false';
+                break;
+			case 'system_js_requred':
+                $sRet = _t('_sys_javascript_requred');
+                break;
+            case 'included_css':
+                $sRet = json_encode($this->getIncludedUrls('css_compiled'));
+                break;
+            case 'included_js':
+                $sRet = json_encode($this->getIncludedUrls('js_compiled'));
+                break;
+            case 'base':
+                $sRet = bx_convert_array2attrs($this->aPage['base']);
+                break;
+            case 'class_name':
+                $sRet = $this->getCssClassName();
+    
+                if (preg_match('/^[A-Za-z0-9_\-]+$/', bx_get('i')))
+                    $sRet .= ' bx-page-' . bx_get('i');
+
+                if(!empty($this->_iMix)) {
+                    $aMix = BxDolDb::getInstance()->getParamsMix($this->_iMix);
+                    if(isset($aMix['dark']) && (int)$aMix['dark'] == 1)
+                        $sRet .= ' dark';
+                }
+                break;
+			case 'css_media_phone':
+			case 'css_media_phone2':
+			case 'css_media_tablet':
+			case 'css_media_tablet2':
+			case 'css_media_desktop':
+                $aData = json_decode(getParam('sys_css_media_classes'), true);
+				$sKey = str_replace('css_media_', '', $sKey);
+                $sRet = $aData[$sKey];
+                break;
+            default:
+                $sRet = ($sTemplAdd = BxTemplFunctions::getInstance()->TemplPageAddComponent($sKey)) !== false ? $sTemplAdd : $aKeyWrappers['left'] . $sKey . $aKeyWrappers['right'];
+        }
+
+        if($bProcessInjection)
+            $sRet = $this->processInjection($this->getPageNameIndex(), $sKey, $sRet);
+
+        return $sRet;
+    }
+
+    /**
+     * Parse tag <A>
+     * 
+     * @param string $sLink link URL
+     * @param string $sContent link content
+     * @param array $aAttrs an array of key => value pairs
+     */
+    function parseLink($sLink, $sContent, $aAttrs = array())
+    {
+        $sAttrs = '';
+        foreach($aAttrs as $sKey => $sValue)
+            $sAttrs .= ' ' . $sKey . '="' . bx_html_attribute($sValue) . '"';
+
+        return '<a href="' . $sLink . '"' . $sAttrs . '>' . $sContent . '</a>';
+    }
+
+    /**
+     * Parse tag <A> using provided HTML template
+     * 
+     * @param string $sName template's file name
+     * @param string $sLink link URL
+     * @param string $sContent link content
+     * @param array $aAttrs an array of key => value pairs
+     */
+    function parseLinkByName($sName, $sLink, $sContent, $aAttrs = array())
+    {
+        $sAttrs = '';
+        foreach($aAttrs as $sKey => $sValue)
+            $sAttrs .= ' ' . $sKey . '="' . bx_html_attribute($sValue) . '"';
+
+        return $this->parseHtmlByName($sName, array(
+            'href' => $sLink,
+            'attrs' => $sAttrs,
+            'content' => $sContent
+        ));
+    }
+
+    /**
+     * Parse tag <BUTTON>
+     * 
+     * @param string $sContent link content
+     * @param array $aAttrs an array of key => value pairs
+     */
+    function parseButton($sContent, $aAttrs = array())
+    {
+        $sAttrs = '';
+        foreach($aAttrs as $sKey => $sValue)
+            $sAttrs .= ' ' . $sKey . '="' . bx_html_attribute($sValue) . '"';
+
+        return '<button' . $sAttrs . '>' . $sContent . '</button>';
+    }
+
+    /**
+     * Parse tag <IMG>
+     * 
+     * @param string $sLink URL to image source 
+     * @param array $aAttrs an array of key => value pairs
+     */
+    function parseImage($sLink, $aAttrs = array())
+    {
+        $sAttrs = '';
+        foreach($aAttrs as $sKey => $sValue)
+            $sAttrs .= ' ' . $sKey . '="' . bx_html_attribute($sValue) . '"';
+
+        return '<img src="' . $sLink . '"' . $sAttrs . ' />';
+    }
+
+    /**
+     * Parse font based icon in <I> tag
+     * 
+     * @param string $sName font icon name
+     * @param array $aAttrs an array of key => value pairs
+     */
+    function parseIcon($sName, $aAttrs = array())
+    {
+		$aIcons = BxTemplFunctions::getInstance()->getIcon($sName, $aAttrs);
+        if ($aIcons[0] != '')
+            $aIcons[0] = '';
+		return implode($aIcons);
+    }
+
+    function getCacheFilePrefix($sType)
+    {
+    	$sResult = '';
+    	switch($sType) {
+            case 'template':
+                $sResult = $this->_sCacheFilePrefix;
+                break;
+
+            case 'less':
+                $sResult = $this->_sCssLessPrefix;
+                break;
+
+            case 'css':
+                $sResult = $this->_sCssCachePrefix;
+                break;
+
+            case 'js':
+                $sResult = $this->_sJsCachePrefix;
+                break;
+    	}
+
+    	return $sResult;
+    }
+    /**
+     * Get cache object for templates
+     * @return cache class instance
+     */
+    function getTemplatesCacheObject ()
+    {
+        $sCacheEngine = getParam('sys_template_cache_engine');
+        $oCacheEngine = bx_instance('BxDolCache' . $sCacheEngine);
+        if(!$oCacheEngine->isAvailable())
+            $oCacheEngine = bx_instance('BxDolCacheFileHtml');
+        return $oCacheEngine;
+    }
+    /**
+     * Get template from cache if it's enabled.
+     *
+     * @param  string  $sName               template name
+     * @param  string  $aVariables          key/value pairs. key should be the same as template's key, but without prefix and postfix.
+     * @param  mixed   $mixedKeyWrapperHtml - key wrapper(string value if left and right parts are the same, array(0 => left, 1 => right) otherwise).
+     * @param  string  $sCheckIn            where the content would be searched(base, template, both)
+     * @param  boolean $bEvaluate           need to evaluate the template or not.
+     * @return string  result of operation or false on failure.
+     */
+    function getCached($sName, &$aVariables, $mixedKeyWrapperHtml = null, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH, $bEvaluate = true)
+    {
+        // initialization
+
+        if(!$this->_bCacheEnable)
+            return false;
+
+        if (in_array($sName, $this->_aCacheExceptions))
+           return false;
+        
+        $sAbsolutePath = $this->_getAbsoluteLocation('path', $this->_sFolderHtml, $sName, $sCheckIn);
+        if(empty($sAbsolutePath))
+            return false;
+
+        $oCacheEngine = $this->getTemplatesCacheObject ();
+        $isFileBasedEngine = $bEvaluate && method_exists($oCacheEngine, 'getDataFilePath');
+
+        // try to get cached content
+
+        $sCacheVariableName = "a";
+        $sCacheKey = $this->_getCacheFileName('html', $sAbsolutePath) . '.php';
+        if ($isFileBasedEngine)
+            $sCacheContent = $oCacheEngine->getDataFilePath($sCacheKey);
+        else
+            $sCacheContent = $oCacheEngine->getData($sCacheKey);
+
+
+        // recreate cache if it is empty
+
+        if ($sCacheContent === null && ($sContent = file_get_contents($sAbsolutePath)) !== false && ($sContent = $this->_compileContent($sContent, "\$" . $sCacheVariableName, 1, $aVariables, $mixedKeyWrapperHtml)) !== false) {
+            if (false === $oCacheEngine->setData($sCacheKey, trim($sContent)))
+                return false;
+
+            if ($isFileBasedEngine)
+                $sCacheContent = $oCacheEngine->getDataFilePath($sCacheKey);
+            else
+                $sCacheContent = $sContent;
+        }
+
+        if ($sCacheContent === null)
+            return false;
+
+        // return simple cache content
+
+        if(!$bEvaluate)
+            return $sCacheContent;
+
+        // return evaluated cache content
+
+        ob_start();
+
+        $$sCacheVariableName = &$aVariables;
+
+        if ($isFileBasedEngine)
+            include($sCacheContent);
+        else
+            eval('?'.'>' . $sCacheContent);
+
+        $sContent = ob_get_clean();
+
+        return $sContent;
+    }
+
+    /**
+     * Add JS file(s) to global output.
+     *
+     * @param  mixed          $mixedFiles string value represents a single JS file name. An array - array of JS file names.
+     * @param  boolean        $bDynamic   in the dynamic mode JS file(s) are not included to global output, but are returned from the function directly.
+     * @return boolean/string result of operation.
+     */
+    function addJs($mixedFiles, $bDynamic = false)
+    {
+        return $this->_processFiles('js', 'add', $mixedFiles, $bDynamic);
+    }
+
+    function addJsPreloaded($aFiles, $sCallback = false, $sCondition = false, $sConditionElseCallback = false)
+    {
+        if(!$aFiles)
+            return '';
+
+        if(!is_array($aFiles))
+            $aFiles = [$aFiles];
+
+        $sMaskLoad = "bx_get_scripts(%s);";
+        $sMaskLoadWithCallback = "bx_get_scripts(%s, function() {%s});";
+
+        $sMaskCondition = "if(%s) {%s}";
+        $sMaskConditionWithElse = "if(%s) {%s} else {setTimeout(function() {%s}, 10);}";
+
+        $iRev = $this->_getRevision();
+
+        $aFilesLocated = [];
+        foreach($aFiles as $sFile) {
+            $mixedFile = $this->_locateFile('js', $sFile);
+            if($mixedFile === false)
+                continue;
+
+            list($sUrl) = $mixedFile;
+
+            $aFilesLocated[] = bx_append_url_params($sUrl, ['rev' => $iRev]);
+        }
+        $sFilesLocated = json_encode($aFilesLocated);
+
+        if($sCallback !== false)
+            $sResult = sprintf($sMaskLoadWithCallback, $sFilesLocated, $sCallback);
+        else
+            $sResult = sprintf($sMaskLoad, $sFilesLocated);
+
+        if($sCondition === false)
+            return $sResult;
+
+        if($sConditionElseCallback !== false)
+            $sResult = sprintf($sMaskConditionWithElse, $sCondition, $sResult, $sConditionElseCallback);
+        else
+            $sResult = sprintf($sMaskCondition, $sCondition, $sResult);
+
+        return $sResult;
+    }
+
+    function addJsPreloadedWrapped($aFiles, $sCallback = false, $sCondition = false, $sConditionElseCallback = false)
+    {
+        $sCode = $this->addJsPreloaded($aFiles, $sCallback, $sCondition, $sConditionElseCallback);
+        if(!$sCode)
+            return '';
+
+        return $this->_wrapInTagJsCode($sCode);
+    }
+
+    function addJsCodeOnLoad($sCallback)
+    {
+        $sMaskLoad = "$(document).ready(function() {%s});";
+
+        return sprintf($sMaskLoad, $sCallback);
+    }
+    
+    function addJsCodeOnLoadWrapped($sCallback)
+    {
+        $sCode = $this->addJsCodeOnLoad($sCallback);
+
+        return $this->_wrapInTagJsCode($sCode); 
+    }
+    
+    /**
+     * get added js files
+     */ 
+    function getJs()
+    {
+        return $this->aPage['js_compiled'];
+    }
+
+    /**
+     * Add System JS file(s) to global output.
+     * System JS files are the files which are attached to all pages. They will be cached separately from the others.
+     *
+     * @param  mixed          $mixedFiles string value represents a single JS file name. An array - array of JS file names.
+     * @param  boolean        $bDynamic   in the dynamic mode JS file(s) are not included to global output, but are returned from the function directly.
+     * @return boolean/string result of operation.
+     */
+    function addJsSystem($mixedFiles)
+    {
+        return $this->_processFiles('js', 'add', $mixedFiles, false, true);
+    }
+
+    /**
+     * Delete JS file(s) from global output.
+     *
+     * @param  mixed   $mixedFiles string value represents a single JS file name. An array - array of JS file names.
+     * @return boolean result of operation.
+     */
+    function deleteJs($mixedFiles)
+    {
+        return $this->_processFiles('js', 'delete', $mixedFiles);
+    }
+
+    /**
+     * Delete System JS file(s) from global output.
+     *
+     * @param  mixed   $mixedFiles string value represents a single JS file name. An array - array of JS file names.
+     * @return boolean result of operation.
+     */
+    function deleteJsSystem($mixedFiles)
+    {
+        return $this->_processFiles('js', 'delete', $mixedFiles, false, true);
+    }
+
+    /**
+     * Compile JS files in one file.
+     *
+     * @param  string $sAbsolutePath CSS file absolute path(full URL for external CSS/JS files).
+     * @param  array  $aIncluded     an array of already included JS files.
+     * @return string result of operation.
+     */
+    function _compileJs($sAbsolutePath, &$aIncluded)
+    {
+        if(isset($aIncluded[$sAbsolutePath]))
+           return '';
+
+        $bExternal = strpos($sAbsolutePath, "http://") !== false || strpos($sAbsolutePath, "https://") !== false;
+        if($bExternal) {
+            $sPath = $sAbsolutePath;
+            $sName = '';
+
+            $sContent = bx_file_get_contents($sAbsolutePath);
+        } else {
+            $aFileInfo = pathinfo($sAbsolutePath);
+            $sPath = $aFileInfo['dirname'] . DIRECTORY_SEPARATOR;
+            $sName = $aFileInfo['basename'];
+
+            $sContent = file_get_contents($sPath . $sName);
+        }
+
+        if(empty($sContent))
+            return '';
+
+        $sUrl = bx_ltrim_str($sPath, realpath(BX_DIRECTORY_PATH_ROOT), BX_DOL_URL_ROOT);
+        $sUrl = str_replace(DIRECTORY_SEPARATOR, '/', $sUrl);
+
+        $sContent = "\r\n/*--- BEGIN: " . $sUrl . $sName . "---*/\r\n" . $sContent . ";\r\n/*--- END: " . $sUrl . $sName . "---*/\r\n";
+        $sContent = preg_replace("/\/\/# sourceMappingURL\s*=.*/si", "", $sContent);
+        $sContent = str_replace(["\n\r", "\r\n", "\r"], "\n", $sContent);
+
+        $aIncluded[$sAbsolutePath] = 1;
+
+        return preg_replace(
+            array(
+                "'<bx_url_root />'",
+                "'\r\n'"
+            ),
+            array(
+                BX_DOL_URL_ROOT,
+                "\n"
+            ),
+            $sContent
+        );
+    }
+
+	/**
+     * Minify JS
+     *
+     * @param  string $s JS string to minify
+     * @return string minified JS string.
+     */
+	function _minifyJs($s)
+    {
+        // since each JS file is minified separately, it has to be in own scope
+    	return "\n {\n" . BxDolMinify::getInstance()->minifyJs($s) . "\n }\n";
+    }
+
+    /**
+     * Wrap an URL to JS file into JS tag.
+     *
+     * @param  string $sFile - URL to JS file.
+     * @return string the result of operation.
+     */
+    function _wrapInTagJs($sFile)
+    {
+        return "<script language=\"javascript\" src=\"" . $sFile . "\"></script>";
+    }
+
+    /**
+     * Wrap JS code into JS tag.
+     *
+     * @param  string $sCode - JS code.
+     * @return string the result of operation.
+     */
+    function _wrapInTagJsCode($sCode)
+    {
+        return "<script language=\"javascript\">\n" . $sCode . "\n</script>";
+    }
+
+    /**
+     * Add CSS file(s) to global output.
+     *
+     * @param  mixed          $mixedFiles string value represents a single CSS file name. An array - array of CSS file names.
+     * @param  boolean        $bDynamic   in the dynamic mode CSS file(s) are not included to global output, but are returned from the function directly.
+     * @return boolean/string result of operation
+     */
+    function addCss($mixedFiles, $bDynamic = false)
+    {
+        if($bDynamic)
+            return $this->addCssPreloadedWrapped($mixedFiles);
+        else
+            return $this->_processFiles('css', 'add', $mixedFiles, $bDynamic);
+    }
+
+    function addCssPreloaded($aFiles)
+    {
+        if(!$aFiles)
+            return '';
+
+        if(!is_array($aFiles))
+            $aFiles = [$aFiles];
+
+        $sMaskLoad = "bx_get_style(%s);";
+
+        $iRev = $this->_getRevision();
+
+        $aFilesLocated = [];
+        foreach($aFiles as $sFile) {
+            $mixedFile = $this->_locateFile('css', $sFile);
+            if($mixedFile === false)
+                continue;
+
+            list($sUrl) = $mixedFile;
+
+            $aFilesLocated[] = bx_append_url_params($sUrl, ['rev' => $iRev]);
+        }
+        $sFilesLocated = json_encode($aFilesLocated);
+
+        return sprintf($sMaskLoad, $sFilesLocated);
+    }
+
+    function addCssPreloadedWrapped($aFiles)
+    {
+        $sCode = $this->addCssPreloaded($aFiles);
+        if(!$sCode)
+            return '';
+
+        return $this->_wrapInTagJsCode($sCode);
+    }
+
+    /**
+     * get added css files
+     */ 
+    function getCss()
+    {
+        return $this->aPage['css_compiled'];
+    }
+
+    /**
+     * Add additional heavy css file (not very necessary) to load asynchronously for desktop browsers only
+     * @param  mixed          $mixedFiles string value represents a single CSS file name. An array - array of CSS file names.
+     */
+    function addCssAsync($mixedFiles)
+    {
+        if (!is_array($mixedFiles))
+            $mixedFiles = array($mixedFiles);
+
+        foreach ($mixedFiles as $sFile)
+            $this->aPage['css_async'][] = $this->_getAbsoluteLocationCss('url', $sFile);
+
+        $this->addJs('loadCSS.js');
+    }
+
+    /**
+     * Return script tag with special code to load async css.
+     * This tag is added after js files list
+     */
+    function includeCssAsync ()
+    {
+        if (empty($this->aPage['css_async']))
+            return '';
+
+        $this->aPage['css_async'] = array_unique($this->aPage['css_async']);
+
+        $sList = '';
+        foreach ($this->aPage['css_async'] as $sUrl)
+            $sList .= 'loadCSS("' . $sUrl . '", document.getElementById("bx_css_async"));';
+
+        // don't load css for mobile devices
+        return '
+            <script id="bx_css_async">
+                if(!/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) {
+                    ' . $sList . '
+                }
+            </script>
+        ';
+    }
+
+    /**
+     * Add System CSS file(s) to global output.
+     * System CSS files are the files which are attached to all pages. They will be cached separately from the others.
+     *
+     * @param  mixed          $mixedFiles string value represents a single CSS file name. An array - array of CSS file names.
+     * @return boolean/string result of operation
+     */
+    function addCssSystem($mixedFiles)
+    {
+        return $this->_processFiles('css', 'add', $mixedFiles, false, true);
+    }
+
+    /**
+     * Delete CSS file(s) from global output.
+     *
+     * @param  mixed   $mixedFiles string value represents a single CSS file name. An array - array of CSS file names.
+     * @return boolean result of operation.
+     */
+    function deleteCss($mixedFiles)
+    {
+        return $this->_processFiles('css', 'delete', $mixedFiles);
+    }
+    /**
+     * Delete System CSS file(s) from global output.
+     *
+     * @param  mixed   $mixedFiles string value represents a single CSS file name. An array - array of CSS file names.
+     * @return boolean result of operation.
+     */
+    function deleteCssSystem($mixedFiles)
+    {
+        return $this->_processFiles('css', 'delete', $mixedFiles, false, true);
+    }
+    /**
+     * Compile CSS files' structure(@see \@import css_file_path) in one file.
+     *
+     * @param  string $sAbsolutePath CSS file absolute path(full URL for external CSS/JS files).
+     * @param  array  $aIncluded     an array of already included CSS files.
+     * @return string result of operation.
+     */
+    function _compileCss($sAbsolutePath, &$aIncluded)
+    {
+        if(isset($aIncluded[$sAbsolutePath]))
+           return '';
+
+        $bExternal = strpos($sAbsolutePath, "http://") !== false || strpos($sAbsolutePath, "https://") !== false;
+        if($bExternal) {
+        	$sPath = $sAbsolutePath;
+            $sName = '';
+
+        	$aAPUrl = parse_url($sAbsolutePath);
+        	if(!empty($aAPUrl['path'])) {
+        		$aAPPath = pathinfo($aAPUrl['path']);
+        		if(!empty($aAPPath['basename'])) {
+        			$sPath = bx_rtrim_str($sAbsolutePath, $aAPPath['basename']);
+        			$sName = $aAPPath['basename'];
+        		}
+        	}
+
+            $sContent = bx_file_get_contents($sAbsolutePath);
+        } else {
+            $aFileInfo = pathinfo($sAbsolutePath);
+            $sPath = $aFileInfo['dirname'] . DIRECTORY_SEPARATOR;
+            $sName = $aFileInfo['basename'];
+
+            $sContent = file_get_contents($sPath . $sName);
+        }
+
+        if(empty($sContent))
+            return '';
+
+        $sUrl = bx_ltrim_str($sPath, realpath(BX_DIRECTORY_PATH_ROOT), BX_DOL_URL_ROOT);
+        $sUrl = str_replace(DIRECTORY_SEPARATOR, '/', $sUrl);
+
+        $sContent = "\r\n/*--- BEGIN: " . $sUrl . $sName . "---*/\r\n" . $sContent . "\r\n/*--- END: " . $sUrl . $sName . "---*/\r\n";
+        $aIncluded[$sAbsolutePath] = 1;
+
+        $sContent = str_replace(array("\n\r", "\r\n", "\r"), "\n", $sContent);
+        if($bExternal) {
+            $sContent = preg_replace(
+                array(
+                    "'@import\s+url\s*\(\s*[\'|\"]*\s*([a-zA-Z0-9\.\/_-]+)\s*[\'|\"]*\s*\)\s*;'",
+                    "'url\s*\(\s*[\'|\"]*\s*([a-zA-Z0-9\.\/\?\#_=-]+)\s*[\'|\"]*\s*\)'"
+                ),
+                array(
+                    "",
+                    "url(" . $sPath . "\\1)"
+                ),
+                $sContent
+            );
+        }
+        else {
+        	try {
+        		$oTemplate = &$this;
+
+        		/* Match URL based imports like the following:
+        		 * @import 'http://[domain]/modules/[vendor]/[module]/template/css/view.css';
+        		 * Is mainly needed for CSS files which are gotten from LESS compiler.
+        		 */
+        		$sContent = preg_replace_callback(
+	                "'@import\s+[\'|\"]*\s*" . str_replace("/", "\/", BX_DOL_URL_ROOT) . "([a-zA-Z0-9\.\/_-]+)\s*[\'|\"]*\s*;'", function ($aMatches)  use($oTemplate, $sPath, &$aIncluded) {
+	                	return $oTemplate->_compileCss(realpath(BX_DIRECTORY_PATH_ROOT . $aMatches[1]), $aIncluded);
+	                }, $sContent);
+
+				/* Match relative path based imports like the following:
+				 * @import url(../../../../../../base/profile/template/css/main.css);
+				 * Is mainly needed for default CSS files.
+				 */
+	            $sContent = preg_replace_callback(
+	                "'@import\s+url\s*\(\s*[\'|\"]*\s*([a-zA-Z0-9\.\/_-]+)\s*[\'|\"]*\s*\)\s*;'", function ($aMatches)  use($oTemplate, $sPath, &$aIncluded) {
+	                	return $oTemplate->_compileCss(realpath($sPath . dirname($aMatches[1])) . DIRECTORY_SEPARATOR . basename($aMatches[1]), $aIncluded);
+	                }, $sContent);
+
+	            $sContent = preg_replace_callback(
+	                "'url\s*\(\s*[\'|\"]*\s*([a-zA-Z0-9\.\/\?\#_=-]+)\s*[\'|\"]*\s*\)'", function ($aMatches)  use($oTemplate, $sPath) {
+						$sFile = basename($aMatches[1]);
+				        $sDirectory = dirname($aMatches[1]);
+
+				        $sRootPath = realpath(BX_DIRECTORY_PATH_ROOT) . '/';
+				        $sAbsolutePath = realpath(addslashes($sPath) . $sDirectory) . DIRECTORY_SEPARATOR . $sFile;
+
+				        $sRootPath = str_replace(DIRECTORY_SEPARATOR, '/', $sRootPath);
+				        $sAbsolutePath = str_replace(DIRECTORY_SEPARATOR, '/', $sAbsolutePath);
+
+				        return 'url(' . bx_ltrim_str($sAbsolutePath, $sRootPath, BX_DOL_URL_ROOT) . ')';
+	                }, $sContent);
+			}
+	    	catch(Exception $oException) {
+	        	return '';
+	        }
+        }
+
+        return $sContent;
+    }
+
+    /**
+     * Less CSS
+     *
+     * @param  mixed $mixed CSS string to process with Less compiler or an array with CSS file's Path and URL.
+     * @return mixed string or an array with CSS file's Path and URL.
+     */
+    function _lessCss($mixed)
+    {
+        if(is_array($mixed) && isset($mixed['url']) && isset($mixed['path'])) {
+            $sPathFile = realpath($mixed['path']);
+            $aInfoFile = pathinfo($sPathFile);
+            if (!isset($aInfoFile['extension']) || $aInfoFile['extension'] != 'less')
+                return $mixed;
+
+        	$aFiles = array($mixed['path'] => $mixed['url']);
+        	$aOptions = array('cache_dir' => $this->_sCachePublicFolderPath, 'prefix' => $this->_sCssLessPrefix);
+        	$sFile = Less_Cache::Get($aFiles, $aOptions, $this->_oTemplateConfig->aLessConfig);
+
+            return array('url' => $this->_sCachePublicFolderUrl . $sFile, 'path' => $this->_sCachePublicFolderPath . $sFile);
+        }
+
+        $oLess = new Less_Parser();
+        $oLess->ModifyVars($this->_oTemplateConfig->aLessConfig);
+        $oLess->parse($mixed);
+        return $oLess->getCss();
+    }
+
+    /**
+     * Minify CSS
+     *
+     * @param  string $s CSS string to minify
+     * @return string minified CSS string.
+     */
+    function _minifyCss($s)
+    {
+    	return BxDolMinify::getInstance()->minifyCss($s);
+    }
+
+    /**
+     * Wrap an URL to CSS file into CSS tag.
+     *
+     * @param  string $sFile - URL to CSS file.
+     * @return string the result of operation.
+     */
+    function _wrapInTagCss($sFile)
+    {
+        if (!$sFile)
+            return '';
+        return "<link href=\"" . $sFile . "\" rel=\"stylesheet\" type=\"text/css\" />";
+    }
+    /**
+     * Wrap CSS code into CSS tag.
+     *
+     * @param  string $sCode - CSS code.
+     * @return string the result of operation.
+     */
+    function _wrapInTagCssCode($sCode)
+    {
+        return "<style>" . $sCode . "</style>";
+    }
+	/**
+     *  Include CSS style(s) in the page's head section.
+     */
+	function includeCssStyles()
+	{
+		$sResult = "";
+		if(empty($this->aPage['css_styles']) || !is_array($this->aPage['css_styles']))
+			return $sResult;
+
+		foreach($this->aPage['css_styles'] as $sName => $aContent) {
+			$sContent = "";
+			if(!empty($aContent) && is_array($aContent))
+				foreach($aContent as $sStyleName => $sStyleValue)
+					$sContent .= "\t" . $sStyleName . ": " . $sStyleValue . ";\r\n";
+
+			$sResult .= $sName . " {\r\n" . $sContent . "}\r\n";
+		}
+
+		return !empty($sResult) ? $this->_wrapInTagCssCode($sResult) : '';
+	}
+    /**
+     * Include CSS/JS file(s) attached to the page in its head section.
+     * @see the method is system and would be called automatically.
+     *
+     * @param  string $sType the type of file('js' or 'css')
+     * @return string the result CSS code.
+     */
+    function includeFiles($sType, $bSystem = false)
+    {
+        $sUpcaseType = ucfirst($sType);
+
+        $sArrayKey = $sType . ($bSystem ? '_system' : '_compiled');
+        $aFiles = isset($this->aPage[$sArrayKey]) ? $this->aPage[$sArrayKey] : array();
+        if(empty($aFiles) || !is_array($aFiles))
+            return "";
+
+        if(!$this->{'_b' . $sUpcaseType . 'Cache'})
+            return $this->_includeFiles($sType, $aFiles);
+
+        //--- If cache already exists, return it ---//
+        $sMethodWrap = '_wrapInTag' . $sUpcaseType;
+        $sMethodCompile = '_compile' . $sUpcaseType;
+        $sMethodLess = '_less' . $sUpcaseType;
+        $sMethodMinify = '_minify' . $sUpcaseType;
+
+        ksort($aFiles);
+
+        $sName = "";
+        foreach($aFiles as $aFile)
+            $sName .= $aFile['url'];
+        $sName = $this->_getCacheFileName($sType, $sName);
+
+        $sCacheAbsoluteUrl = $this->_sCachePublicFolderUrl . $sName . '.' . $sType;
+        $sCacheAbsolutePath = $this->_sCachePublicFolderPath . $sName . '.' . $sType;
+        if(file_exists($sCacheAbsolutePath)) {
+            if($this->{'_b' . $sUpcaseType . 'Archive'})
+                $sCacheAbsoluteUrl = $this->_getLoaderUrl($sType, $sName);
+
+           return $this->$sMethodWrap($sCacheAbsoluteUrl);
+        }
+
+        //--- Collect all attached CSS/JS in one file ---//
+
+        $sResult = "";
+        $aIncluded = array();
+        foreach($aFiles as $aFile) {
+            if($this->{'_b' . $sUpcaseType . 'Less'})
+                $aFile = $this->$sMethodLess($aFile);
+
+            if(($sContent = $this->$sMethodCompile($aFile['path'], $aIncluded)) === false)
+                continue;                
+
+            if(!preg_match('/[\.-]min.(js|css)$/i', $aFile['path']) && $this->{'_b' . $sUpcaseType . 'Minify'}) // don't minify minified files
+                $sContent = $this->$sMethodMinify($sContent);
+            
+            $sResult .= $sContent;
+        }
+
+        $mixedWriteResult = false;
+        if(!empty($sResult) && ($rHandler = fopen($sCacheAbsolutePath, 'w')) !== false) {
+            $mixedWriteResult = fwrite($rHandler, $sResult);
+            fclose($rHandler);
+            @chmod ($sCacheAbsolutePath, BX_DOL_FILE_RIGHTS);
+        }
+
+        if($mixedWriteResult === false)
+            return $this->_includeFiles($sType, $aFiles);
+
+        if($this->{'_b' . $sUpcaseType . 'Archive'})
+            $sCacheAbsoluteUrl = $this->_getLoaderUrl($sType, $sName);
+
+        return $this->$sMethodWrap($sCacheAbsoluteUrl);
+    }
+    /**
+     * Include CSS/JS files without caching.
+     *
+     * @param  string $sType  the file type (css or js)
+     * @param  array  $aFiles CSS/JS files to be added to the page.
+     * @return string result of operation.
+     */
+    function _includeFiles($sType, &$aFiles)
+    {
+        $iRev = $this->_getRevision();
+        $sUpcaseType = ucfirst($sType);
+
+        $sMethodWrap = '_wrapInTag' . $sUpcaseType;
+        $sMethodLess = '_less' . $sUpcaseType;
+
+        $sResult = "";
+        foreach($aFiles as $aFile) {
+            if($this->{'_b' . $sUpcaseType . 'Less'})
+                $aFile = $this->$sMethodLess($aFile);
+
+            $sFileUrl = $aFile['url'];
+            if(!$this->{'_b' . $sUpcaseType . 'Cache'})
+                $sFileUrl = bx_append_url_params($sFileUrl, array(
+                    'rev' => $iRev
+                ));
+
+            $sResult .= $this->$sMethodWrap($sFileUrl);
+        }
+
+        return $sResult;
+    }
+    /**
+     * Insert/Delete CSS file from output stack.
+     *
+     * @param  string  $sType      the file type (css or js)
+     * @param  string  $sAction    add/delete
+     * @param  mixed   $mixedFiles string value represents a single CSS file name. An array - array of CSS file names.
+     * @return boolean result of operation.
+     */
+    function _processFiles($sType, $sAction, $mixedFiles, $bDynamic = false, $bSystem = false)
+    {
+        if(empty($mixedFiles))
+            return $bDynamic ? "" : false;
+
+        if(is_string($mixedFiles))
+            $mixedFiles = array($mixedFiles);
+
+        $iRev = $this->_getRevision();
+
+        $sUpcaseType = ucfirst($sType);
+        $sMethodLocate = '_getAbsoluteLocation' . $sUpcaseType;
+        $sMethodWrap = '_wrapInTag' . $sUpcaseType;
+        $sResult = '';
+        foreach($mixedFiles as $sFile) {
+            $mixedFile = $this->_locateFile($sType, $sFile);
+            if($mixedFile === false)
+                continue;
+
+            list($sUrl, $sPath) = $mixedFile;
+
+            $sArrayKey = $sType . ($bSystem ? '_system' : '_compiled');
+            switch($sAction) {
+                case 'add':
+                    bx_alert('system', 'add_files', 0, 0, [
+                        'file' => $sFile,
+                        'type' => $sType,
+                        'dynamic' => $bDynamic,
+                        'system' => $bSystem,
+                        'url' => &$sUrl,
+                        'path' => &$sPath,
+                    ]);
+
+                    if($bDynamic)
+                        $sResult .= $this->$sMethodWrap(bx_append_url_params($sUrl, array(
+                            'rev' => $iRev
+                        )));
+                    else {
+                        $bFound = false;
+                        $aSearchIn = $bSystem ? $this->aPage[$sArrayKey] : array_merge($this->aPage[$sType . '_system'], $this->aPage[$sArrayKey]);
+                        foreach($aSearchIn as $iKey => $aValue)
+                            if($aValue['url'] == $sUrl && $aValue['path'] == $sPath) {
+                                $bFound = true;
+                                break;
+                            }
+
+                        if(!$bFound)
+                            $this->aPage[$sArrayKey][] = array('url' => $sUrl, 'path' => $sPath);
+                    }
+                    break;
+                case 'delete':
+                    if(!$bDynamic)
+                        foreach($this->aPage[$sArrayKey]  as $iKey => $aValue)
+                            if($aValue['url'] == $sUrl) {
+                                unset($this->aPage[$sArrayKey][$iKey]);
+                                break;
+                            }
+                    break;
+            }
+        }
+
+        return $bDynamic ? $sResult : true;
+    }
+
+    function _locateFile($sType, $sFile)
+    {
+        //--- Process 3d Party CSS/JS file ---//
+        if(strpos($sFile, "http://") !== false || strpos($sFile, "https://") !== false) {
+            $sUrl = $sFile;
+            $sPath = $sFile;
+        }
+        //--- Process Custom CSS/JS file ---//
+        else if(strpos($sFile, "|") !== false) {
+            $sFile = implode('', explode("|", $sFile));
+            $sFile = bx_ltrim_str($sFile, BX_DIRECTORY_PATH_ROOT);
+
+            $sUrl = BX_DOL_URL_ROOT . $sFile;
+            $sPath = realpath(BX_DIRECTORY_PATH_ROOT . $sFile);
+        }
+        //--- Process Common CSS/JS file(check in default locations) ---//
+        else {
+            $sMethodLocate = '_getAbsoluteLocation' . ucfirst($sType);
+
+            $sUrl = $this->$sMethodLocate('url', $sFile);
+            $sPath = $this->$sMethodLocate('path', $sFile);
+        }
+
+        return !empty($sUrl) && !empty($sPath) ? [$sUrl, $sPath] : false;
+    }
+
+    /**
+     * Parse content.
+     *
+     * @param  string $sContent            - HTML file's content.
+     * @param  array  $aVariables          - key/value pairs. key should be the same as template's key, but without prefix and postfix.
+     * @param  mixed  $mixedKeyWrapperHtml - key wrapper(string value if left and right parts are the same, array(0 => left, 1 => right) otherwise).
+     * @return string the result of operation.
+     */
+    function _parseContent($sContent, $aVariables, $mixedKeyWrapperHtml = null)
+    {
+        $aKeysSrc = array_keys($aVariables);
+        $aValuesSrc = array_values($aVariables);
+
+        $aKeyWrappers = $this->_getKeyWrappers($mixedKeyWrapperHtml);
+
+        $sKeyIf = 'bx_if:';
+        $sKeyRepeat = 'bx_repeat:';
+        $iCountKeys = count($aKeysSrc);
+        $aKeys = $aValues = array();
+
+        //--- Parse simple keys ---//
+        for ($i = 0; $i < $iCountKeys; $i++) {
+            if (strncmp($aKeysSrc[$i], $sKeyRepeat, 10) === 0 || strncmp($aKeysSrc[$i], $sKeyIf, 6) === 0)
+                continue;
+
+            $aKeys[] = "'" . $aKeyWrappers['left'] . $aKeysSrc[$i] . $aKeyWrappers['right'] . "'s";
+            if (is_string($aValuesSrc[$i]) || is_null($aValuesSrc[$i]))
+                $aValues[] = is_null($aValuesSrc[$i]) ? '' : str_replace('$', '\\$', str_replace('\\', '\\\\', $aValuesSrc[$i]));
+            else if(is_array($aValuesSrc[$i]))
+                $aValues[] = _t('_error occured');
+            else
+                $aValues[] = $aValuesSrc[$i];
+        }
+
+        //--- Parse keys with constructions ---//
+        for ($i = 0; $i < $iCountKeys; $i++) {
+            if (strncmp($aKeysSrc[$i], $sKeyRepeat, 10) === 0) {
+                $sKey = "'<" . $aKeysSrc[$i] . ">(.*)<\/" . $aKeysSrc[$i] . ">'s";
+
+                $aMatches = array();
+                preg_match($sKey, $sContent, $aMatches);
+
+                $sValue = '';
+                if(isset($aMatches[1]) && !empty($aMatches[1])) {
+                    if(is_array($aValuesSrc[$i]))
+                        foreach($aValuesSrc[$i] as $aValue)
+                            if(is_array($aValue))
+                                $sValue .= $this->parseHtmlByContent($aMatches[1], $aValue, $mixedKeyWrapperHtml);
+                            else if(is_string($aValue))
+                                $sValue .= $aValue;
+                    else if(is_string($aValuesSrc[$i]))
+                        $sValue = $aValuesSrc[$i];
+                }
+            } 
+            else if (strncmp($aKeysSrc[$i], $sKeyIf, 6) === 0) {
+                $sKey = "'<" . $aKeysSrc[$i] . ">(.*)<\/" . $aKeysSrc[$i] . ">'s";
+
+                $aMatches = array();
+                preg_match($sKey, $sContent, $aMatches);
+
+                $sValue = '';
+                if(isset($aMatches[1]) && !empty($aMatches[1]))
+                    if(is_array($aValuesSrc[$i]) && isset($aValuesSrc[$i]['content']) && isset($aValuesSrc[$i]['condition']) && $aValuesSrc[$i]['condition'])
+                        $sValue .= $this->parseHtmlByContent($aMatches[1], $aValuesSrc[$i]['content'], $mixedKeyWrapperHtml);
+            } 
+            else 
+                continue;
+
+            $aKeys[] = $sKey;
+            $aValues[] = str_replace('$', '\\$', str_replace('\\', '\\\\', $sValue));
+        }
+
+        try {
+            $oTemplate = &$this;
+
+            $aCallbackPatterns = array(
+                "'<bx_include_auto:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_BOTH,
+                "'<bx_include_auto_mod_general:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_general'),
+                "'<bx_include_auto_mod_profile:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_profile'),
+                "'<bx_include_auto_mod_group:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_group'),
+                "'<bx_include_auto_mod_text:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_text'),
+                "'<bx_include_base:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_BASE,
+                "'<bx_include_base_mod_general:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_general'),
+                "'<bx_include_base_mod_profile:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_profile'),
+                "'<bx_include_base_mod_group:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_group'),
+                "'<bx_include_base_mod_text:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_text'),
+                "'<bx_include_tmpl:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_TMPL,
+                "'<bx_include_tmpl_mod_general:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_general'),
+                "'<bx_include_tmpl_mod_profile:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_profile'),
+                "'<bx_include_tmpl_mod_group:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_group'),
+                "'<bx_include_tmpl_mod_text:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_text')
+            );
+            foreach($aCallbackPatterns as $sPattern => $sCheckIn)
+                $sContent = preg_replace_callback($sPattern, function($aMatches) use($oTemplate, $aVariables, $mixedKeyWrapperHtml, $sCheckIn) {
+                    return $oTemplate->parseHtmlByName($aMatches[1], $aVariables, $mixedKeyWrapperHtml, $sCheckIn);
+                }, $sContent);
+
+            $sContent = $this->_parseContentKeys($sContent, array(
+                "'<bx_menu:([^\s]+) \/>'s" => 'get_menu',
+            ));
+        }
+        catch(Exception $oException) {
+            bx_log('sys_template', "Error in _parseContent method. Cannot parse template insertion (<bx_include... />).\n" . 
+                "  Error ({$oException->getCode()}): {$oException->getMessage()}\n" . 
+                (getLoggedId() ? "  Account ID: " . getLoggedId() . "\n" : "")
+            );
+
+            return '';
+        }
+
+        $aKeys = array_merge($aKeys, array(
+            "'<bx_url_root />'",
+            "'<bx_url_studio />'",
+        ));
+        $aValues = array_merge($aValues, array(
+            BX_DOL_URL_ROOT,
+            BX_DOL_URL_STUDIO,
+        ));
+
+        //--- Parse Predefined Keys ---//
+        $sContent = preg_replace($aKeys, $aValues, $sContent);
+
+        //--- Parse System Keys ---//
+        try {
+            $sContent = preg_replace_callback("'" . $aKeyWrappers['left'] . "([a-zA-Z0-9_-]+)" . $aKeyWrappers['right'] . "'", function($aMatches) use($oTemplate, $mixedKeyWrapperHtml) {
+                return $oTemplate->parseSystemKey($aMatches[1], $mixedKeyWrapperHtml);
+            }, $sContent);
+        }
+        catch(Exception $oException) {
+            bx_log('sys_template', "Error in _parseContent method. Cannot parse System Keys.\n" . 
+                "  Error ({$oException->getCode()}): {$oException->getMessage()}\n" . 
+                (getLoggedId() ? "  Account ID: " . getLoggedId() . "\n" : "")
+            );
+
+            return '';
+        }
+
+        return $sContent;
+    }
+
+    /**
+     * Compile content
+     *
+     * @param  string  $sContent            template.
+     * @param  string  $aVarName            variable name to be saved in the output file.
+     * @param  integer $iVarDepth           depth is used to process nesting, for example, in cycles.
+     * @param  array   $aVarValues          values to be compiled in.
+     * @param  mixed   $mixedKeyWrapperHtml key wrapper(string value if left and right parts are the same, array(0 => left, 1 => right) otherwise).
+     * @return string  the result of operation.
+     */
+    function _compileContent($sContent, $aVarName, $iVarDepth, $aVarValues, $mixedKeyWrapperHtml = null)
+    {
+        $aKeys = array_keys($aVarValues);
+        $aValues = array_values($aVarValues);
+
+        $aKeyWrappers = $this->_getKeyWrappers($mixedKeyWrapperHtml);
+
+        for($i = 0; $i < count($aKeys); $i++) {
+            if(strpos($aKeys[$i], 'bx_repeat:') === 0) {
+                $sKey = "'<" . $aKeys[$i] . ">(.*)<\/" . $aKeys[$i] . ">'s";
+
+                $aMatches = array();
+                preg_match($sKey, $sContent, $aMatches);
+
+                $sValue = '';
+                if(isset($aMatches[1]) && !empty($aMatches[1])) {
+                    if(empty($aValues[$i]) || !is_array($aValues[$i]))
+                        return false;
+
+                    $sIndex = "\$" . str_repeat("i", $iVarDepth);
+                    $sValue .= "<?php if(is_array(" . $aVarName . "['" . $aKeys[$i] . "'])) for(" . $sIndex . "=0; " . $sIndex . "<count(" . $aVarName . "['" . $aKeys[$i] . "']); " . $sIndex . "++){ ?>";
+                    if(($sInnerValue = $this->_compileContent($aMatches[1], $aVarName . "['" . $aKeys[$i] . "'][" . $sIndex . "]", $iVarDepth + 1, current($aValues[$i]), $mixedKeyWrapperHtml)) === false)
+                        return false;
+                    $sValue .= $sInnerValue;
+                    $sValue .= "<?php } else if(is_string(" . $aVarName . "['" . $aKeys[$i] . "'])) echo " . $aVarName . "['" . $aKeys[$i] . "']; ?>";
+                }
+            } else if(strpos($aKeys[$i], 'bx_if:') === 0) {
+                $sKey = "'<" . $aKeys[$i] . ">(.*)<\/" . $aKeys[$i] . ">'s";
+
+                $aMatches = array();
+                preg_match($sKey, $sContent, $aMatches);
+
+                $sValue = '';
+                if(isset($aMatches[1]) && !empty($aMatches[1])) {
+                    if(!is_array($aValues[$i]) || !isset($aValues[$i]['content']) || empty($aValues[$i]['content']) || !is_array($aValues[$i]['content']))
+                        return false;
+
+                    $sValue .= "<?php if(" . $aVarName . "['" . $aKeys[$i] . "']['condition']){ ?>";
+                    if(($sInnerValue = $this->_compileContent($aMatches[1], $aVarName . "['" . $aKeys[$i] . "']['content']", $iVarDepth, $aValues[$i]['content'], $mixedKeyWrapperHtml)) === false)
+                        return false;
+                    $sValue .= $sInnerValue;
+                    $sValue .= "<?php } ?>";
+                }
+            } else {
+                $sKey = "'" . $aKeyWrappers['left'] . $aKeys[$i] . $aKeyWrappers['right'] . "'s";
+                $sValue = "<?php echo " . $aVarName . "['" . $aKeys[$i] . "'];?>";
+            }
+
+            $aKeys[$i] = $sKey;
+            $aValues[$i] = $sValue;
+        }
+
+        try {
+            $oTemplate = &$this;
+
+            $aCallbackPatterns = array(
+                "'<bx_include_auto:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_BOTH,
+                "'<bx_include_auto_mod_general:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_general'),
+                "'<bx_include_auto_mod_profile:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_profile'),
+                "'<bx_include_auto_mod_group:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_group'),
+                "'<bx_include_auto_mod_text:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_text'),
+                "'<bx_include_base:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_BASE,
+                "'<bx_include_base_mod_general:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_general'),
+                "'<bx_include_base_mod_profile:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_profile'),
+                "'<bx_include_base_mod_group:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_group'),
+                "'<bx_include_base_mod_text:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_text'),
+                "'<bx_include_tmpl:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_TMPL,
+                "'<bx_include_tmpl_mod_general:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_general'),
+                "'<bx_include_tmpl_mod_profile:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_profile'),
+                "'<bx_include_tmpl_mod_group:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_group'),
+                "'<bx_include_tmpl_mod_text:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_text')
+            );
+            foreach($aCallbackPatterns as $sPattern => $sCheckIn)
+                $sContent = preg_replace_callback($sPattern, function($aMatches) use($oTemplate, $aVarValues, $mixedKeyWrapperHtml, $sCheckIn) {
+                    $mixedResult = $oTemplate->getCached($aMatches[1], $aVarValues, $mixedKeyWrapperHtml, $sCheckIn, false);
+                    if($mixedResult === false)
+                        throw new Exception("Unable to create cache file ({$aMatches[1]}).", 1);
+
+                    return $mixedResult;
+                }, $sContent);
+
+            $sContent = $this->_parseContentKeys($sContent);
+        }
+        catch(Exception $oException) {
+            if(($iCode = $oException->getCode()) != 1)
+                bx_log('sys_template', "Error in _compileContent method. Cannot parse template insertion (<bx_include... />).\n" . 
+                    "  Error ({$iCode}): {$oException->getMessage()}\n" . 
+                    (getLoggedId() ? "  Account ID: " . getLoggedId() . "\n" : "")
+                );
+
+            return false;
+        }
+
+        $aKeys = array_merge($aKeys, array(
+            "'<bx_menu:([^\s]+) \/>'s",
+            "'<bx_url_root />'",
+            "'<bx_url_studio />'"
+        ));
+        $aValues = array_merge($aValues, array(
+            "<?php echo \$this->getMenu('\\1'); ?>",
+            BX_DOL_URL_ROOT,
+            BX_DOL_URL_STUDIO
+        ));
+
+        //--- Parse Predefined Keys ---//
+        $sContent = preg_replace($aKeys, $aValues, $sContent);
+
+        //--- Parse System Keys ---//
+        $sContent = preg_replace( "'" . $aKeyWrappers['left'] . "([a-zA-Z0-9_-]+)" . $aKeyWrappers['right'] . "'", "<?php echo \$this->parseSystemKey('\\1', \$mixedKeyWrapperHtml);?>", $sContent);
+
+        return $sContent;
+    }
+    protected function _parseContentKeys($sContent, $aCallbackPatterns = array())
+    {
+    	$oTemplate = &$this;
+
+    	$aCallbackPatterns = array_merge($aCallbackPatterns, array(
+            "'<bx_image_auto:([^\s]+) \/>'s" => "get_image_auto",
+            "'<bx_image_url:([^\s]+) \/>'s" => "get_image_url",
+            "'<bx_icon_url:([^\s]+) \/>'s" => "get_icon_url",
+            "'<bx_text:([_\{\}\w\d\s]+[^\s]{1}) \/>'s" => "get_text",
+            "'<bx_text_js:([^\s]+) \/>'s" => "get_text_js",
+            "'<bx_text_attribute:([^\s]+) \/>'s" => "get_text_attribute",
+            "'<bx_page:([^\s]+) \/>'s" => "get_page",
+        ));
+
+        foreach($aCallbackPatterns as $sPattern => $sAction)
+            $sContent = preg_replace_callback($sPattern, function($aMatches) use($oTemplate, $sAction) {
+            $sResult = '';
+
+            switch($sAction) {
+                case 'get_image_auto':
+                    $sResult = $oTemplate->getImageAuto($aMatches[1]);
+                    break;
+                case 'get_image_url':
+                    $sResult = $oTemplate->getImageUrl($aMatches[1]);
+                    break;
+                case 'get_icon_url':
+                    $sResult = $oTemplate->getIconUrl($aMatches[1]);
+                    break;
+                case 'get_text':
+                    $sResult = _t($aMatches[1]);
+                    break;
+                case 'get_text_js':
+                    $sResult = bx_js_string(_t($aMatches[1]));
+                    break;
+                case 'get_text_attribute':
+                    $sResult = bx_html_attribute(_t($aMatches[1]));
+                    break;
+                case 'get_injection':
+                    $sResult = $oTemplate->processInjection($oTemplate->getPageNameIndex(), $aMatches[1]);
+                    break;
+                case 'get_menu':
+                    $sResult = $oTemplate->getMenu($aMatches[1]);
+                    break;
+                case 'get_page':
+                    $oPage = BxDolPage::getObjectInstanceByURI($aMatches[1], false, true);
+                    $oPage->setSubPage(true);
+                    $sResult = $oPage ? $oPage->getCode() : '';
+                    break;
+            }
+
+            return $sResult;
+        }, $sContent);
+
+        return $sContent;
+    }
+
+    /**
+     * Get absolute location of some template's part.
+     *
+     * @param  string $sType    - result type. Available values 'url' and 'path'.
+     * @param  string $sFolder  - folders to be searched in. @see $_sFolderHtml, $_sFolderCss, $_sFolderImages and $_sFolderIcons
+     * @param  string $sName    - requested part name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string absolute location (path/url) of the part.
+     */
+    function _getAbsoluteLocation($sType, $sFolder, $sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        $sDirectory = $this->getPath();
+
+        if($sType == 'path') {
+            $sDivider = DIRECTORY_SEPARATOR;
+            $sRoot = BX_DIRECTORY_PATH_ROOT;
+        } else if($sType == 'url') {
+            $sDivider = '/';
+            $sRoot = BX_DOL_URL_ROOT;
+        }
+
+        if(strpos($sName,'|') !== false) {
+            $aParts = explode('|', $sName);
+            $sName = $aParts[1];
+
+            if(strpos($aParts[0],'@') !== false) {
+                $aLocationParts = explode('@', $aParts[0]);
+                $sLocationKey = $this->addLocation($aLocationParts[0], BX_DIRECTORY_PATH_ROOT . $aLocationParts[1], BX_DOL_URL_ROOT . $aLocationParts[1]);
+            }
+        }
+
+        /** 
+         * Module(mod) related locations will be checked first in TMPL and BASE,
+         * then system(sys) location(s) will be checked in TMPL and BASE.
+         */
+        $aLocationsList = array_reverse($this->_aLocations, true);
+        $aLocationsGrouped = ['mod' => [], 'sys' => []];
+        foreach($aLocationsList as $sLocation => $aLocation) {
+            if($sLocation == 'system')
+                $aLocationsGrouped['sys'][$sLocation] = $aLocation;
+            else
+                $aLocationsGrouped['mod'][$sLocation] = $aLocation;
+        }
+
+        $sResult = '';
+        foreach($aLocationsGrouped as $aLocations) {
+            //--- Check it Template.
+            $bInSub = false;
+            $aCheckIn = [BX_DOL_TEMPLATE_CHECK_IN_BOTH, BX_DOL_TEMPLATE_CHECK_IN_TMPL];
+            if(in_array($sCheckIn, $aCheckIn) || $bInSub = (isset($sCheckIn['in'], $sCheckIn['sub']) && in_array($sCheckIn['in'], $aCheckIn)))
+                foreach($aLocations as $sKey => $aLocation)
+                    if((!$bInSub || $sCheckIn['sub'] == $sKey) && extFileExists(BX_DIRECTORY_PATH_MODULES . $this->getPath(). 'data' . DIRECTORY_SEPARATOR . BX_DOL_TEMPLATE_FOLDER_ROOT . DIRECTORY_SEPARATOR . $sKey . DIRECTORY_SEPARATOR . $sFolder . $sName)) {
+                        $sResult = $sRoot . 'modules' . $sDivider . $sDirectory. 'data' . $sDivider . BX_DOL_TEMPLATE_FOLDER_ROOT . $sDivider . $sKey . $sDivider . $sFolder . $sName;
+                        break 2;
+                    }
+
+            //--- Check it Base.
+            $bInSub = false;
+            $aCheckIn = [BX_DOL_TEMPLATE_CHECK_IN_BOTH, BX_DOL_TEMPLATE_CHECK_IN_BASE];
+            if(empty($sResult) && (in_array($sCheckIn, $aCheckIn) || $bInSub = (isset($sCheckIn['in'], $sCheckIn['sub']) && in_array($sCheckIn['in'], $aCheckIn))))
+                foreach($aLocations as $sKey => $aLocation)
+                    if((!$bInSub || $sCheckIn['sub'] == $sKey) && extFileExists($aLocation['path'] . BX_DOL_TEMPLATE_FOLDER_ROOT . DIRECTORY_SEPARATOR . $sFolder . $sName)) {
+                        $sResult = $aLocation[$sType] . BX_DOL_TEMPLATE_FOLDER_ROOT . $sDivider . $sFolder . $sName;
+                        break 2;
+                    }
+        }
+
+        /**
+         * try to find from received path
+         */
+        if(!$sResult && @is_file(BX_DIRECTORY_PATH_ROOT . $aParts[0] . DIRECTORY_SEPARATOR . $aParts[1])) {
+            $sResult = $sRoot . $aParts[0] . $sDivider . $aParts[1];
+        }
+
+        if(isset($sLocationKey))
+           $this->removeLocation($sLocationKey);
+
+        return $sType == 'path' && !empty($sResult) ? realpath($sResult) : $sResult;
+    }
+    /**
+     * Get absolute location of some template's part.
+     *
+     * @param  string $sType result type. Available values 'url' and 'path'.
+     * @param  string $sName requested part name.
+     * @return string absolute location (path/url) of the part.
+     */
+    function _getAbsoluteLocationJs($sType, $sName)
+    {
+        $sResult = '';
+        $aLocations = array_reverse($this->_aLocationsJs, true);
+        foreach($aLocations as $sKey => $aLocation) {
+            if(extFileExists($aLocation['path'] . $sName))
+                $sResult = $aLocation[$sType] . $sName;
+            else
+                continue;
+            break;
+        }
+        return $sType == 'path' && !empty($sResult) ? realpath($sResult) : $sResult;
+    }
+    function _getAbsoluteLocationCss($sType, $sName)
+    {
+    	$sNameLess = str_replace('.css', '.less', $sName);
+
+    	$sResult = $this->_getAbsoluteLocation($sType, $this->_sFolderCss, $sNameLess);
+    	if(!empty($sResult))
+    		return $sResult;
+
+        return $this->_getAbsoluteLocation($sType, $this->_sFolderCss, $sName);
+    }
+    /**
+     * Get inline data for Images and Icons.
+     *
+     * @param  string  $sType    image/icon
+     * @param  string  $sName    file name
+     * @param  string  $sCheckIn where the content would be searched(base, template, both)
+     * @return unknown
+     */
+    function _getInlineData($sType, $sName, $sCheckIn)
+    {
+        switch($sType) {
+            case 'image':
+                $sFolder = $this->_sFolderImages;
+                break;
+            case 'icon':
+                $sFolder = $this->_sFolderIcons;
+                break;
+        }
+        $sPath = $this->_getAbsoluteLocation('path', $sFolder, $sName, $sCheckIn);
+
+        $iFileSize = 0;
+        if($this->_bImagesInline && ($iFileSize = filesize($sPath)) !== false && $iFileSize < $this->_iImagesMaxSize) {
+            $aFileInfo = pathinfo($sPath);
+            return $this->getImageMimeType($aFileInfo['extension']) . ";base64," . base64_encode(file_get_contents($sPath));
+        }
+
+        return false;
+    }
+
+    /**
+     * Get file name where the template would be cached.
+     *
+     * @param  string $sAbsolutePath template's real path.
+     * @return string the result of operation.
+     */
+    function _getCacheFileName($sType, $sAbsolutePath)
+    {
+        $sResult = bx_site_hash($sAbsolutePath);
+        switch($sType) {
+            case 'html':
+                $sResult = $this->_sCacheFilePrefix . bx_lang_name() . '_' . $this->_sCode .  '_' . $sResult;
+                break;
+            case 'css':
+                $sResult = $this->_sCssCachePrefix . (!empty($this->_iMix) ? $this->_iMix . '_' : '') .  $sResult;
+                break;
+            case 'js':
+                $sResult = $this->_sJsCachePrefix . $sResult;
+                break;
+        }
+
+        return $sResult;
+    }
+    /**
+     * Get template key wrappers(left, right)
+     *
+     * @param  mixed $mixedKeyWrapperHtml key wrapper(string value if left and right parts are the same, array(0 => left, 1 => right) otherwise).
+     * @return array result of operation.
+     */
+    function _getKeyWrappers($mixedKeyWrapperHtml)
+    {
+        $aResult = array();
+        if(!empty($mixedKeyWrapperHtml) && is_string($mixedKeyWrapperHtml))
+            $aResult = array('left' => $mixedKeyWrapperHtml, 'right' => $mixedKeyWrapperHtml);
+        else if(!empty($mixedKeyWrapperHtml) && is_array($mixedKeyWrapperHtml))
+            $aResult = array('left' => $mixedKeyWrapperHtml[0], 'right' => $mixedKeyWrapperHtml[1]);
+        else
+            $aResult = array('left' => $this->_sKeyWrapperHtml, 'right' => $this->_sKeyWrapperHtml);
+        return $aResult;
+    }
+
+    /**
+     * Process all added language translations and return them as a string.
+     *
+     * @return string with JS code.
+     */
+    function _processJsTranslations()
+    {
+        $sReturn = '';
+        if(isset($this->aPage['js_translations']) && is_array($this->aPage['js_translations'])) {
+            foreach($this->aPage['js_translations'] as $sKey => $sString)
+                $sReturn .= "'" .  bx_js_string($sKey) . "': '" . bx_js_string($sString) . "',";
+
+            $sReturn = substr($sReturn, 0, -1);
+        }
+
+        return '
+<script language="javascript">
+    if (\'undefined\' === typeof(aDolLang)) 
+        var aDolLang = {' . $sReturn . '};
+    else
+        $.extend(aDolLang, {' . $sReturn . '});
+</script>';
+    }
+    /**
+     * Process all added options and return them as a string.
+     *
+     * @return string with JS code.
+     */
+    function _processJsOptions()
+    {
+        $sReturn = '';
+        if(isset($this->aPage['js_options']) && is_array($this->aPage['js_options'])) {
+            foreach($this->aPage['js_options'] as $sName => $mixedValue)
+                $sReturn .= "'" .  bx_js_string($sName) . "': '" . bx_js_string($mixedValue) . "',";
+
+            $sReturn = substr($sReturn, 0, -1);
+        }
+
+        return '<script language="javascript">var aDolOptions = {' . $sReturn . '};</script>';
+    }
+    /**
+     * Process all added images and return them as a string.
+     *
+     * @return string with JS code.
+     */
+    function _processJsImages()
+    {
+        $sReturn = '';
+        if(isset($this->aPage['js_images']) && is_array($this->aPage['js_images'])) {
+            foreach($this->aPage['js_images'] as $sKey => $sUrl)
+                $sReturn .= "'" .  bx_js_string($sKey) . "': '" . bx_js_string($sUrl) . "',";
+
+            $sReturn = substr($sReturn, 0, -1);
+        }
+
+        return '<script language="javascript">var aDolImages = {' . $sReturn . '};</script>';
+    }
+
+    /**
+     * Get Gzip loader URL.
+     *
+     * @param $sType content type CSS/JS
+     * @param $sName file name.
+     * @return string with URL
+     */
+    function _getLoaderUrl($sType, $sName)
+    {
+        return BX_DOL_URL_ROOT . 'gzip_loader.php?file=' . $sName . '.' . $sType;
+    }
+
+    /**
+     * Get current revision number.
+     * 
+     * @return integer number
+     */
+    function _getRevision()
+    {
+        return (int)getParam('sys_revision');
+    }
+
+    /**
+     *
+     * Functions to display pages with errors, messages and so on.
+     *
+     */
+    function displayAccessDenied ($sMsg = '', $iPage = BX_PAGE_DEFAULT, $iDesignBox = BX_DB_PADDING_DEF)
+    {
+        bx_import('BxDolLanguages');
+        header('HTTP/1.0 403 Forbidden');
+        header('Status: 403 Forbidden');
+        $this->displayMsg($sMsg ? $sMsg : _t('_Access denied'), false, $iPage, $iDesignBox);
+    }
+
+    function displayNoData ($sMsg = '', $iPage = BX_PAGE_DEFAULT, $iDesignBox = BX_DB_PADDING_DEF)
+    {
+        bx_import('BxDolLanguages');
+        header('HTTP/1.0 204 No Content');
+        header('Status: 204 No Content');
+        $this->displayMsg($sMsg ? $sMsg : _t('_Empty'), false, $iPage, $iDesignBox);
+    }
+
+    function displayErrorOccured ($sMsg = '', $iPage = BX_PAGE_DEFAULT, $iDesignBox = BX_DB_PADDING_DEF)
+    {
+        bx_import('BxDolLanguages');
+        header('HTTP/1.0 500 Internal Server Error');
+        header('Status: 500 Internal Server Error');
+        $this->displayMsg($sMsg ? $sMsg : _t('_error occured'), false, $iPage, $iDesignBox);
+    }
+
+    function displayPageNotFound ($sMsg = '', $iPage = BX_PAGE_DEFAULT, $iDesignBox = BX_DB_PADDING_DEF)
+    {
+        bx_import('BxDolLanguages');
+        header('HTTP/1.0 404 Not Found');
+        header('Status: 404 Not Found');
+        $this->displayMsg($sMsg ? $sMsg : _t('_sys_request_page_not_found_cpt'), false, $iPage, $iDesignBox);
+    }
+
+    function displayMsg ($s, $bTranslate = false, $iPage = BX_PAGE_DEFAULT, $iDesignBox = BX_DB_PADDING_DEF)
+    {
+        $sError = '_Error';
+        $bArray = is_array($s);
+
+        $sTitle = $bArray ? $s['title'] : ($bTranslate ? $sError : _t($sError));
+        $sContent = $bArray ? $s['content'] : $s;
+
+        if($bTranslate) {
+            $sTitle = _t($sTitle);
+            $sContent = _t($sContent);
+        }
+
+        $sContent = MsgBox($sContent);
+        if($iPage == BX_PAGE_DEFAULT)
+            $sContent = DesignBoxContent($sTitle, $sContent, $iDesignBox);
+
+        $oTemplate = BxDolTemplate::getInstance();
+        $oTemplate->setPageNameIndex ($iPage);
+        $oTemplate->setPageHeader ($sTitle);
+        $oTemplate->setPageContent ('page_main_code', $sContent);
+        $oTemplate->getPageCode();
+        exit;
+    }
+    
+    /**
+     * * * * Static methods for work with template injections * * *
+     *
+     * Static method is used to add/replace the content of some key in the template.
+     * It's usefull when you don't want to modify existing template but need to add some data to existing template key.
+     *
+     * @param  integer $iPageIndex - page index where injections would processed. Use 0 if you want it to be done on all the pages.
+     * @param  string  $sKey       - template key.
+     * @param  string  $sValue     - the data to be added.
+     * @return string  the result of operation.
+     */
+    function processInjection($iPageIndex, $sKey, $sValue = "")
+    {
+        if($iPageIndex != 0 && isset($this->aPage['injections']['page_0'][$sKey]) && isset($this->aPage['injections']['page_' . $iPageIndex][$sKey]))
+           $aSelection = @array_merge($this->aPage['injections']['page_0'][$sKey], $this->aPage['injections']['page_' . $iPageIndex][$sKey]);
+        else if(isset($this->aPage['injections']['page_0'][$sKey]))
+           $aSelection = $this->aPage['injections']['page_0'][$sKey];
+        else if(isset($this->aPage['injections']['page_' . $iPageIndex][$sKey]))
+            $aSelection = $this->aPage['injections']['page_' . $iPageIndex][$sKey];
+        else
+            $aSelection = array();
+
+        if(is_array($aSelection))
+            foreach($aSelection as $aInjection) {
+
+                if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->beginInjection($sRand = time().rand());
+
+                $sInjData = '';
+                switch($aInjection['type']) {
+                    case 'text':
+                        $sInjData = $aInjection['data'];
+                        break;
+
+                    case 'service':
+                    	if(BxDolService::isSerializedService($aInjection['data']))
+                    		$sInjData = BxDolService::callSerialized($aInjection['data']);                    	
+                        break;
+                }
+
+                if((int)$aInjection['replace'] == 1)
+                    $sValue = $sInjData;
+                else
+                    $sValue .= $sInjData;
+
+                if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->endInjection($sRand, $aInjection);
+
+            }
+
+        return $sValue != '__' . $sKey . '__' ? str_replace('__' . $sKey . '__', '', $sValue) : $sValue;
+    }
+    /**
+     * Static method to add ingection available on the current page only.
+     *
+     * @param string  $sKey     - template's key.
+     * @param string  $sType    - injection type(text, php).
+     * @param string  $sData    - the data to be added.
+     * @param integer $iReplace - replace already existed data or not.
+     */
+    function addInjection($sKey, $sType, $sData, $iReplace = 0)
+    {
+        $this->aPage['injections']['page_0'][$sKey][] = array(
+            'page_index' => 0,
+            'key' => $sKey,
+            'type' => $sType,
+            'data' => $sData,
+            'replace' => $iReplace
+        );
+    }
+
+    function getPageCode($oTemplate = null)
+    {
+        if (empty($oTemplate))
+           $oTemplate = $this;
+
+        bx_alert('system', 'design_before_output', 0, 0, ['page' => &$this->aPage, 'page_content' => &$this->aPageContent]);
+
+        header( 'Content-type: text/html; charset=utf-8' );
+        header( 'X-Frame-Options: sameorigin' );
+        if (BX_PAGE_EMBED == $oTemplate->getPageNameIndex())
+            header('Content-Security-Policy: frame-ancestors ' . getParam('sys_csp_frame_ancestors'));
+
+        $sResult = $oTemplate->parsePageByName('page_' . $oTemplate->getPageNameIndex() . '.html', $oTemplate->getPageContent());
+        
+        bx_alert('system', 'design_after_output', 0, false, ['override_result' => &$sResult]);
+        
+        echo $sResult;
+    }
+}
+
+/** @} */

+ 305 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/classes/BxDolVoteReactions.php

@@ -0,0 +1,305 @@
+<?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
+ * @{
+ */
+
+/**
+ * Like based votes for any content
+ */
+class BxDolVoteReactions extends BxTemplVote
+{
+    protected static $_sCounterStyleSimple = 'simple'; // total counter only.
+    protected static $_sCounterStyleDivided = 'divided'; // counters [icon + counter] divided by reactions.
+    protected static $_sCounterStyleCompound = 'compound'; // total counter with a list of reaction icons.
+
+    protected $_sMenuDoVote; //--- Do vote (reaction)  menu name.
+
+    protected $_sDataList; //--- Reactions list name.
+    protected $_aDataList; //--- Reactions list with all reactions.
+
+    protected $_sDefault; //--- Default reaction name.
+
+    protected $_bQuickMode; //--- Give 'default' reaction when clicked.
+
+    public function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
+    {
+        parent::__construct($sSystem, $iId, $iInit, $oTemplate);
+
+        $this->_oQuery = new BxDolVoteReactionsQuery($this);
+        $this->_sType = BX_DOL_VOTE_TYPE_REACTIONS;
+
+        $this->_sMenuDoVote = 'sys_vote_reactions_do';
+
+        $this->_sDataList = 'sys_vote_reactions';
+        $this->_aDataList = array();
+
+        $this->_sDefault = 'default';
+
+        $this->_bQuickMode = getParam('sys_vote_reactions_quick_mode') == 'on';
+    }
+
+    public function init($iId)
+    {
+        if(!parent::init($iId))
+            return false;
+
+        $aReactions = BxDolFormQuery::getDataItems($this->_sDataList, false, BX_DATA_VALUES_ALL);
+
+        $sDefault = '';
+        foreach($aReactions as $sReaction => $aReaction) {
+            $aData = !empty($aReaction['Data']) ? unserialize($aReaction['Data']) : array();
+
+            if(!empty($aData['default']))
+                $sDefault = $sReaction;
+
+            $this->_aDataList[$sReaction] = array(
+                'name' => $sReaction,
+                'title' => $aReaction['LKey'],
+                'title_aux' => $aReaction['LKey2'],
+                'use' => isset($aData['use']) ? $aData['use'] : 'emoji',
+                'icon' => isset($aData['icon']) ? $aData['icon'] : '',
+                'emoji' => isset($aData['emoji']) ? $aData['emoji'] : '',
+                'image' => isset($aData['image']) ? $aData['image'] : '',
+                'color' => isset($aData['color']) ? $aData['color'] : '',
+                'weight' => isset($aData['weight']) ? $aData['weight'] : 1,
+                'default' => isset($aData['default']) ? $aData['default'] : '',
+            );
+        }
+
+        if(empty($sDefault) && !empty($this->_aDataList))
+            $sDefault = current(array_keys($this->_aDataList));
+
+        $this->_aDataList[$this->_sDefault] = array_merge($this->_aDataList[$sDefault], [
+            'icon' => $this->_aDataList[$sDefault]['default'],
+            'emoji' => '',
+            'image' => '',
+            'color' => ''
+        ]);
+    }
+    /**
+     * Interface functions for outer usage
+     */
+    public function getValue()
+    {
+        return (int)$this->_aSystem['min_value'];
+    }
+
+    public function getDefault()
+    {
+        return $this->_sDefault;
+    }
+
+    public function getReaction($sName)
+    {
+        if(empty($this->_aDataList))
+            $this->getReactions();
+
+        return !empty($this->_aDataList[$sName]) ? $this->_aDataList[$sName] : false;
+    }
+
+    public function getReactions($bFullInfo = false)
+    {
+        return $bFullInfo ? $this->_aDataList : array_keys($this->_aDataList);
+    }
+
+    public function getTrackBy($aParams)
+    {
+        return $this->_oQuery->getTrackBy($aParams);
+    }
+    
+    public function getIcon($sReaction, $bWithColor = true)
+    {
+        $aReaction = isset($this->_aDataList[$sReaction]) ? $this->_aDataList[$sReaction] : $this->_aDataList[$this->_sDefault];
+
+    	return $aReaction['icon'] . ($bWithColor && !empty($aReaction['color']) ? ' ' . $aReaction['color'] : '');
+    }
+    
+    public function getEmoji($sReaction, $bWithColor = true)
+    {
+        $aReaction = isset($this->_aDataList[$sReaction]) ? $this->_aDataList[$sReaction] : $this->_aDataList[$this->_sDefault];
+
+    	return $aReaction['emoji'];
+    }
+
+    public function getImage($sReaction, $bWithColor = true)
+    {
+        $aReaction = isset($this->_aDataList[$sReaction]) ? $this->_aDataList[$sReaction] : $this->_aDataList[$this->_sDefault];
+
+    	return $aReaction['image'];
+    }
+
+    /**
+     * Actions functions
+     */
+    public function actionGetDoVotePopup()
+    {
+        if(!$this->isEnabled())
+           return '';
+
+        return $this->_getDoVotePopup((int)bx_get('value'));
+    }
+
+    public function actionGetVotedBy()
+    {
+        if(!$this->isEnabled())
+           return '';
+
+        $aParams = array();
+
+        $sReaction = bx_get('reaction');
+        if($sReaction !== false) {
+            $sReaction = bx_process_input($sReaction);
+            
+            $aReactions = $this->getReactions();
+            if(!in_array($sReaction, $aReactions))
+                $sReaction = $this->_sDefault;
+
+            $aParams['reaction'] = $sReaction;
+        }
+
+        return $this->_getVotedBy($aParams);
+    }
+
+    /**
+     * Internal functions
+     */
+    protected function _isVote($iObjectId = 0, $bForceGet = false)
+    {
+        $aVote = $this->_getVote($iObjectId, $bForceGet);
+        foreach($aVote as $sKey => $iValue)
+            if(strpos($sKey, 'count_') !== false && !empty($iValue))
+                return true;
+
+        return false;
+    }
+
+    protected function _isDuplicate($iObjectId, $iAuthorId, $iAuthorIp, $bVoted)
+    {
+        return $bVoted && !$this->isUndo();
+    }
+
+    protected function _isCount($aVote = array())
+    {
+        if(empty($aVote))
+            $aVote = $this->_getVote();
+
+        foreach($aVote as $sKey => $mixedValue)
+            if(substr($sKey, 0, 5) == 'count' && (int)$mixedValue != 0)
+                return true;
+
+        return false;
+    }
+
+    protected function _getVoteData()
+    {
+        $aData = parent::_getVoteData();
+        if($aData === false)
+            return false;
+
+        $sReaction = bx_get('reaction');
+        if($sReaction === false)
+            return false;
+
+        $aData['reaction'] = bx_process_input($sReaction);
+        return $aData;
+    }
+
+    protected function _returnVoteData($iObjectId, $iAuthorId, $iAuthorIp, $aData, $bVoted, $aParams = array())
+    {
+        $aReactions = $this->getReactions(true);
+        $sReaction = $aData['reaction'];
+
+        $bUndo = $this->isUndo();
+        $bDisabled = $bVoted && !$bUndo;
+
+        $aVote = $this->_getVote($iObjectId, true);
+        $aTrack = $bVoted ? $this->_getTrack($iObjectId, $iAuthorId) : [];
+
+        $sSwitchTo = $bVoted ? $sReaction : $this->_sDefault;
+
+        $sLabelUse = !empty($aReactions[$sSwitchTo]['use']) ? $aReactions[$sSwitchTo]['use'] : 'emoji';
+        $sLabelIcon = $this->_getIconDoWithTrack($bVoted, $aTrack);
+        if(!$bVoted && $sLabelUse != 'icon') {
+            $sLabelUse = 'icon';
+            $sLabelIcon = $this->_oTemplate->parseIcon($sLabelIcon);
+        }
+
+        $sJsClick = '';
+        if(!$bDisabled)
+            $sJsClick = $bVoted && $bUndo ? $this->getJsClickDo($sReaction) : $this->getJsClick();
+
+        $iTotalC = $iTotalS = 0;
+        foreach(array_keys($aReactions) as $sName) {
+            $iTotalC += (int)$aVote['count_' . $sName];
+            $iTotalS += (int)$aVote['sum_' . $sName];
+        }
+        $fTotalR = $iTotalC != 0 ? round($iTotalS / $iTotalC, 2) : 0;
+
+        $iCount = (int)$aVote['count_' . $sReaction];
+        $aResult = array(
+            'code' => 0,
+            'reaction' => $aReactions[$sSwitchTo]['name'],
+            'rate' => $aVote['rate_' . $sReaction],
+            'count' => $iCount,
+            'countf' => $iCount > 0 ? $this->_getCounterLabel($iCount, array('reaction' => $sReaction)) : '',
+            'label_use' => $sLabelUse,
+            'label_icon' => $sLabelIcon,
+            'label_emoji' => $this->_getEmojiDoWithTrack($bVoted, $aTrack),
+            'label_image' => $this->_getImageDoWithTrack($bVoted, $aTrack),
+            'label_title' => _t($this->_getTitleDoWithTrack($bVoted, $aTrack)),
+            'label_click' => $sJsClick,
+            'voted' => $bVoted,
+            'disabled' => $bVoted && !$this->isUndo(),
+            'total' => array(
+                'rate' => $fTotalR,
+                'count' => $iTotalC,
+                'countf' => $iTotalC > 0 ? $this->_getCounterLabel($iTotalC, array('show_counter_label_icon' => false, 'reaction' => '')) : '',
+            )
+        );
+
+        return $aResult;
+    }
+
+    protected function _getIconDoWithTrack($bVoted, $aTrack = array())
+    {
+        $sReaction = $bVoted ? $aTrack['reaction'] : $this->_sDefault;
+
+    	return $this->getIcon($sReaction);
+    }
+    
+    protected function _getEmojiDoWithTrack($bVoted, $aTrack = array())
+    {
+        $sReaction = $bVoted ? $aTrack['reaction'] : $this->_sDefault;
+
+    	return $this->getEmoji($sReaction);
+    }
+    
+    protected function _getImageDoWithTrack($bVoted, $aTrack = array())
+    {
+        $sReaction = $bVoted ? $aTrack['reaction'] : $this->_sDefault;
+
+    	return $this->getImage($sReaction);
+    }
+
+    protected function _getTitleDoWithTrack($bVoted, $aTrack = array())
+    {
+        $sReaction = $bVoted ? $aTrack['reaction'] : $this->_sDefault;
+
+    	return $this->_aDataList[$sReaction]['title'];
+    }
+    
+    protected function _getTitleDoBy($aParams = array())
+    {
+        if(isset($aParams['show_counter_style']) && $aParams['show_counter_style'] == self::$_sCounterStyleCompound)
+            return _t('_vote_do_by_reactions');
+
+        $sReaction = !empty($aParams['reaction']) ? $aParams['reaction'] : $this->_sDefault;
+    	return _t('_vote_do_by_x_reaction', _t($this->_aDataList[$sReaction]['title']));
+    }
+}
+
+/** @} */

+ 500 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/js/editor.quill.js

@@ -0,0 +1,500 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+function bx_editor_init(oEditor, oParams){
+    hljs.configure({   
+        languages: ['javascript', 'php', 'html', 'css']
+    });
+
+    bEmptyToolbar = oParams.toolbar == false ? true : false;
+    
+    if (oParams.query_params == undefined)
+        oParams.query_params = {};
+
+    oParams.toolbar = {
+        container: bEmptyToolbar ? [] : oParams.toolbar,
+            handlers: {
+            'show-html': function() {
+                 bx_alert('<textarea class="bx-def-font-inputs bx-form-input-textarea bx-form-input-html-quill-code" id="' + oParams.name + '_code">' + oEditor.container.firstChild.innerHTML + '</textarea>',function() {
+                     oEditor.container.firstChild.innerHTML = $('#'+oParams.name+'_code').val();
+                 });
+            },  
+            'embed': function(value) {
+                var range = oEditor.getSelection();
+                bx_prompt(_t('_sys_txt_quill_tooltip_embed_popup_header'), '', function(oPopup){
+                    sLink = $(oPopup).find("input[type = 'text']").val()
+                    $.getJSON(oParams.root_url + 'embed.php?', {a: 'get_link', l: sLink}, function(aData){
+                        if (range) {
+                            oEditor.clipboard.dangerouslyPasteHTML(range.index, "</p>&#8205; <p>&#8205; </p>", 'api');
+                            oEditor.insertEmbed(range.index+2, 'embed-link', {source: sLink, inlinecode : aData.code}, 'api');
+                        }
+                        else{
+                            oEditor.insertEmbed(0, 'embed-link', {source: sLink, inlinecode : aData.code}, 'api');
+                        }
+                            
+                        if ($(oParams.selector).next().next().find('.bx-embed-link a[href="' + aData.link + '"]').length > 0)
+                            bx_embed_link($(oParams.selector).next().next().find('.bx-embed-link a[href="' + aData.link + '"]')[0]);
+                        else
+                            bx_embed_link();
+                    });
+                });
+            },
+            'link': function(value) {
+                if (value) {
+                    var range = oEditor.getSelection();
+                    bx_prompt(_t('_sys_txt_quill_tooltip_link_popup_header'), '', function(oPopup){
+                        var sLink = $(oPopup).find("input[type = 'text']").val();
+                        if (range) {
+                            if (range.length > 0){
+                                oEditor.format('link', sLink);
+                            }
+                            else{
+                                oEditor.insertText(range.index, sLink, 'link', sLink);
+                            }
+                        }
+                        else{
+                            oEditor.insertText(0, sLink, 'link', sLink);
+                        }
+                    });
+                } else {
+                    oEditor.format('link', false);
+                }
+            }
+        }
+    }
+    
+    sVal = $(oParams.selector).val();    
+    if(!oParams.empty_tags){
+        sVal = sVal.replaceAll('<p><br></p>','');    
+    }
+    
+    $(oParams.selector).after("<div id='" + oParams.name + "' class='bx-def-font-inputs bx-form-input-textarea bx-form-input-html bx-form-input-html-quill " + oParams.css_class + "'>" + $(oParams.selector).val() + "</div>" );
+    
+    $(oParams.selector).hide();
+    
+    $(oParams.selector).attr('object_editor', oParams.name);
+                
+    if (typeof bQuillRegistred === 'undefined' ) {
+        
+        var Embed = Quill.import('blots/embed');
+        
+        const BaseLink = Quill.import("formats/link");
+        class Link extends BaseLink {
+            format(name, value) {
+                if (["href", "target"].indexOf(name) > -1) {
+                    if (value) {
+                        this.domNode.setAttribute(name, value);
+                    } else {
+                        this.domNode.removeAttribute(name);
+                    }
+                } else {
+                    super.format(name, value);
+                    if (name == 'link'){
+                        if (value){
+                            if (!value.startsWith(oParams.root_url) && (value.startsWith('http://') || value.startsWith('https://'))){
+                                this.domNode.setAttribute("target", "_blank");  
+                            }
+                            else{
+                                this.domNode.removeAttribute('target');
+                            }
+                        }
+                    }
+                }
+            }
+            static create(value) {
+                const node = super.create(value);
+                node.setAttribute("href", this.sanitize(value));
+                if (!value.startsWith(oParams.root_url) && (value.startsWith('http://') || value.startsWith('https://'))){
+                    node.setAttribute("target", "_blank");  
+                }
+                else{
+                    node.removeAttribute('target');
+                }
+                return node;
+            }
+        }
+        Quill.register(Link, true);
+        
+        class MenthionLink extends Embed {
+            static create(value) {
+                let node = super.create(value);
+                if (value.id && value.value){
+                    
+                    node.setAttribute('href', value.id);
+                    node.innerHTML = value.denotationChar + value.value;
+                    node.setAttribute('title', value.value);
+                    node.setAttribute('dchar', value.denotationChar);
+                    node.setAttribute('data-profile-id', value.dataProfileId);
+                }
+                if (value.url && value.title && value.dataProfileId){
+                    node.setAttribute('href', value.url);
+                    node.innerHTML = value.text;
+                    node.setAttribute('title', value.title);
+                    if (value.dchar)
+                        node.setAttribute('dchar', value.dchar);
+                    else
+                        node.setAttribute('dchar', '@');
+                    node.setAttribute('data-profile-id', value.dataProfileId);
+                }
+                return node;
+            }
+            
+            format(name, value) {
+                if (name === 'href' || name === 'title' || name === 'dchar' || name === 'data-profile-id' || name === 'dataProfileId') {
+                    if (value) {
+                        if (name === 'dchar'){
+                            this.domNode.innerHTML = value + this.domNode.getAttribute('title');
+                        }
+                        if (name === 'dataProfileId'){
+                            this.domNode.setAttribute('data-profile-id', value);
+                        }
+                        else{
+                            this.domNode.setAttribute(name, value);
+                        }
+                    } else {
+                        this.domNode.removeAttribute(name, value);
+                    }
+                } 
+                else {
+                    super.format(name, value);
+                }
+            };
+            
+            static value(node) {
+                return { url: node.getAttribute('href') ,title: node.getAttribute('title'), dchar: node.getAttribute('dchar'), dataProfileId: node.getAttribute('data-profile-id'), text: node.innerText }
+            }
+                
+            static formats(node) {
+                let format = {};
+                if (node.hasAttribute('href')) {
+                    format.href = node.getAttribute('href');
+                }
+                if (node.hasAttribute('title')) {
+                    format.title = node.getAttribute('title');
+                }
+                else{
+                     format.title = node.innerText.replace(node.getAttribute('dchar'), '');
+                }
+                if (node.hasAttribute('dchar')) {
+                    format.dchar = node.getAttribute('dchar');
+                }
+                if (node.hasAttribute('data-profile-id')) {
+                    format['data-profile-id'] = node.getAttribute('data-profile-id');
+                }
+                if (node.hasAttribute('dataProfileId')) {
+                    format.dataProfileId = node.getAttribute('dataProfileId');
+                }
+                return format;
+            }  
+            
+        }
+        MenthionLink.blotName = 'menthion-link'; 
+        MenthionLink.className = 'bx-mention-link';
+        MenthionLink.tagName = 'a';                
+        Quill.register({
+            'formats/menthion-link': MenthionLink
+        });
+        
+        class EmbedCode extends Embed {
+            static create(value) {
+                let node = super.create(value);
+                if (value.source && value.inlinecode){
+                    node.setAttribute('source', value.source);
+                    node.innerHTML = value.inlinecode + "&nbsp;";
+                    return node;
+                }
+                return node;
+            };
+           
+            format(name, value) {
+                if (name === 'source') {
+                    if (value) {
+                        this.domNode.setAttribute(name, value);
+                    } else {
+                        this.domNode.removeAttribute(name, value);
+                    }
+                } 
+                else {
+                    super.format(name, value);
+                }
+            };
+            
+            static value(node) {
+                return { source: node.getAttribute('source'), inlinecode: "emded"}
+            }
+                
+            static formats(node) {
+                let format = {};
+                if (node.hasAttribute('source')) {
+                  format.source = node.getAttribute('source');
+                }
+                return format;
+            }    
+        }
+        EmbedCode.className = 'bx-embed-link';
+        EmbedCode.blotName = 'embed-link'; 
+        EmbedCode.tagName = 'div';                
+        Quill.register({
+            'formats/embed-link': EmbedCode
+        });
+        
+        var AlignStyle = Quill.import('attributors/style/align');
+        Quill.register(AlignStyle, true);
+        
+        var DirectionStyle = Quill.import('attributors/style/direction');
+        Quill.register(DirectionStyle, true);
+        
+        const Parchment = Quill.import("parchment");
+        const pixelLevels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
+        const TAB_MULTIPLIER = 30
+
+        class IndentAttributor extends Parchment.Attributor.Style {
+            add(node, value) {
+                return super.add(node, `${+value * TAB_MULTIPLIER}px`)
+            }
+            value(node) {
+                return parseFloat(super.value(node)) / TAB_MULTIPLIER || undefined // Don't return NaN
+            }
+        }
+
+        const IndentStyle = new IndentAttributor("indent", "margin-left", {
+            scope: Parchment.Scope.BLOCK,
+            whitelist: pixelLevels.map(value => `${value * TAB_MULTIPLIER}px`),
+        })
+
+        Quill.register({"formats/indent": IndentStyle}, true);
+        
+        bQuillRegistred = true; 
+        
+        var icons = Quill.import('ui/icons');
+        icons['embed'] = '<svg class="fr-svg" focusable="false" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path class="ql-fill" d="M20.73889,15.45929a3.4768,3.4768,0,0,0-5.45965-.28662L9.5661,12.50861a3.49811,3.49811,0,0,0-.00873-1.01331l5.72174-2.66809a3.55783,3.55783,0,1,0-.84527-1.81262L8.70966,9.6839a3.50851,3.50851,0,1,0,.0111,4.63727l5.7132,2.66412a3.49763,3.49763,0,1,0,6.30493-1.526ZM18.00745,5.01056A1.49993,1.49993,0,1,1,16.39551,6.3894,1.49994,1.49994,0,0,1,18.00745,5.01056ZM5.99237,13.49536a1.49989,1.49989,0,1,1,1.61194-1.37878A1.49982,1.49982,0,0,1,5.99237,13.49536Zm11.78211,5.494a1.49993,1.49993,0,1,1,1.61193-1.37885A1.49987,1.49987,0,0,1,17.77448,18.98932Z"></path></svg>';
+        icons['show-html'] = '<svg class="fr-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path class="ql-fill" d="M162.1 257.8c-7.812-7.812-20.47-7.812-28.28 0l-48 48c-7.812 7.812-7.812 20.5 0 28.31l48 48C137.8 386.1 142.9 388 148 388s10.23-1.938 14.14-5.844c7.812-7.812 7.812-20.5 0-28.31L128.3 320l33.86-33.84C169.1 278.3 169.1 265.7 162.1 257.8zM365.3 93.38l-74.63-74.64C278.6 6.742 262.3 0 245.4 0H64C28.65 0 0 28.65 0 64l.0065 384c0 35.34 28.65 64 64 64H320c35.2 0 64-28.8 64-64V138.6C384 121.7 377.3 105.4 365.3 93.38zM336 448c0 8.836-7.164 16-16 16H64.02c-8.838 0-16-7.164-16-16L48 64.13c0-8.836 7.164-16 16-16h160L224 128c0 17.67 14.33 32 32 32h79.1V448zM221.9 257.8c-7.812 7.812-7.812 20.5 0 28.31L255.7 320l-33.86 33.84c-7.812 7.812-7.812 20.5 0 28.31C225.8 386.1 230.9 388 236 388s10.23-1.938 14.14-5.844l48-48c7.812-7.812 7.812-20.5 0-28.31l-48-48C242.3 250 229.7 250 221.9 257.8z"/></svg>';
+    }
+    
+    var oConfig = {              
+         theme: oParams.skin,
+         modules: {
+            clipboard: {
+                matchVisual: false
+            },
+            syntax: true, 
+            imageResize: {},
+            toolbar: oParams.toolbar,
+            "emoji-toolbar": true,
+            mention: {
+                allowedChars: /^[\S]*$/,
+                mentionDenotationChars: ["@", "#"],
+                showDenotationChar: true,
+                blotName: 'menthion-link',
+                mentionContainerClass: 'bx-popup bx-popup-trans bx-popup-border bx-popup-color-bg',
+                mentionListClass: 'ql-mention-list bx-menu-ver',
+                dataAttributes: ['id', 'value', 'denotationChar', 'link', 'target','disabled', 'dataProfileId'], 
+                listItemClass: 'bx-menu-item bx-def-color-bg-hl-hover',
+                source: function (searchTerm, renderList, mentionChar) {
+                    var aFlds = oParams.query_params['fi'].split(",");
+                    aFlds.forEach(fld => {
+                        oParams.query_params[fld] = $('#' + oParams.query_params['f'] + ' [name="' + fld + '"]').val()
+                    });
+                        
+                    $.getJSON(oParams.root_url + 'searchExtended.php?action=get_mention', $.extend({}, {symbol: mentionChar, term: searchTerm}, oParams.query_params), function(data){
+                        renderList(data, searchTerm);
+                    });
+                },
+                renderItem: function(item, searchTerm){
+                    item.id = item.url;
+                    item.dataProfileId = item.value;
+                    item.value = item.label;
+                    return item.symbol + item.value;
+                },
+                onSelect: function(item, insertItem){
+                    insertItem(item, false)
+                }
+            },
+        }
+    };
+    
+    if (oParams.allowed_tags){
+        oConfig.formats = oParams.allowed_tags;
+    }
+    
+    oConfig.modules.imageUploader = {
+        upload: file => {
+            return new Promise((resolve, reject) => {
+                var bProcessed = false;
+                
+                if (typeof glOnInsertImageInEditor !== 'undefined' && glOnInsertImageInEditor instanceof Array && glOnInsertImageInEditor.length > 0) {
+                    for (var i = 0; i < glOnInsertImageInEditor.length; i++) {
+                        if (typeof glOnInsertImageInEditor[i] === "function") {
+                            var bTmp = glOnInsertImageInEditor[i](file,oParams.selector);
+                            if (bTmp)
+                                bProcessed = true;
+                        }
+                    }
+                    if(bProcessed)
+                        reject("External uploader");
+                }
+                
+                if(!bProcessed){
+                    const formData = new FormData();
+                   
+                    formData.append("file", file);
+                    fetch(oParams.root_url + "storage.php?o=sys_images_editor&t=sys_images_editor&a=upload", {
+                            method: "POST",
+                            body: formData
+                        }
+                    )
+                    .then(response => response.json())
+                    .then(result => {
+                        if ('undefined' != typeof(result.link))
+                            resolve(result.link);
+                        else
+                            reject("Upload failed");
+                    })
+                    .catch(error => {
+                        reject("Upload failed");
+                    });
+                }
+            });
+        }
+    }
+
+    oEditor = new Quill('#' + oParams.name, oConfig)
+    $(oParams.selector).next().find('button.ql-bold').attr('title', _t('_sys_txt_quill_tooltip_bold'));
+    $(oParams.selector).next().find('button.ql-italic').attr('title', _t('_sys_txt_quill_tooltip_italic'));
+    $(oParams.selector).next().find('button.ql-underline').attr('title', _t('_sys_txt_quill_tooltip_underline'));
+    $(oParams.selector).next().find('button.ql-clean').attr('title', _t('_sys_txt_quill_tooltip_clean'));
+    $(oParams.selector).next().find('button.ql-list[value = "ordered"]').attr('title', _t('_sys_txt_quill_tooltip_list_ordered'));
+    $(oParams.selector).next().find('button.ql-list[value = "bullet"]').attr('title', _t('_sys_txt_quill_tooltip_list_bullet'));
+    $(oParams.selector).next().find('button.ql-indent[value = "-1"]').attr('title', _t('_sys_txt_quill_tooltip_indent_1'));
+    $(oParams.selector).next().find('button.ql-indent[value = "+1"]').attr('title', _t('_sys_txt_quill_tooltip_indent_2'));
+    $(oParams.selector).next().find('button.ql-blockquote').attr('title', _t('_sys_txt_quill_tooltip_blockquote'));
+    $(oParams.selector).next().find('button.ql-direction').attr('title', _t('_sys_txt_quill_tooltip_direction'));
+    $(oParams.selector).next().find('button.ql-script[value="sub"]').attr('title', _t('_sys_txt_quill_tooltip_script_sub'));
+    $(oParams.selector).next().find('button.ql-script[value="super"]').attr('title', _t('_sys_txt_quill_tooltip_script_super'));
+    $(oParams.selector).next().find('button.ql-link').attr('title', _t('_sys_txt_quill_tooltip_link'));
+    $(oParams.selector).next().find('button.ql-image').attr('title', _t('_sys_txt_quill_tooltip_image'));
+    $(oParams.selector).next().find('button.ql-code-block').attr('title', _t('_sys_txt_quill_tooltip_code_block'));
+    $(oParams.selector).next().find('span.ql-color').attr('title', _t('_sys_txt_quill_tooltip_color'));
+    $(oParams.selector).next().find('span.ql-background').attr('title', _t('_sys_txt_quill_tooltip_background'));
+    $(oParams.selector).next().find('span.ql-align').attr('title', _t('_sys_txt_quill_tooltip_align'));
+    $(oParams.selector).next().find('span.ql-header').attr('title', _t('_sys_txt_quill_tooltip_header'));
+	
+	$('#' + oParams.name).find('.ql-editor').addClass('bx-def-vanilla-html');
+    
+    if (bEmptyToolbar)
+        $(oParams.selector).next().hide();
+
+    oEditor.clipboard.addMatcher (Node.TEXT_NODE, function (node, delta) {
+        const Delta = Quill.import('delta')
+        bx_editor_on_space_enter(node.data, oParams.selector, false);
+        return new Delta().insert(node.data); 
+    });
+    
+    if (oParams.insert_as_plain_text){
+        oEditor.clipboard.addMatcher (Node.ELEMENT_NODE, function (node, delta) {
+			let ops = []
+			delta.ops.forEach(op => {
+				if (op.insert && typeof op.insert === 'string') {
+					ops.push({
+						insert: op.insert
+					})
+				}
+			})
+			delta.ops = ops
+			return delta
+        });
+    }
+    else{
+        oEditor.clipboard.addMatcher (Node.ELEMENT_NODE, function (node, delta) {
+			delta.forEach(e => {
+                if(e.attributes){
+                    e.attributes.color = '';
+                    e.attributes.background = '';
+                }
+            });
+            return delta;
+        });
+    }
+
+    
+    oEditor.on('editor-change', function(delta, oldDelta, source) {
+        sVal = oEditor.container.firstChild.innerHTML;
+        if(!oParams.empty_tags){
+            sVal = sVal.replaceAll('<p><br></p>','');
+        }
+        if ((oEditor.getContents()['ops'] || []).length == 1 && oEditor.getText().trim().length === 0) { 
+            sVal = '' 
+        }
+        $(oParams.selector).val(sVal); 
+    });
+    
+    return oEditor
+}
+    
+function bx_editor_activate(oEditor)
+{
+    oEditor.focus();
+}
+
+function bx_editor_insert_html (sEditorId, sImgId, sHtml) 
+{
+    if ($('#' + sEditorId) &&  $('#' + sEditorId).attr('object_editor')){
+        eval ('oEditor = ' + $('#' + sEditorId).attr('object_editor'));
+        if (oEditor.getSelection())
+            oEditor.clipboard.dangerouslyPasteHTML(oEditor.getSelection().index, sHtml, 'api');
+        else
+            oEditor.clipboard.dangerouslyPasteHTML(0, sHtml, 'api');
+    }
+}
+
+function bx_editor_insert_img (sEditorId, sImgId, sImgUrl, sClasses) 
+{
+    console.log(888);
+
+    if ('undefined' == typeof(sClasses))
+        sClasses = '';
+    
+    bx_editor_insert_html(sEditorId, sImgId, '<img id="' + sImgId + '" class="' + sClasses + '" src="' + sImgUrl + '" />')
+}
+
+function bx_editor_on_space_enter (sCode, sEditorId, bSpace = true)
+{
+    if (typeof glBxEditorOnSpaceEnterTimer !== 'undefined')
+        clearTimeout(glBxEditorOnSpaceEnterTimer);
+    
+    if (bSpace)
+        glBxEditorOnSpaceEnterTimer = setTimeout(bx_editor_on_space_enter_in, 500, sCode, sEditorId);
+    else
+        bx_editor_on_space_enter_in(sCode, sEditorId);
+}
+
+function bx_editor_on_space_enter_in(sCode, sEditorId) {
+    glBxEditorOnSpaceEnterTimer = undefined;
+    if (typeof glOnSpaceEnterInEditor !== 'undefined' && glOnSpaceEnterInEditor instanceof Array) {
+        for (var i = 0; i < glOnSpaceEnterInEditor.length; i++) {
+            if (typeof glOnSpaceEnterInEditor[i] === "function") {
+                glOnSpaceEnterInEditor[i](sCode, sEditorId);
+            }
+        }
+    }
+}
+
+function bx_editor_remove_img (aEditorIds, aMarkers) 
+{
+    for (var i = 0; i < aEditorIds.length; i++) {
+        var eEditor = $('#' + $('#' + aEditorIds[i]).attr('object_editor'));
+        // delete images in html editor
+        for (var k = 0; k < aMarkers.length; k++) {
+            var jFiles = eEditor.find(aMarkers[k]);
+            jFiles.each(function () {
+                $(this).remove(); 
+            });
+        }
+    }
+}
+
+/** @} */

+ 372 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/profiles.inc.php

@@ -0,0 +1,372 @@
+<?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
+ * @{
+ */
+
+/**
+ * @return corrently logged in profile id
+ */
+function bx_get_logged_profile_id ()
+{
+    $o = BxDolProfile::getInstance();
+    return $o ? $o->id() : false;
+}
+
+/**
+ * @return true if user is logged in
+ */
+function isLogged()
+{
+    return getLoggedId() != 0;
+}
+
+/**
+ * @return logged in account id
+ */
+function getLoggedId()
+{
+    if (getParam('sys_session_auth')) {
+        return (!empty($GLOBALS['logged']['member']) || !empty($GLOBALS['logged']['admin'])) ? BxDolSession::getInstance()->getUserId() : 0;
+    }
+    else {
+        return isset($_COOKIE['memberID']) && (!empty($GLOBALS['logged']['member']) || !empty($GLOBALS['logged']['admin'])) ? (int)$_COOKIE['memberID'] : 0;
+    }
+}
+
+/**
+ * @return logged in account password
+ */
+function getLoggedPassword()
+{
+    $oAccount = BxDolAccount::getInstance();
+    return ($GLOBALS['logged']['member'] || $GLOBALS['logged']['admin']) && $oAccount ? BxDolAccountQuery::getInstance()->getPassword($oAccount->id()) : "";
+}
+
+/**
+ * It checks if account role is member.
+ */
+function isMember($iId = 0)
+{
+    return isRole(BX_DOL_ROLE_MEMBER, $iId);
+}
+
+if (!function_exists("isAdmin")) {
+    /**
+     * @return true if account is admin
+     */
+    function isAdmin($iId = 0)
+    {
+        if (!$iId && isset($GLOBALS['logged']['admin']) && $GLOBALS['logged']['admin']) // easier check for currently logged in user
+            return true;
+        return isRole(BX_DOL_ROLE_ADMIN, $iId);
+    }
+}
+
+/**
+ * Check user role
+ * @param $iRole role to check user for
+ * @param $iId optional account id, if it isn't specified then curently logged in account is used
+ * @return true if user is in the provided role
+ */
+function isRole($iRole, $iId = 0)
+{
+    if (!(int)$iId)
+        $iId = getLoggedId();
+
+    $oAccount = BxDolAccount::getInstance($iId);
+    if (!$oAccount)
+        return false;
+
+    $aAccountInfo = $oAccount->getInfo();
+
+    if (!$aAccountInfo)
+        return false;
+
+    if (!((int)$aAccountInfo['role'] & $iRole))
+        return false;
+
+    return true;
+}
+
+/**
+ * Login user by setting necessary cookies
+ * @param $iId account id
+ * @param $bRememberMe remember session
+ * @return false if id isn't correct or array of user info if user was logged in
+ */
+function bx_login($iId, $bRememberMe = false)
+{
+    $oAccountQuery = BxDolAccountQuery::getInstance();
+
+    $sPassword = $oAccountQuery->getPassword($iId);
+    if (!$sPassword)
+        return false;
+
+    if (!getParam('sys_session_auth')) {
+        $iCookieTime = $bRememberMe ? time() + 60 * getParam('sys_session_lifetime_in_min') : 0;
+        bx_setcookie("memberID", $iId, $iCookieTime, 'auto');
+        $_COOKIE['memberID'] = $iId;
+        bx_setcookie("memberPassword", $sPassword, $iCookieTime, 'auto', '', 'auto', true /* http only */);
+        $_COOKIE['memberPassword'] = $sPassword;
+    }
+
+    BxDolSession::getInstance()->setUserId($iId);
+    if (getParam('sys_session_auth'))
+        BxDolSession::getInstance()->setValue('remember_me', $bRememberMe);
+
+    $oAccountQuery->updateLoggedIn($iId);
+
+    bx_alert('account', 'login',  $iId);
+    
+    bx_audit(
+        $iId, 
+        'bx_accounts', 
+        '_sys_audit_action_account_login',  
+        array('content_title' => '', 'data' => ['display_info' => ['User agent' => (isset($_SERVER["HTTP_USER_AGENT"]) ? $_SERVER["HTTP_USER_AGENT"] : '')]])
+    );
+    
+    return $oAccountQuery->getInfoById($iId);
+}
+
+/**
+ * Logout user by removing cookies
+ */
+function bx_logout($bNotify = true)
+{
+    if (!($iMemberId = BxDolSession::getInstance()->getUserId()))
+        return;
+
+    if ($bNotify && isMember())
+        bx_alert('account', 'logout', $iMemberId);
+
+    bx_audit(
+        $iMemberId, 
+        'bx_accounts', 
+        '_sys_audit_action_account_logout',  
+        array('content_title' => '', 'data' => ['display_info' => ['User agent' => (isset($_SERVER["HTTP_USER_AGENT"]) ? $_SERVER["HTTP_USER_AGENT"] : '')]])
+    );
+
+    if (!getParam('sys_session_auth')) {
+        bx_setcookie('memberID', '', time() - 96 * 3600);
+        bx_setcookie('memberPassword', '', time() - 96 * 3600, 'auto', '', 'auto', true);
+
+        unset($_COOKIE['memberID']);
+        unset($_COOKIE['memberPassword']);
+    }
+
+    BxDolSession::getInstance()->destroy();
+}
+
+/**
+ * Check if user is logged in (necessary cookies are present) and set some global variables
+ */
+function check_logged()
+{
+    $aAccTypes = array(
+       BX_DOL_ROLE_ADMIN => 'admin',
+       BX_DOL_ROLE_MEMBER => 'member'
+    );
+
+    $bLogged = false;
+    $bID = false;
+    $sID = 0;
+
+    if (getParam('sys_session_auth')) {
+        $o = BxDolSession::getInstance();
+        $sID = $o->getUserId();
+        if ($sID && $oAccount = BxDolAccount::getInstance((int)$sID)) {
+            $aAccountInfo = $oAccount->getInfo();
+            foreach ($aAccTypes as $iRole => $sValue) {
+                if ($GLOBALS['logged'][$sValue] = ((int)$aAccountInfo['role'] & $iRole)) {
+                    $bLogged = true;
+                    $bID = true;
+                    break;
+                }
+            }
+        }
+    }
+    else {
+        $bID = isset($_COOKIE['memberID']);
+        $sID = $bID ? bx_process_input($_COOKIE['memberID']) : false;
+
+        $bPassword = isset($_COOKIE['memberPassword']);
+        $sPassword = $bPassword ? bx_process_input($_COOKIE['memberPassword']) : false;
+        
+        foreach ($aAccTypes as $iRole => $sValue) {
+            if ($GLOBALS['logged'][$sValue] = ($sID && !bx_check_login($sID, $sPassword, $iRole))) {
+                BxDolSession::getInstance();
+                $bLogged = true;
+                break;
+            }
+        }
+    }
+
+    if($bID && $bLogged) {
+        header("Cache-Control: no-cache, no-store, must-revalidate");
+        bx_alert('account', 'logged', getLoggedId());
+    }
+
+    if($bID && !$bLogged)
+        bx_logout(false);
+}
+
+/**
+ * Check unencrypted password
+ * @return empty string on success or error string on error
+ */
+function bx_check_password($sLogin, $sPassword, $iRole = BX_DOL_ROLE_MEMBER)
+{
+    $oAccount = BxDolAccount::getInstance($sLogin);
+    if (!$oAccount) {
+        bx_import('BxDolLanguages');
+        return _t("_sys_txt_login_error");
+    }
+    
+    if ($oAccount->isLocked()){
+        bx_import('BxDolLanguages');
+		return _t("_sys_txt_login_locked", bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=forgot-password')));
+	}
+
+    $aAccountInfo = $oAccount->getInfo();
+
+    $sPassCheck = encryptUserPwd($sPassword, $aAccountInfo['salt']);
+
+	// regenerate password using another encrypt function if necessary
+	bx_alert('system', 'encrypt_password_after', 0, false, array(
+            'info' => $aAccountInfo,
+			'pwd' => $sPassword,
+            'password' => &$sPassCheck,
+        ));
+    
+    if ($sErrorMsg = bx_check_login($aAccountInfo['id'], $sPassCheck, $iRole)){
+        bx_audit(
+            $aAccountInfo['id'], 
+            'bx_accounts', 
+            '_sys_audit_action_account_failed_login_attempt',  
+            array('content_title' => '', 'data' => ['display_info' => ['User agent' => (isset($_SERVER["HTTP_USER_AGENT"]) ? $_SERVER["HTTP_USER_AGENT"] : '')]]),
+            $aAccountInfo['profile_id']
+        );
+
+        $iMaxLoginAttempts = getParam('sys_account_limit_incorrect_login_attempts');
+        if ($iMaxLoginAttempts >0){
+            $oAccountQuery = BxDolAccountQuery::getInstance();
+            if ($aAccountInfo['login_attempts'] >= $iMaxLoginAttempts){
+                $oAccountQuery->lockAccount($aAccountInfo['id'], 1);
+                bx_import('BxDolLanguages');
+                return _t("_sys_txt_login_locked", bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=forgot-password')));
+            }
+            else{
+                $oAccountQuery->updateAttemptsCounter($aAccountInfo['id']);
+            }
+        }
+        return $sErrorMsg;
+    }
+
+    // Admin can always login even if he is blocked/banned/suspended/etc
+    if (isAdmin($aAccountInfo['id']))
+        return '';
+
+    $sErrorMsg = '';
+    bx_alert('account', 'check_login',  $aAccountInfo['id'], false, array('error_msg' => &$sErrorMsg));
+    
+    return $sErrorMsg;
+}
+
+/**
+ * Check encrypted password (ex., from Cookie)
+ * @return empty string on success or error string on error
+ */
+function bx_check_login($iID, $sPassword, $iRole = BX_DOL_ROLE_MEMBER)
+{
+    $oAccount = BxDolAccount::getInstance((int)$iID);
+
+    // If no such account available
+    if (!$oAccount) {
+        bx_import('BxDolLanguages');
+        return _t("_sys_txt_login_error");
+    }
+
+    $aAccountInfo = $oAccount->getInfo();
+
+    // If password is incorrect
+    if (strcmp($aAccountInfo['password'], $sPassword) !== 0) {
+        bx_import('BxDolLanguages');
+        return _t("_sys_txt_login_error");
+    }
+
+    // If wrong account role
+    if (!((int)$aAccountInfo['role'] & $iRole)) {
+        bx_import('BxDolLanguages');
+        return _t("_sys_txt_login_invalid_role");
+    }
+
+    return '';
+}
+
+/**
+ * Declare that content is require user authoriztion and display login form if user isn't logged in
+ * @param $bStudio require webmaster authorization
+ */
+function bx_require_authentication ($bStudio = false, $bAjaxMode = false, $sForceRelocate = '')
+{
+    $iRole = BX_DOL_ROLE_MEMBER;
+    if ($bStudio)
+        $iRole = BX_DOL_ROLE_ADMIN;
+
+    $sID = 0;
+    $sPassword = '';
+
+    if (getParam('sys_session_auth')) {
+        $o = BxDolSession::getInstance();
+        $sID = $o->getUserId();
+        $sPassword = $sID ? BxDolAccountQuery::getInstance()->getPassword($sID) : '';
+    } 
+    else {
+        $sID = isset($_COOKIE['memberID']) ? bx_process_input($_COOKIE['memberID']) : false;
+        $sPassword = isset($_COOKIE['memberPassword']) ? bx_process_input($_COOKIE['memberPassword']) : false;
+    }
+
+    if (bx_check_login($sID, $sPassword, $iRole)) {
+        bx_login_form($bStudio, $bAjaxMode, $sForceRelocate);
+    }
+
+    check_logged();
+}
+
+
+
+/**
+ * Display login form and exit
+ * @param $bStudio display login form for studio
+ * @param $bAjaxMode login form displayed via AJAX
+ * @param sForceRelocate forece relocate
+ */
+function bx_login_form($bStudio = false, $bAjaxMode = false, $sForceRelocate = '')
+{
+    if ($bStudio == 1) {
+        BxTemplStudioFunctions::getInstance()->getLoginForm();
+        exit;
+    }
+
+    $sFormCode = BxDolService::call('system', 'login_form', array('', $sForceRelocate), 'TemplServiceLogin');
+
+    if ($bAjaxMode) {
+        echo $GLOBALS['oFunctions']->transBox('', $sFormCode, false, true);
+        exit;
+    }
+
+    BxDolTemplate::getInstance()->setPageNameIndex (BX_PAGE_DEFAULT);
+    BxDolTemplate::getInstance()->setPageHeader (getParam('site_title') . ' ' . _t("_Member Login"));
+    BxDolTemplate::getInstance()->setPageContent ('page_main_code', DesignBoxContent(_t("_Member Login"), $sFormCode, BX_DB_PADDING_DEF));
+    BxDolTemplate::getInstance()->getPageCode();
+
+    exit;
+}
+
+check_logged();
+
+/** @} */

+ 2433 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/inc/utils.inc.php

@@ -0,0 +1,2433 @@
+<?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:
+        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 https://github.com/unaio/una/issues/4203
+        $s = preg_replace("/(((<\w+>)+[ \n(<br>)]*(<\/\w+>)+)+)|<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;
+
+    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 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: text/html; 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_HEADER, NULL === $sHttpCode ? false : true);
+        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_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'] = strip_tags($aMatch[1]);
+        else
+            $aResult['title'] = parse_url($sSourceUrl, PHP_URL_HOST);
+
+        $aResult['description'] = bx_parse_html_tag($sContent, 'meta', 'name', 'description', 'content', $sCharset);
+        $aResult['keywords'] = bx_parse_html_tag($sContent, 'meta', 'name', 'keywords', 'content', $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);
+}
+
+/**
+ * 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_relative_url($sUrl, $sPrefix = BX_DOL_URL_ROOT)
+{
+    return '/' . bx_ltrim_str($sUrl, $sPrefix);
+}
+
+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 (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_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 [
+            'src' => $sUrl,
+            'width' => $iWidth,
+            'height' => $iHeight,
+        ];
+    }    
+    return false;
+}
+
+/** @} */

+ 12 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/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', '13.0.0-RC4'); ///< system files version, for DB version @see bx_get_ver, DB and files version must match
+
+/** @} */

+ 36 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/logout.php

@@ -0,0 +1,36 @@
+<?php
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+ob_start();
+require_once('./inc/header.inc.php');
+require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+ob_end_clean();
+
+bx_import('BxDolLanguages');
+
+if (getParam('sys_session_auth')) {    
+    if (isset($_COOKIE['memberSession']) && BxDolSession::getInstance()->getUserId())
+        bx_logout();
+}
+else {
+    if (isset($_COOKIE['memberID']) && isset($_COOKIE['memberPassword']))
+        bx_logout();
+}
+
+$oTemplate = BxDolTemplate::getInstance();
+$oTemplate->setPageNameIndex (BX_PAGE_TRANSITION);
+$oTemplate->setPageHeader (_t('_Please Wait'));
+$oTemplate->setPageContent ('page_main_code', MsgBox(_t('_Please Wait')));
+$oTemplate->setPageContent ('url_relocate', BX_DOL_URL_ROOT);
+
+send_headers_page_changed();
+
+$oTemplate->getPageCode();
+
+/** @} */

+ 73 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/manifest.json.php

@@ -0,0 +1,73 @@
+<?php
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+define('BX_MANIFEST', true);
+
+require_once('./inc/header.inc.php');
+
+$aContent = [
+    'name' => '',
+    'short_name' => '',
+    'start_url' => parse_url(BX_DOL_URL_ROOT, PHP_URL_PATH),
+    'display' => 'standalone',
+];
+
+$sPattern = '/^[A-Za-z0-9\.\-]+$/';
+
+if(!empty($_GET['bx_name']) && preg_match($sPattern, $_GET['bx_name']))
+    $aContent['name'] = $_GET['bx_name'];
+
+if(!empty($_GET['bx_short_name']) && preg_match($sPattern, $_GET['bx_short_name']))
+    $aContent['short_name'] = $_GET['bx_short_name'];
+else
+    $aContent['short_name'] = $aContent['name'];
+
+if(isLogged())
+    $aContent['gcm_sender_id'] = '482941778795';
+
+/*
+ * Android device icons
+ */
+
+$aAdi = [];
+if(($iId = (int)getParam('sys_site_icon_android')) != 0) {
+    $oTranscoder = BxDolTranscoderImage::getObjectInstance(BX_DOL_TRANSCODER_OBJ_ICON_ANDROID);
+
+    $sSizes = '192x192';
+    if(($aTranscoderParams = $oTranscoder->getFilterParams('Resize')) !== false)
+        $sSizes = $aTranscoderParams['w'] . 'x' . $aTranscoderParams['h'];
+
+    $aAdi[] = [
+        'src' => $oTranscoder->getFileUrl($iId), 
+        'type' => $oTranscoder->getFileMimeType($iId), 
+        'sizes' => $sSizes
+    ];
+}
+
+if(($iId = (int)getParam('sys_site_icon_android_splash')) != 0) {
+    $oTranscoder = BxDolTranscoderImage::getObjectInstance(BX_DOL_TRANSCODER_OBJ_ICON_ANDROID_SPLASH);
+
+    $sSizes = '512x512';
+    if(($aTranscoderParams = $oTranscoder->getFilterParams('Resize')) !== false)
+        $sSizes = $aTranscoderParams['w'] . 'x' . $aTranscoderParams['h'];
+
+    $aAdi[] = [
+        'src' => $oTranscoder->getFileUrl($iId), 
+        'type' => $oTranscoder->getFileMimeType($iId), 
+        'sizes' => $sSizes
+    ];
+}
+
+if(!empty($aAdi) && is_array($aAdi))
+    $aContent['icons'] = $aAdi;
+
+header("Content-Type: application/json");
+header("X-Robots-Tag: none");
+echo json_encode($aContent);
+?>

+ 688 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/general/classes/BxBaseModGeneralFormsEntryHelper.php

@@ -0,0 +1,688 @@
+<?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
+ *
+ * @{
+ */
+
+/**
+ * Entry forms helper functions
+ */
+class BxBaseModGeneralFormsEntryHelper extends BxDolProfileForms
+{
+    protected $_oModule;
+
+    protected $_bIsApi;
+
+    /**
+     * 'Ajax Mode' determines the format of response. If it's TRUE the response 
+     * (a form or an error appeared during form creation) should be returned 
+     * as text during initial loading, while all other responses appeared after 
+     * form submit should be arrays, which are ready to path to echoJson.
+     */
+    protected $_bAjaxMode;
+
+    /**
+     * Use absolute Action URL in generated form object. 
+     * It's needed in Ajax Mode.
+     */
+    protected $_bAbsoluteActionUrl;
+
+    protected $_bDynamicMode;
+
+    public function __construct($oModule)
+    {
+        parent::__construct();
+        $this->_oModule = $oModule;
+
+        $this->_bIsApi = bx_is_api();
+
+        $this->_bDynamicMode = false;
+
+        $this->_bAjaxMode = false;
+        $mixedAjaxMode = bx_get('ajax_mode');
+        if($mixedAjaxMode !== false)
+            $this->setAjaxMode($mixedAjaxMode);
+
+        $this->_bAbsoluteActionUrl = false;
+        $mixedAbsoluteActionUrl = bx_get('absolute_action_url');
+        if($mixedAbsoluteActionUrl !== false)
+            $this->setAbsoluteActionUrl($mixedAbsoluteActionUrl);
+    }
+
+    public function setAjaxMode($bAjaxMode)
+    {
+        $this->_bAjaxMode = (bool)$bAjaxMode;
+        if($this->_bAjaxMode)
+            $this->setDynamicMode(true);
+    }
+
+    public function setAbsoluteActionUrl($bAbsoluteActionUrl)
+    {
+        $this->_bAbsoluteActionUrl = (bool)$bAbsoluteActionUrl;
+    }
+
+    public function setDynamicMode($bDynamicMode)
+    {
+        $this->_bDynamicMode = (bool)$bDynamicMode;
+    }
+
+    public function getObjectStorage()
+    {
+        return BxDolStorage::getObjectInstance($this->_oModule->_oConfig->CNF['OBJECT_STORAGE']);
+    }
+
+    public function getObjectFormAdd ($sDisplay = false)
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(false === $sDisplay)
+            $sDisplay = $CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD'];
+
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $sDisplay, $this->_oModule->_oTemplate);
+        if($this->_bAjaxMode)
+            $oForm->setAjaxMode($this->_bAjaxMode);
+
+        if($this->_bAbsoluteActionUrl)
+            $this->_setAbsoluteActionUrl('add', $oForm);
+
+        return $oForm; 
+    }
+
+    public function getObjectFormEdit ($sDisplay = false)
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(false === $sDisplay)
+            $sDisplay = $CNF['OBJECT_FORM_ENTRY_DISPLAY_EDIT'];
+
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $sDisplay, $this->_oModule->_oTemplate);
+        if($this->_bAjaxMode)
+            $oForm->setAjaxMode($this->_bAjaxMode);
+
+        if($this->_bAbsoluteActionUrl)
+            $this->_setAbsoluteActionUrl('edit', $oForm);
+
+        return $oForm;
+    }
+
+    public function getObjectFormView ($sDisplay = false)
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (false === $sDisplay)
+            $sDisplay = $CNF['OBJECT_FORM_ENTRY_DISPLAY_VIEW'];
+
+        return BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $sDisplay, $this->_oModule->_oTemplate);
+    }
+
+    public function getObjectFormDelete ($sDisplay = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (false === $sDisplay)
+            $sDisplay = $CNF['OBJECT_FORM_ENTRY_DISPLAY_DELETE'];
+
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $sDisplay, $this->_oModule->_oTemplate);
+        if($this->_bAjaxMode)
+            $oForm->setAjaxMode($this->_bAjaxMode);
+
+        if($this->_bAbsoluteActionUrl)
+            $this->_setAbsoluteActionUrl('delete', $oForm);
+
+        return $oForm;
+    }
+
+    public function viewDataEntry ($iContentId)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sMsgCnt = '';
+        // get content data and profile info
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        if (!$aContentInfo)
+            $sMsgCnt = _t('_sys_txt_error_entry_is_not_defined');
+
+        // check access
+        if (empty($sMsgCnt) && CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->checkAllowedView($aContentInfo)))
+            $sMsgCnt = $sMsg;
+        
+        if ($sMsgCnt)
+            return bx_is_api() ? [bx_api_get_msg($sMsgCnt)] : MsgBox($sMsgCnt);
+
+        $oForm = $this->getObjectFormView();
+        if (!$oForm)
+            return '';
+
+        $oForm->initChecker($aContentInfo);
+
+        if(!empty($CNF['FIELD_TEXT']) &&  !$oForm->isInputVisible($CNF['FIELD_TEXT']))
+            $s = '';
+        else
+            $s = $this->_oModule->_oTemplate->entryText($aContentInfo);
+        
+        return bx_is_api() ? [bx_api_get_block('entity_text', $s)] : $s;
+    }
+
+    public function addData ($iProfile, $aValues, $sDisplay = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // check and display form
+        $oForm = $this->getObjectFormAdd($sDisplay);
+        if (!$oForm)
+            return array('code' => 1, '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 = false;
+            if (is_array($oForm->aParams['db']['submit_name'])) {
+                foreach ($oForm->aParams['db']['submit_name'] as $sVal) {
+                    if (isset($oForm->aInputs[$sVal])) {
+                        $sSubmitName = $sVal;
+                        break;
+                    }
+                }
+            } 
+            else {
+                $sSubmitName = $oForm->aParams['db']['submit_name'];
+            }
+            if ($sSubmitName && isset($oForm->aInputs[$sSubmitName]))
+                $aValues[$sSubmitName] = $oForm->aInputs[$sSubmitName]['value'];
+        }
+
+        $oForm->initChecker(array(), $aValues);
+        if (!$oForm->isSubmittedAndValid()) {
+            $aErrors = array();
+            array_walk($oForm->aInputs, function($aInput, $sKey) use (&$aErrors) {
+                if(!empty($aInput['error']))
+                    $aErrors[$sKey] = $aInput['error'];
+            });
+
+            return array('code' => 2, 'message' => '_sys_txt_error_occured', 'errors' => $aErrors);
+        }
+
+        // insert data into database
+        $aValsToAdd = array ();
+        if(isset($CNF['FIELD_AUTHOR']))
+            $aValsToAdd[$CNF['FIELD_AUTHOR']] = $iProfile;
+
+        $iContentId = $oForm->insert($aValsToAdd);
+        if (!$iContentId) {
+            if (!$oForm->isValid())
+                return array('code' => 2, 'message' => '_sys_txt_error_occured');
+            else
+                return array('code' => 3, 'message' => '_sys_txt_error_entry_creation');
+        }
+
+        $sResult = $this->onDataAddAfter(BxDolProfile::getInstance($iProfile)->getAccountId(), $iContentId);
+        if($sResult)
+            return array('code' => 4, 'message' => $sResult);
+
+        list($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+
+        /*
+         * 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->_oModule->processMetasAdd($iContentId);
+
+        /*
+         * Create alert about the completed action.
+         */
+        $this->_oModule->alertAfterAdd($aContentInfo);
+
+        return array('code' => 0, 'message' => '', 'content' => $aContentInfo);
+    }
+
+    public function addDataForm ($sDisplay = false, $sCheckFunction = false)
+    {
+        if (!$sCheckFunction)
+            $sCheckFunction = 'checkAllowedAdd';
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $bAsJson = false;
+
+        // get form object
+        $oForm = $this->getObjectFormAdd($sDisplay);
+        if (!$oForm)
+            return bx_is_api() ? [bx_api_get_msg('_sys_txt_error_occured')] : $this->prepareResponse(MsgBox(_t('_sys_txt_error_occured')), $bAsJson, 'msg');
+
+        $bAsJson = $this->_bAjaxMode && $oForm->isSubmitted();
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->$sCheckFunction())) {
+            $oProfile = BxDolProfile::getInstance();
+            if ($oProfile && ($aProfileInfo = $oProfile->getInfo()) && $aProfileInfo['type'] == 'system' && is_subclass_of($this->_oModule, 'BxBaseModProfileModule') && $this->_oModule->serviceActAsProfile()) // special check for system profile is needed, because of incorrect error message
+                return bx_is_api() ? [bx_api_get_msg('_sys_txt_access_denied')] : $this->prepareResponse(MsgBox(_t('_sys_txt_access_denied')), $bAsJson, 'msg');
+            else
+                return bx_is_api() ? [bx_api_get_msg($sMsg)] : $this->prepareResponse(MsgBox($sMsg), $bAsJson, 'msg');
+        }
+
+        // check and display form
+        $oForm->initChecker();
+        if (!$oForm->isSubmittedAndValid())
+            return bx_is_api() ? [bx_api_get_block('form', $oForm->getCodeAPI(), ['ext' => ['request' => ['url' => '/api.php?r=' . $this->_oModule->getName() . '/entity_create', 'immutable' => true]]])] : $this->prepareResponse($oForm->getCode($this->_bDynamicMode), $bAsJson, 'form', array(
+            	'form_id' => $oForm->getId()
+            ));
+
+        // insert data into database
+        $aValsToAdd = array ();
+        $iContentId = $oForm->insert ($aValsToAdd);
+        if (!$iContentId) {
+            if (!$oForm->isValid())
+                return bx_is_api() ? [bx_api_get_block('form', $oForm->getCodeAPI(), ['ext' => ['request' => ['url' => '/api.php?r=' . $this->_oModule->getName() . '/entity_create', 'immutable' => true]]])] : $this->prepareResponse($oForm->getCode($this->_bDynamicMode), $bAsJson, 'form', array(
+                    'form_id' => $oForm->getId()
+                ));
+            else
+                return bx_is_api() ? [bx_api_get_msg('_sys_txt_error_entry_creation')] : $this->prepareResponse(MsgBox(_t('_sys_txt_error_entry_creation')), $bAsJson, 'msg');
+        }
+
+        $sResult = $this->onDataAddAfter (getLoggedId(), $iContentId);
+        if ($sResult)
+            return $this->prepareResponse($sResult, $bAsJson, 'msg');
+
+        list($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+
+        /*
+         * 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->_oModule->processMetasAdd($iContentId);
+
+        // Perform ACL action
+        $this->_oModule->$sCheckFunction(true);
+
+        // Create alert about the completed action.
+        $this->_oModule->alertAfterAdd($aContentInfo);
+
+        
+
+        // Redirect
+        if (bx_is_api())
+            return $this->redirectAfterAdd($aContentInfo);
+        else
+            $this->redirectAfterAdd($aContentInfo);
+    }
+
+    public function redirectAfterAdd($aContentInfo, $sUrl = '')
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+        if ($sUrl == '')
+            $sUrl = 'page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']];
+        
+        bx_alert($this->_oModule->getName(), 'redirect_after_add', 0, false, array(
+            'ajax_mode' => $this->_bAjaxMode,
+            'content' => $aContentInfo,
+            'override_result' => &$sUrl,
+        ));
+
+        if (bx_is_api())
+            return [bx_api_get_block('redirect', ['uri' => BxDolPermalinks::getInstance()->permalink($sUrl), 'timeout' => 1000])];
+        
+        if($this->_bAjaxMode) {
+            echoJson($this->prepareResponse($sUrl, $this->_bAjaxMode, 'redirect'));
+            exit;
+        }
+        else
+            $this->_redirectAndExit($sUrl);
+    }
+
+    public function editDataForm ($iContentId, $sDisplay = false, $sCheckFunction = false, $bErrorMsg = true)
+    {
+        if (!$sCheckFunction)
+            $sCheckFunction = 'checkAllowedEdit';
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // get content data and profile info
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        if (!$aContentInfo)
+            return $bErrorMsg && ($sMsg = '_sys_txt_error_entry_is_not_defined') ? ($this->_bIsApi ? bx_api_get_msg($sMsg) : MsgBox(_t($sMsg))) : '';
+
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->$sCheckFunction($aContentInfo)))
+            return $bErrorMsg ? ($this->_bIsApi ? bx_api_get_msg($sMsg) : MsgBox($sMsg)) : '';
+
+        // check and display form
+        $oForm = $this->getObjectFormEdit($sDisplay);
+        if (!$oForm)
+            return $bErrorMsg && ($sMsg = '_sys_txt_error_occured') ? ($this->_bIsApi ? bx_api_get_msg($sMsg) : MsgBox(_t($sMsg))) : '';
+
+        $aSpecificValues = array();        
+        if (!empty($CNF['OBJECT_METATAGS'])) {
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+            if ($oMetatags->locationsIsEnabled())
+                $aSpecificValues = $oMetatags->locationGet($iContentId, empty($CNF['FIELD_LOCATION_PREFIX']) ? '' : $CNF['FIELD_LOCATION_PREFIX']);
+        }
+
+        $oForm->initChecker($aContentInfo, $aSpecificValues);
+        if (!$oForm->isSubmittedAndValid())
+            return $this->_bIsApi ? $oForm : $oForm->getCode();
+
+        // update data in the DB
+        $aTrackTextFieldsChanges = null;
+
+        $this->onDataEditBefore ($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $aTrackTextFieldsChanges, $oProfile, $oForm);
+
+        if (!$oForm->update ($aContentInfo[$CNF['FIELD_ID']], array(), $aTrackTextFieldsChanges)) {
+            if (!$oForm->isValid())
+                return $this->_bIsApi ? $oForm : $oForm->getCode();
+            else
+                return ($sMsg = '_sys_txt_error_entry_update') && $this->_bIsApi ? bx_api_get_msg($sMsg) : MsgBox(_t($sMsg));
+        }
+
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        $sResult = $this->onDataEditAfter ($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $aTrackTextFieldsChanges, $oProfile, $oForm);
+        if ($sResult)
+            return $this->_bIsApi ? bx_api_get_msg($sResult) : $sResult;
+
+        /*
+         * 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->_oModule->processMetasEdit($iContentId, $oForm);
+
+        // Perform ACL action
+        $this->_oModule->$sCheckFunction($aContentInfo, true);
+
+        // Create alert about the completed action.
+        $this->_oModule->alertAfterEdit($aContentInfo);
+        
+        // Redirect
+        $this->redirectAfterEdit($aContentInfo);
+    }
+
+    protected function redirectAfterEdit($aContentInfo)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sUrl = 'page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']];
+        bx_alert($this->_oModule->getName(), 'redirect_after_edit', 0, false, array(
+            'content' => $aContentInfo,
+            'override_result' => &$sUrl,
+        ));
+
+        $this->_redirectAndExit($sUrl);
+    }
+
+    public function deleteDataForm ($iContentId, $sDisplay = false, $sCheckFunction = false)
+    {
+        if (!$sCheckFunction)
+            $sCheckFunction = 'checkAllowedDelete';
+        
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // get content data and profile info
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        if (!$aContentInfo)
+            return ($sMsg = '_sys_txt_error_entry_is_not_defined') && $this->_bIsApi ? bx_api_get_msg($sMsg) : MsgBox(_t($sMsg));
+
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->$sCheckFunction($aContentInfo)))
+            return $this->_bIsApi ? bx_api_get_msg($sMsg) : MsgBox($sMsg);
+
+        // check and display form
+        $oForm = $this->getObjectFormDelete($sDisplay);
+        if (!$oForm)
+            return ($sMsg = '_sys_txt_error_occured') && $this->_bIsApi ? bx_api_get_msg($sMsg) : MsgBox(_t($sMsg));
+
+        $oForm->initChecker($aContentInfo);
+        if (!$oForm->isSubmittedAndValid())
+            return $this->_bIsApi ? $oForm : $oForm->getCode();
+
+        if ($sError = $this->deleteData($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $oProfile, $oForm))
+            return $this->_bIsApi ? bx_api_get_msg($sError) : MsgBox($sError);
+
+        // perform action
+        $this->_oModule->$sCheckFunction($aContentInfo, true);
+
+        // redirect
+        $this->redirectAfterDelete($aContentInfo);
+    }
+
+    protected function redirectAfterDelete($aContentInfo)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sUrl = $CNF['URL_HOME'];
+        $aMarkers = array(
+            'account_id' => getLoggedId(),
+            'profile_id' => bx_get_logged_profile_id(),
+        );
+
+        bx_alert($this->_oModule->getName(), 'redirect_after_delete', 0, false, array(
+            'content' => $aContentInfo,
+            'markers' => &$aMarkers,
+            'override_result' => &$sUrl,
+        ));
+
+        $this->_redirectAndExit($sUrl, true, $aMarkers);
+    }
+
+    /**
+     * Delete data entry
+     * @param $iContentId entry id
+     * @param $oForm optional content info array
+     * @param $aContentInfo optional content info array
+     * @param $oProfile optional content author profile
+     * @return error string on error or empty string on success
+     */
+    public function deleteData ($iContentId, $aContentInfo = false, $oProfile = null, $oForm = null)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (!$aContentInfo || !$oProfile)
+            list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+
+        if (!$aContentInfo)
+            return _t('_sys_txt_error_entry_is_not_defined');
+
+        if (!$oForm)
+            $oForm = $this->getObjectFormDelete();
+
+        if (!$oForm->delete ($aContentInfo[$CNF['FIELD_ID']], $aContentInfo))
+            return _t('_sys_txt_error_entry_delete');
+
+        // remove data from nested forms 
+        $aNestedForms = BxDolFormQuery::getNestedFormObjects($oForm->aParams['object']);
+        foreach ($aNestedForms as $aNestedForm) {
+            BxDolFormQuery::deleteDataFromNestedForm($aNestedForm['table'], $iContentId);
+        }
+        
+        if ($sResult = $this->onDataDeleteAfter ($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $oProfile))
+            return $sResult;
+
+        // create an alert
+        bx_alert($this->_oModule->getName(), 'deleted', $aContentInfo[$CNF['FIELD_ID']], false, array(
+            'content' => &$aContentInfo
+        ));
+
+        return '';
+    }
+
+    public function viewDataForm ($iContentId, $sDisplay = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // get content data and profile info
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        if (!$aContentInfo)
+            return $this->_bIsApi ? bx_api_get_msg('_sys_txt_error_entry_is_not_defined') : MsgBox(_t('_sys_txt_error_entry_is_not_defined'));
+
+        // check access
+        if ($sMsg = $this->_processPermissionsCheckForViewDataForm ($aContentInfo, $oProfile))
+            return $this->_bIsApi ? bx_api_get_msg($sMsg) : MsgBox($sMsg);
+
+        // get form
+        $oForm = $this->getObjectFormView($sDisplay);
+        if (!$oForm)
+            return $this->_bIsApi ? bx_api_get_msg('_sys_txt_error_occured') : MsgBox(_t('_sys_txt_error_occured'));
+
+        // process metatags
+        if (!empty($CNF['OBJECT_METATAGS'])) {
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+            if ($oMetatags->keywordsIsEnabled()) {
+                $aFields = $oMetatags->metaFields($aContentInfo, $CNF, $CNF['OBJECT_FORM_ENTRY_DISPLAY_VIEW']);
+                $oForm->setMetatagsKeywordsData($iContentId, $aFields, $oMetatags);
+            }
+        }        
+
+        // display profile
+        $oForm->initChecker($aContentInfo);
+        
+        if ($this->_bIsApi)
+            return [bx_api_get_block('entity_info', $oForm->getCodeAPI())];
+        
+        return $oForm->getCode();
+    }
+
+    protected function _processPermissionsCheckForViewDataForm ($aContentInfo, $oProfile)
+    {
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->checkAllowedView($aContentInfo)))
+            return $sMsg;
+
+        return '';
+    }
+
+    public function onDataDeleteAfter ($iContentId, $aContentInfo, $oProfile)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(($oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW'])) !== false)
+            $oPrivacy->deleteGroupCustomByContentId($iContentId);
+
+        bx_audit(
+            $iContentId, 
+            $this->_oModule->getName(), 
+            '_sys_audit_action_detete_content',  
+            $this->_oModule->_prepareAuditParams($aContentInfo)
+        );
+        
+        return '';
+    }
+
+    public function onDataEditBefore ($iContentId, $aContentInfo, &$aTrackTextFieldsChanges, &$oProfile, &$oForm)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(isset($CNF['FIELD_STATUS']) && isset($aContentInfo[$CNF['FIELD_STATUS']]) && $aContentInfo[$CNF['FIELD_STATUS']] == 'failed')
+            $oForm->addTrackFields($CNF['FIELD_STATUS'], $aContentInfo);
+    }
+
+    public function onDataEditAfter ($iContentId, $aContentInfo, $aTrackTextFieldsChanges, $oProfile, $oForm)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        /*
+         * Load update data.
+         */
+        list($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+
+        if(isset($CNF['FIELD_PHOTO']))
+            $oForm->processFiles ($CNF['FIELD_PHOTO'], $iContentId, false);
+
+        if(isset($CNF['FIELD_STATUS']) && ($aTrackResult = $oForm->isTrackFieldChanged($CNF['FIELD_STATUS'], true)) !== false)
+            if($aTrackResult['old'] == 'failed' && $aTrackResult['new'] == 'active')
+                $this->_oModule->alertAfterAdd($aContentInfo);
+
+        if(isset($CNF['FIELD_ALLOW_VIEW_TO']) && !empty($aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]) && ($oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW'])) !== false)
+            $oPrivacy->reassociateGroupCustomWithContent($oProfile->id(), $iContentId, (int)$aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]);
+        
+        bx_audit(
+            $iContentId, 
+            $this->_oModule->getName(), 
+            '_sys_audit_action_edit_content',  
+            $this->_oModule->_prepareAuditParams($aContentInfo)
+        );
+        
+        return '';
+    }
+
+    public function onDataAddAfter ($iAccountId, $iContentId)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        list($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+
+        if(($oForm = $this->getObjectFormAdd()) !== false) {
+            if(isset($CNF['FIELD_PHOTO']))
+                $oForm->processFiles($CNF['FIELD_PHOTO'], $iContentId, true);
+        }
+
+        if(isset($CNF['FIELD_ALLOW_VIEW_TO']) && !empty($aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]) && ($oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW'])) !== false)
+            $oPrivacy->associateGroupCustomWithContent($oProfile->id(), $iContentId, (int)$aContentInfo[$CNF['FIELD_ALLOW_VIEW_TO']]);
+
+        bx_audit(
+            $iContentId, 
+            $this->_oModule->getName(), 
+            '_sys_audit_action_add_content',  
+            $this->_oModule->_prepareAuditParams($aContentInfo)
+        );
+        
+        return '';
+    }
+
+    protected function prepareCustomRedirectUrl($s, $aContentInfo)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aContentInfo[$CNF['FIELD_ID']], $this->_oModule->getName());
+
+        $aMarkers = array(
+            '{profile_id}',
+            '{content_id}',
+            '{module}',
+        );
+        $aReplacements = array(
+            $oProfile ? $oProfile->id() : 0,
+            $aContentInfo[$CNF['FIELD_ID']],
+            $this->_oModule->getName(),
+        );
+        $s = str_replace($aMarkers, $aReplacements, $s);
+
+        $s = bx_absolute_url(BxDolPermalinks::getInstance()->permalink($s));
+        
+        return $s;
+    }
+    
+    protected function prepareResponse($mixedResponse, $bAsJson = false, $sKey = 'msg', $aAdditional = array())
+    {
+    	if(!$bAsJson)
+            return $mixedResponse;
+
+        $aResponse = array(
+            $sKey => $mixedResponse,
+            '_dt' => 'json'
+        );
+
+        if(!empty($aAdditional) && is_array($aAdditional))
+            $aResponse = array_merge($aResponse, $aAdditional);
+
+        return $aResponse;
+    }
+    
+    protected function _setAbsoluteActionUrl($sType, &$oForm)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sKeyUri = 'URI_' . strtoupper($sType) . '_ENTRY';
+        if(empty($CNF[$sKeyUri]))
+            return;
+
+        $oForm->setAbsoluteActionUrl(bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF[$sKeyUri])));
+    }
+}
+
+/** @} */

+ 263 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/general/classes/BxBaseModGeneralMenuSnippetMeta.php

@@ -0,0 +1,263 @@
+<?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 BxBaseModGeneralMenuSnippetMeta extends BxTemplMenuUnitMeta
+{
+    protected $_bShowZeros;
+
+    protected $_sModule;
+    protected $_oModule;
+
+    protected $_iContentId;
+    protected $_aContentInfo;
+
+    public function __construct($aObject, $oTemplate = false)
+    {
+        $this->_oModule = BxDolModule::getInstance($this->_sModule);
+        if(empty($oTemplate))
+            $oTemplate = $this->_oModule->_oTemplate;
+
+        parent::__construct($aObject, $oTemplate);
+
+        $this->_sStylePrefix = 'bx-base-general-unit-meta';
+        $this->_bShowZeros = false;
+    }
+
+    public function getCode()
+    {
+        if(empty($this->_aContentInfo) || !is_array($this->_aContentInfo))
+            return '';
+
+        return parent::getCode();
+    }
+
+    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));
+    }
+
+    protected function _getMenuItemAPI($aItem, $mixedType = 'text', $aData = [])
+    {
+        $sDisplayType = 'link';
+        $sContentType = 'text';
+        if(is_array($mixedType)) {
+            if(isset($mixedType['display']))
+                $sDisplayType = $mixedType['display'];
+            if(isset($mixedType['content']))
+                $sContentType = $mixedType['content'];
+        }
+        else
+            $sContentType = $mixedType;
+
+        $aItemData = array_merge([
+            'id' => $aItem['id'],
+            'name' => $aItem['name'],
+            'display_type' => $sDisplayType,
+            'content_type' => $sContentType,
+            'title' => $aItem['title'],
+            'link' => !empty($aItem['link']) && $aItem['link'] != 'javascript:void(0)' ? $aItem['link'] : ''
+        ], $aData);
+
+        return $aItemData;
+    }
+
+    protected function _getMenuItemAuthor($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if($this->_bIsApi)
+            return $this->_getMenuItemAPI($aItem, 'profile', [
+                'data' => BxDolProfile::getData($this->_aContentInfo[$CNF['FIELD_AUTHOR']])
+            ]);
+
+        $oProfile = BxDolProfile::getInstanceMagic($this->_aContentInfo[$CNF['FIELD_AUTHOR']]);
+
+        return $this->getUnitMetaItemLink($oProfile->getDisplayName(), array(
+            'href' => $oProfile->getUrl(),
+            'class' => 'bx-base-text-unit-author'
+        ));
+    }
+
+    protected function _getMenuItemDate($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        
+        if($this->_bIsApi)
+            return $this->_getMenuItemAPI($aItem, 'time', [
+                'title' => $this->_aContentInfo[$CNF['FIELD_ADDED']]
+            ]);
+
+        if ($aItem['icon'] == '')
+            return $this->getUnitMetaItemText(bx_time_js($this->_aContentInfo[$CNF['FIELD_ADDED']], BX_FORMAT_DATE));
+        else
+            return $this->getUnitMetaItemExtended(bx_time_js($this->_aContentInfo[$CNF['FIELD_ADDED']], BX_FORMAT_DATE), $aItem['icon'], '');
+    }
+
+    protected function _getMenuItemCategory($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($CNF['OBJECT_CATEGORY']) || empty($CNF['FIELD_CATEGORY']) || empty($this->_aContentInfo[$CNF['FIELD_CATEGORY']]))
+            return false;
+
+        $oCategory = BxDolCategory::getObjectInstance($CNF['OBJECT_CATEGORY']);
+        if(!$oCategory)
+            return false;
+
+        $sTitle = $oCategory->getCategoryTitle($this->_aContentInfo[$CNF['FIELD_CATEGORY']]);
+
+        if($this->_bIsApi)
+            return $this->_getMenuItemAPI($aItem, 'text', [
+                'title' => $sTitle,
+                'link' => $oCategory->getCategoryUrl($this->_aContentInfo[$CNF['FIELD_CATEGORY']])
+            ]);
+        
+        return $this->getUnitMetaItemCustom($oCategory->getCategoryLink($sTitle, $this->_aContentInfo[$CNF['FIELD_CATEGORY']]));
+    }
+
+    protected function _getMenuItemTags($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($CNF['OBJECT_METATAGS']))
+            return false;
+
+        $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+        if(!$oMetatags || !$oMetatags->keywordsIsEnabled())
+            return false;
+
+        if($this->_bIsApi)
+            return false;
+
+        return $this->getUnitMetaItemCustom($oMetatags->getKeywordsList($this->_aContentInfo[$CNF['FIELD_ID']], 3));
+    }
+
+    protected function _getMenuItemViews($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($CNF['FIELD_VIEWS']) || (empty($this->_aContentInfo[$CNF['FIELD_VIEWS']]) && !$this->_bShowZeros))
+            return false;
+
+        $sTitle = _t('_view_n_views', $this->_aContentInfo[$CNF['FIELD_VIEWS']]);
+
+        if($this->_bIsApi)
+            return $this->_getMenuItemAPI($aItem, 'text', [
+                'title' => $sTitle
+            ]);
+
+        return $this->getUnitMetaItemText($sTitle);
+    }
+
+    protected function _getMenuItemVotes($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($CNF['OBJECT_VOTES']) || (empty($this->_aContentInfo[$CNF['FIELD_VOTES']]) && !$this->_bShowZeros))
+            return false;
+
+        $sTitle = _t('_vote_n_votes', $this->_aContentInfo[$CNF['FIELD_VOTES']]);
+
+        if($this->_bIsApi)
+            return $this->_getMenuItemAPI($aItem, 'text', [
+                'title' => $sTitle
+            ]);
+
+        return $this->getUnitMetaItemText($sTitle);
+    }
+
+    protected function _getMenuItemRating($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($CNF['OBJECT_VOTES_STARS']))
+            return false;
+
+        if($this->_bIsApi)
+            return false;
+
+        $oVotes = BxDolVote::getObjectInstance($CNF['OBJECT_VOTES_STARS'], $this->_aContentInfo[$CNF['FIELD_ID']]);
+        if(!$oVotes)
+            return false;
+
+        return $this->getUnitMetaItemCustom($oVotes->getElementInline(array('show_counter' => true)));
+    }
+    
+    protected function _getMenuItemReactions($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($CNF['OBJECT_REACTIONS']))
+            return false;
+
+        if($this->_bIsApi)
+            return false;
+
+        $oVotes = BxDolVote::getObjectInstance($CNF['OBJECT_REACTIONS'], $this->_aContentInfo[$CNF['FIELD_ID']]);
+        if(!$oVotes)
+            return false;
+
+        return $this->getUnitMetaItemCustom($oVotes->getElementInline(array('show_counter' => false)));
+    }
+
+    protected function _getMenuItemComments($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($CNF['OBJECT_COMMENTS']) || empty($CNF['FIELD_COMMENTS']) || (empty($this->_aContentInfo[$CNF['FIELD_COMMENTS']]) && !$this->_bShowZeros))
+            return false;
+
+        $oComments = BxDolCmts::getObjectInstance($CNF['OBJECT_COMMENTS'], $this->_aContentInfo[$CNF['FIELD_ID']]);
+        if(!$oComments || !$oComments->isEnabled())
+            return false;
+        
+        $sTitle = _t('_cmt_txt_n_comments', $oComments->getCommentsCountAll(0, true));
+        $sUrl =  $oComments->getListUrl();
+
+        if($this->_bIsApi)
+            return $this->_getMenuItemAPI($aItem, 'text', [
+                'title' => $sTitle,
+                'link' => $sUrl
+            ]);
+
+        return $this->getUnitMetaItemLink($sTitle, [
+            'href' => $sUrl
+        ]);
+    }
+
+    protected function _getMenuItemDefault($aItem)
+    {
+        $sResult = false;
+
+        $a = array(
+            'res' => &$sResult, 
+            'menu' => $this->_sObject, 
+            'menu_object' => $this, 
+            'item' => $aItem,
+            'module' => $this->_sModule,
+            'content_id' => $this->_iContentId,
+            'content_data' => $this->_aContentInfo,
+        );
+        bx_alert($this->_sModule, 'menu_custom_item', 0, 0, $a);
+        bx_alert('menu', 'menu_custom_item', 0, 0, $a);
+        
+        if (false !== $sResult)
+            return $sResult;
+
+        return parent::_getMenuItemDefault($aItem);
+    }
+}
+
+/** @} */

+ 3667 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/general/classes/BxBaseModGeneralModule.php

@@ -0,0 +1,3667 @@
+<?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 $_iProfileId;
+    protected $_aSearchableNamesExcept;
+    protected $_aFormParams;
+
+    function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+
+        $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()
+        );
+    }
+
+    // ====== ACTIONS METHODS
+
+    public function isEntryActive($aContentInfo)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $iProfileId = bx_get_logged_profile_id();
+        if($aContentInfo[$CNF['FIELD_AUTHOR']] == $iProfileId || $this->_isModerator())
+            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(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 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 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));
+    }
+
+    // ====== 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' => '',
+            // 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 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;
+    }
+
+    /**
+     * @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(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 '';
+
+        $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'])) {
+            BxDolMenu::getObjectInstance($CNF['OBJECT_MENU_SUBMENU'])->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()
+    {
+    	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(is_string($aParams)){
+            $aParams = json_decode($aParams, true);
+            if (isset($aParams['params'])){
+                //TODO: improve
+                $_GET['start'] = $aParams['params']['start'];
+                $_GET['per_page'] = $aParams['params']['per_page'];
+            }
+        }
+        
+        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_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;
+
+        $mixedContextId = isset($aParams['context_id']) ? $aParams['context_id'] : false;
+
+        $bContext = $mixedContextId !== false;
+        if($bContext && ($oContextProfile = BxDolProfile::getInstance(abs($mixedContextId))) !== false)
+            if($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) == (int)bx_get_logged_profile_id();
+
+            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';
+            }
+        }
+
+        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'))
+            $aResult = bx_api_get_block('form', $mixedResult->getCodeAPI(), ['ext' => ['request' => ['url' => '/api.php?r=' . $this->_aModule['name'] . '/entity_edit', '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'))
+            $aResult = bx_api_get_block('form', $mixedResult->getCodeAPI(), ['ext' => ['request' => ['url' => '/api.php?r=' . $this->_aModule['name'] . '/entity_delete', '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 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, '...');
+
+        return array(
+            'entry_sample' => $CNF['T']['txt_sample_single'],
+            'entry_url' => $sEntryUrl,
+            'entry_caption' => $sEntryCaption,
+            '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 array();
+
+        $oComment = BxDolCmts::getObjectInstance($CNF['OBJECT_COMMENTS'], $iContentId);
+        if(!$oComment || !$oComment->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, '...');
+
+        return array(
+            'entry_sample' => $CNF['T']['txt_sample_single'],
+            'entry_url' => $sEntryUrl,
+            'entry_caption' => $sEntryCaption,
+            'entry_author' => $aContentInfo[$CNF['FIELD_AUTHOR']],
+            'subentry_sample' => $CNF['T']['txt_sample_comment_single'],
+            'subentry_url' => bx_absolute_url($oComment->getViewUrl((int)$aEvent['subobject_id'], false), '{bx_url_root}'),
+            '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 array();
+
+    	$iParentId = (int)$aEvent['object_id'];
+        $aParentInfo = $oComment->getQueryObject()->getCommentsBy(array('type' => 'id', 'id' => $iParentId));
+        if(empty($aParentInfo) || !is_array($aParentInfo))
+            return array();
+
+        $iObjectId = (int)$aParentInfo['cmt_object_id'];
+            $oComment->init($iObjectId);
+
+        return array(
+            '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_author' => (int)$aParentInfo['cmt_author_id'],
+            'subentry_sample' => '_cmt_txt_sample_reply_to',
+            'subentry_url' => bx_absolute_url($oComment->getViewUrl((int)$aEvent['subobject_id'], false), '{bx_url_root}'),
+            '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 array();
+
+        $oVote = BxDolVote::getObjectInstance($CNF['OBJECT_VOTES'], $iContentId);
+        if(!$oVote || !$oVote->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, '...');
+
+        return array(
+            'entry_sample' => $CNF['T']['txt_sample_single'],
+            'entry_url' => $sEntryUrl,
+            'entry_caption' => $sEntryCaption,
+            '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 array();
+
+        $oReaction = BxDolVote::getObjectInstance($CNF['OBJECT_REACTIONS'], $iContentId);
+        if(!$oReaction || !$oReaction->isEnabled())
+            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';
+
+        $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, '...');
+
+        return array(
+            'entry_sample' => $CNF['T']['txt_sample_single'],
+            'entry_url' => $sEntryUrl,
+            'entry_caption' => $sEntryCaption,
+            '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, '...');
+
+        return array(
+            'entry_sample' => $CNF['T']['txt_sample_single'],
+            'entry_url' => $sEntryUrl,
+            'entry_caption' => $sEntryCaption,
+            '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);
+        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;
+
+        //--- 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);
+                $oProfile = $aContentInfo ? BxDolProfile::getInstance($aContentInfo[$CNF['FIELD_AUTHOR']]) : 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($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 ($aDataEntry[$this->_oConfig->CNF['FIELD_AUTHOR']] == $this->_iProfileId || -$aDataEntry[$this->_oConfig->CNF['FIELD_AUTHOR']] == $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 (($aDataEntry[$this->_oConfig->CNF['FIELD_AUTHOR']] == $this->_iProfileId || -$aDataEntry[$this->_oConfig->CNF['FIELD_AUTHOR']] == $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;
+
+        $aParams = $this->_alertParams($aContentInfo);
+        bx_alert($this->getName(), 'added', $iContentId, $aContentInfo[$CNF['FIELD_AUTHOR']], $aParams);
+    }
+
+    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(),
+            'unit' => $oProfile->getUnit(),
+            '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));
+    }
+    
+    
+    // ====== 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);
+
+        //--- 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);
+        }
+
+    	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,
+            '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 '';
+	}
+}
+
+/** @} */

+ 228 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/general/classes/BxBaseModGeneralSearchResult.php

@@ -0,0 +1,228 @@
+<?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_SYS_PER_PAGE_BROWSE_SHOWCASE', 32);
+
+class BxBaseModGeneralSearchResult extends BxTemplSearchResult
+{
+    protected $oModule;
+    protected $bShowcaseView = false;
+    protected $aUnitViews = array();
+    protected $sUnitViewDefault = 'gallery';
+
+    function __construct($sMode = '', $aParams = array())
+    {
+        $this->_sMode = $sMode;
+        parent::__construct();
+    }
+
+    function getMain()
+    {
+        return BxDolModule::getInstance($this->aCurrent['module_name']);
+    }
+
+    function getContentInfoObject()
+    {
+        return BxDolContentInfo::getObjectInstance($this->aCurrent['name']);
+    }
+
+    function getRssUnitLink (&$a)
+    {
+        $CNF = &$this->oModule->_oConfig->CNF;
+
+        return bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $a[$CNF['FIELD_ID']]));
+    }
+
+    function getRssPageUrl ()
+    {
+        if (false === parent::getRssPageUrl())
+            return false;
+
+        $oPermalinks = BxDolPermalinks::getInstance();
+        return bx_absolute_url($oPermalinks->permalink($this->aCurrent['rss']['link']));
+    }
+
+    function rss ()
+    {
+        if (!isset($this->aCurrent['rss']))
+            return '';
+
+        $this->aCurrent['paginate']['perPage'] = empty($this->oModule->_oConfig->CNF['PARAM_NUM_RSS']) ? 10 : getParam($this->oModule->_oConfig->CNF['PARAM_NUM_RSS']);
+
+        return parent::rss();
+    }
+
+    protected function addCustomConditions($CNF, $oProfile, $sMode, $aParams)
+    {
+        $this->addConditionsForAuthorStatus($CNF);
+
+        $this->addConditionsForCf($CNF);
+
+        if(!empty($aParams['filter']) && is_array($aParams['filter']))
+            $this->addConditionsForFilter($CNF, $sMode, $aParams);
+    }
+
+    protected function addConditionsForAuthorStatus($CNF)
+    {
+        if (empty($CNF['FIELD_AUTHOR']))
+            return;
+
+        $this->aCurrent['restriction']['statusAuthor'] = [
+            'value' => 'active',
+            'field' => 'status',
+            'operator' => '=',
+            'table' => 'sys_profiles',
+        ];
+
+        $this->aCurrent['join']['statusAuthor'] = [
+            'type' => 'INNER',
+            'table' => 'sys_profiles',
+            'mainField' => $CNF['FIELD_AUTHOR'],
+            'onField' => 'id',
+            'joinFields' => array(),
+        ];
+    }
+
+    protected function addConditionsForCf($CNF)
+    {
+        if(empty($CNF['FIELD_CF']))
+            return;
+
+        $oCf = BxDolContentFilter::getInstance();
+        if(!$oCf->isEnabled()) 
+            return;
+
+        $aConditions = $oCf->getConditions($this->aCurrent['table'], $CNF['FIELD_CF']);
+        if(!empty($aConditions) && is_array($aConditions))
+            $this->aCurrent['restriction'] = array_merge($this->aCurrent['restriction'], $aConditions);
+    }
+    
+    protected function addConditionsForFilter($CNF, $sMode, $aParams)
+    {
+        $aFilter = $aParams['filter'];
+        
+        if(empty($aFilter['field']) || empty($aFilter['value']))
+            return;
+        
+        $aRestriction = [
+            'value' => $aFilter['value'],
+            'field' => $aFilter['field'],
+            'operator' => '=',
+        ];
+
+        if(isset($aFilter['operator']))
+            $aRestriction['operator'] = $aFilter['operator'];
+
+        if(isset($aFilter['table']))
+            switch($aFilter['table']) {
+                case 'table':
+                    $aRestriction['table'] = $this->aCurrent['table'];
+                    break;
+
+                case 'tableSearch':
+                    $aRestriction['table'] = $this->aCurrent['tableSearch'];
+                    break;
+
+                default: 
+                    $aRestriction['table'] = $aFilter['table'];
+            }
+
+        $this->aCurrent['restriction']['filter'] = $aRestriction;
+    }
+
+    /**
+     * Add conditions for private content
+     */
+    protected function addConditionsForPrivateContent($CNF, $oProfile, $aCustomGroup = array()) 
+    {
+        // default is bProcessPrivateContent = 1, 
+        // so private items are shown as empty boxes with "Private" title
+
+        // we can show public content when privacy object is available
+        
+        if(empty($CNF['OBJECT_PRIVACY_VIEW']))
+            return;
+
+        $oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW']);
+        if(!$oPrivacy)
+            return;
+
+        // for posts in some context we need to show all items, not only public content
+        if (!empty($this->aCurrent['restriction']['context']['value']) || !empty($this->aCurrent['restriction']['author']['value'])) {
+            $this->setProcessPrivateContent(true);
+            return;
+        }
+
+        // build condition to show only public content
+        $aCondition = $oPrivacy->getContentPublicAsCondition($oProfile ? $oProfile->id() : 0, $aCustomGroup);
+        if(empty($aCondition) || !is_array($aCondition))
+            return;
+
+        if(isset($aCondition['restriction'])) {
+            $this->aCurrent['restriction'] = array_merge($this->aCurrent['restriction'], $aCondition['restriction']);
+            $this->aPrivateConditionsIndexes['restriction'] = array_keys($aCondition['restriction']);
+        }
+        if(isset($aCondition['join'])) {
+            $this->aCurrent['join'] = array_merge($this->aCurrent['join'], $aCondition['join']);
+            $this->aPrivateConditionsIndexes['join'] = array_keys($aCondition['join']);
+        }
+
+        $this->setProcessPrivateContent(false);
+    }
+
+    function showPagination($bAdmin = false, $bChangePage = true, $bPageReload = true)
+    {
+        if($this->bShowcaseView)
+            return '';
+
+        $sPagination = parent::showPagination ($bAdmin, $bChangePage, $bPageReload);
+        if(empty($sPagination))
+            return '';
+
+        return $sPagination;
+    }
+
+    protected function getItemPerPageInShowCase ()
+    {
+        $iPerPageInShowCase = BX_SYS_PER_PAGE_BROWSE_SHOWCASE;
+        $CNF = &$this->oModule->_oConfig->CNF;
+        if (isset($CNF['PARAM_PER_PAGE_BROWSE_SHOWCASE']))
+            $iPerPageInShowCase = getParam($CNF['PARAM_PER_PAGE_BROWSE_SHOWCASE']);
+        return $iPerPageInShowCase;
+    }
+    
+    function displayResultBlock()
+    {
+        if ($this->bShowcaseView) {
+            $this->addContainerClass(array('bx-base-unit-showcase-wrapper'));
+            $this->aCurrent['paginate']['perPage'] = $this->getItemPerPageInShowCase();
+            $this->oModule->_oTemplate->addCss(array(BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'flickity/|flickity.css'));
+            $this->oModule->_oTemplate->addJs(array('flickity/flickity.pkgd.min.js','modules/base/general/js/|showcase.js'));
+        }
+
+        return parent::displayResultBlock();
+    }
+
+    function decodeDataUrl ($oContentInfo, $r)
+    {
+        return bx_relative_url($oContentInfo->getContentLink($r['id']));
+    }
+
+    function decodeDataSummaryPlain ($oContentInfo, $r)
+    {
+        $CNF = $this->oModule->_oConfig->CNF;
+        $sSummary = $this->oModule->_oTemplate->getText($r);
+        return isset($CNF['PARAM_CHARS_SUMMARY_PLAIN']) && $CNF['PARAM_CHARS_SUMMARY_PLAIN'] ? BxTemplFunctions::getInstance()->getStringWithLimitedLength(strip_tags($sSummary), (int)getParam($CNF['PARAM_CHARS_SUMMARY_PLAIN'])) : '';
+    }
+
+}
+
+/** @} */

+ 783 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/general/classes/BxBaseModGeneralTemplate.php

@@ -0,0 +1,783 @@
+<?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
+ *
+ * @{
+ */
+
+/**
+ * Module representation.
+ */
+class BxBaseModGeneralTemplate extends BxDolModuleTemplate
+{
+    protected $MODULE;
+    
+    public $aMethodsToCallAddJsCss = array('entry', 'unit');
+
+    function __construct(&$oConfig, &$oDb)
+    {
+        parent::__construct($oConfig, $oDb);
+    }
+
+    public function addLocationBase()
+    {
+        parent::addLocationBase();
+
+        $this->addLocation('mod_general', BX_DIRECTORY_PATH_MODULES . 'base' . DIRECTORY_SEPARATOR . 'general' . DIRECTORY_SEPARATOR, BX_DOL_URL_MODULES . 'base/general/');
+    }
+
+    public function getJsCode($sType, $aParams = array(), $mixedWrap = true)
+    {
+        $sMask = "{var} {object} = new {class}({params});";
+        $aMaskMarkers = array();
+        if(is_array($mixedWrap)) {
+            if(!empty($mixedWrap['mask']))
+                $sMask = $mixedWrap['mask'];
+
+            if(!empty($mixedWrap['mask_markers']) && is_array($mixedWrap['mask_markers']))
+                $aMaskMarkers = $mixedWrap['mask_markers'];
+        }
+
+        $sJsClass = $this->_oConfig->getJsClass($sType);
+        if(!empty($aParams['js_class'])) {
+            $sJsClass = $aParams['js_class'];
+            unset($aParams['js_class']);
+        }
+
+        $sJsObject = $this->_oConfig->getJsObject($sType);
+        if(!empty($aParams['js_object'])) {
+            $sJsObject = $aParams['js_object'];
+            unset($aParams['js_object']);
+        }
+
+        $sBaseUri = $this->_oConfig->getBaseUri();
+        $aParams = array_merge([
+            'sActionUri' => $sBaseUri,
+            'sActionUrl' => BX_DOL_URL_ROOT . $sBaseUri,
+            'sObjName' => $sJsObject,
+            'aHtmlIds' => [],
+            'oRequestParams' => []
+        ], $aParams);
+
+        $sContent = false;
+        bx_alert('system', 'get_js_code', 0, 0, [
+            'mask' => &$sMask,
+            'mask_markers' => &$aMaskMarkers,
+            'object' => &$sJsObject,
+            'class' => &$sJsClass,
+            'params' => &$aParams,
+            'override_result' => &$sContent,
+        ]);
+
+        if($sContent === false)
+            $sContent = bx_replace_markers($sMask, array_merge([
+                'var' => 'var',
+                'object' => $sJsObject, 
+                'class' => $sJsClass,
+                'params' => json_encode($aParams)
+            ], $aMaskMarkers));
+
+        return ($mixedWrap === true || (is_array($mixedWrap) && isset($mixedWrap['wrap']) && $mixedWrap['wrap'] === true)) ? $this->_wrapInTagJsCode($sContent) : $sContent;
+    }
+
+    public function getTitleAuto($aData, $iMaxLen = 20, $sEllipsisSign = '...')
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        if(isset($aData[$CNF['FIELD_TITLE']]))
+            return $this->getTitle($aData);
+
+        $sResult = $this->getText($aData);
+        if(strlen($sResult) > 0 && $iMaxLen > 0)
+            $sResult = strmaxtextlen($sResult, $iMaxLen, $sEllipsisSign);
+
+        return $sResult;
+    }
+
+    public function getTitle($aData, $mixedProcessOutput = BX_DATA_TEXT)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+        
+        if(!isset($aData[$CNF['FIELD_TITLE']]))
+            return '';
+
+        $sResult = $aData[$CNF['FIELD_TITLE']];
+        if($mixedProcessOutput !== false && !empty($sResult))
+            $sResult = bx_process_output($sResult, (int)$mixedProcessOutput);
+
+        return $sResult;
+    }
+
+    public function getText($aData, $mixedProcessOutput = BX_DATA_HTML)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+        
+        if(!isset($CNF['FIELD_TEXT']) || !isset($aData[$CNF['FIELD_TEXT']]))
+            return '';
+
+        $sResult = $aData[$CNF['FIELD_TEXT']];
+        if($mixedProcessOutput !== false && !empty($sResult))
+            $sResult = bx_process_output($sResult, (int)$mixedProcessOutput);
+
+        return $sResult;
+    }
+
+    protected function getSummary($aData, $sTitle = '', $sText = '', $sUrl = '')
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+        if(empty($CNF['PARAM_CHARS_SUMMARY']))
+            return '';
+
+        // get summary
+        $sLinkMore = ' <a title="' . bx_html_attribute(_t('_sys_read_more', $sTitle)) . '" href="' . $sUrl . '"><i class="sys-icon ellipsis-h"></i></a>';
+        return strmaxtextlen($sText, (int)getParam($CNF['PARAM_CHARS_SUMMARY']), $sLinkMore);
+    }
+
+    public function getProfileLink($mixedProfile)
+    {
+    	if(!is_array($mixedProfile))
+            $mixedProfile = $this->getModule()->getProfileInfo((int)$mixedProfile);
+
+    	return $this->getLink('link', array(
+            'href' => $mixedProfile['link'],
+            'title' => bx_html_attribute(!empty($mixedProfile['title']) ? $mixedProfile['title'] : $mixedProfile['name']),
+            'content' => $mixedProfile['name']
+    	));
+    }
+
+    public function getLink($sTemplate, $aParams)
+    {
+    	return $this->parseHtmlByName($sTemplate . '.html', array(
+            'href' => $aParams['href'],
+            'title' => $aParams['title'],
+            'content' => $aParams['content']
+        ));
+    }
+    
+    function getContextAddon ($aData, $oProfile)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF; 
+        $sUrl = 'page.php?i=' . $CNF['URI_ENTRIES_BY_CONTEXT'] . '&profile_id=' . $oProfile->id();
+        $bActAsProfile = BxDolService::call($oProfile->getModule(), 'act_as_profile');
+        if ($bActAsProfile)
+            $sUrl = 'page.php?i=' . $CNF['URI_AUTHOR_ENTRIES'] . '&profile_id=' . $oProfile->id();
+        $sUrl = BxDolPermalinks::getInstance()->permalink($sUrl);
+        return _t($CNF['T']['txt_all_entries_in'], $sUrl, $oProfile->getDisplayName(), $this->getModule()->_oDb->getEntriesNumByContext($oProfile->id()));
+    }
+    
+    function getContextDesc ($aData)
+    {
+        return '';
+    }
+
+    public function getTmplVarsText($aData)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        $aVars = $aData;
+        $aVars['entry_title'] = $this->getTitle($aData);
+        $aVars['entry_text'] = $this->getText($aData);
+		$aVars['badges'] = $this->getModule()->serviceGetBadges($aData[$CNF['FIELD_ID']]);
+        if (!empty($CNF['OBJECT_METATAGS'])) {
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+
+            // keywords
+            if ($oMetatags->keywordsIsEnabled()) {
+                $aFields = array_merge($oMetatags->metaFields($aData, $CNF, $CNF['OBJECT_FORM_ENTRY_DISPLAY_VIEW']), array('entry_title', 'entry_text'));
+                foreach ($aFields as $sField)
+                    $aVars[$sField] = $oMetatags->keywordsParse($aData[$CNF['FIELD_ID']], $aVars[$sField]);
+            }
+
+            // mentions
+            if ($oMetatags->mentionsIsEnabled()) {
+                $aFields = array_merge($oMetatags->metaFields($aData, $CNF, $CNF['OBJECT_FORM_ENTRY_DISPLAY_VIEW'], true), array('entry_text'));
+                foreach ($aFields as $sField)
+                    $aVars[$sField] = $oMetatags->mentionsParse($aData[$CNF['FIELD_ID']], $aVars[$sField]);
+            }
+            
+            // location
+            $aVars['location'] = $oMetatags->locationsIsEnabled() ? $oMetatags->locationsString($aData[$CNF['FIELD_ID']]) : '';
+        }
+
+        unset($aVars['recipients']);
+
+        return $aVars;
+    }
+
+    public function entryBreadcrumb($aContentInfo, $aTmplVarsItems = array())
+    {
+    	$CNF = &BxDolModule::getInstance($this->MODULE)->_oConfig->CNF;
+    	return bx_is_api() ? ['id' => 2, 'type' => 'breadcrumb', 'data' => ['name' => BxDolModule::getInstance($this->MODULE)->_aModule['title'], 'url' => BxDolPermalinks::getInstance()->permalink($CNF['URL_HOME'])]] : $this->parseHtmlByName('breadcrumb.html', [
+    		'url_home' => bx_absolute_url(BxDolPermalinks::getInstance()->permalink($CNF['URL_HOME'])),
+    		'icon_home' => $CNF['ICON'],
+    		'bx_repeat:items' => $aTmplVarsItems
+    	]);
+    }
+
+    public function entryText ($aData, $sTemplateName = 'entry-text.html')
+    {
+        $aVars = $this->getTmplVarsText($aData);
+
+        if (empty($aVars['entry_text']))
+            return false;
+        
+        return bx_is_api() ? $aVars : $this->parseHtmlByName($sTemplateName, $aVars);
+    }
+
+    public function entryLocation ($iContentId)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        if (empty($CNF['OBJECT_METATAGS']))
+            return '';
+
+        $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);        
+
+        if (!($sLocationString = $oMetatags->locationsString($iContentId)))
+            return '';
+
+        $aVars = array (
+            'location' => $sLocationString
+        );
+        return $this->parseHtmlByName('entry-location.html', $aVars);
+    }
+
+	public function entryInfo($aData, $aValues = array())
+    {
+    	$CNF = $this->_oConfig->CNF;
+        $aValuesDefault = array();
+
+        if (isset($aData[$CNF['FIELD_ADDED']]))
+            $aValuesDefault[] = array(
+                'title' => _t('_sys_txt_field_created'),
+                'value' => bx_time_js($aData[$CNF['FIELD_ADDED']]),
+            );
+
+        if (isset($aData[$CNF['FIELD_CHANGED']]))
+            $aValuesDefault[] = array(
+                'title' => _t('_sys_txt_field_updated'),
+                'value' => bx_time_js($aData[$CNF['FIELD_CHANGED']]),
+            );
+
+        $aValues = array_merge($aValuesDefault, $aValues);
+
+    	return $this->parseHtmlByName('entry-info.html', array(
+    		'bx_repeat:info' => $aValues,
+    	));
+    }
+    
+    public function getFavoriteList($oProfile, $iStart, $iPerPage, $aParams)
+    {
+        $CNF = $this->_oConfig->CNF;
+        
+        $bEmptyMessage = false;
+        if(isset($aParams['empty_message'])) {
+            $bEmptyMessage = (bool)$aParams['empty_message'];
+            unset($aParams['empty_message']);
+        }
+        
+        $oFavorite = BxDolFavorite::getObjectInstance($CNF['OBJECT_FAVORITES'], 0, true);
+        $aListsData = $oFavorite->getQueryObject()->getList(array('type' => 'active', 'author_id' => $oProfile->id(), 'need_default' => true, 'start' => $iStart, 'limit' => $iPerPage + 1));
+        $iNum = count($aListsData);
+        if ($iNum > $iPerPage)
+            $aListsData = array_slice($aListsData, 0, $iPerPage);
+        
+        $oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_LIST_VIEW']);
+        $aListsTmpl = array();
+        foreach($aListsData as $iListId => $sName) {
+            $aParams['list_id'] = $iListId;
+            if ($oPrivacy->check($iListId) || $iListId == 0){
+                $aTmp = $this->getModule()->_serviceBrowse ('favorite', array_merge(array('user' => $oProfile->id()), $aParams), BX_DB_PADDING_DEF, $bEmptyMessage, false);
+                if ($aTmp && $aTmp['content']){
+                    $aListsTmpl[] = array(
+                        'title' => $sName, 
+                        'content_url' => $this->getModule()->_getFavoriteListUrl($iListId, $oProfile->id()),
+                        'content' => $aTmp['content']
+                    );
+                }
+            }
+            else{
+                $aListsTmpl[] = array(
+                    'title' => 'private list', 
+                    'content_url' => 'javascript:',
+                    'content' => ''
+                );
+            }
+        } 
+        
+        $oPaginate = new BxTemplPaginate(array(
+            'on_change_page' => "return !loadDynamicBlockAutoPaginate(this, '{start}', '{per_page}', " . bx_js_string(json_encode($aParams)) . ", 'list_start', 'list_per_page');",
+            'num' => $iNum,
+            'per_page' => $iPerPage,
+            'start' => $iStart,
+        ));
+        if (count($aListsTmpl) > 0)
+            return $this->parseHtmlByName('favorite-lists.html', array('bx_repeat:items' => $aListsTmpl)) . $oPaginate->getSimplePaginate() . $oFavorite->getJsScript();
+        
+        return false;
+    }
+    
+    public function getFavoritesListInfo($aList, $oProfile)
+    {
+        $CNF = $this->_oConfig->CNF;
+
+        $iListId = !empty($aList['id']) ? (int)$aList['id'] : 0;
+
+        $oFavorite = BxDolFavorite::getObjectInstance($CNF['OBJECT_FAVORITES'], 0, true);
+        $aListInfo = $oFavorite->getQueryObject()->getList(array('type' => 'info', 'list_id' => $iListId, 'author_id' => $oProfile->id()));
+
+        $oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_LIST_VIEW']);
+        $sTitle = "";
+        if ($aListInfo['allow_view_favorite_list_to'] < 0){
+            $oProfileContext = BxDolProfile::getInstance(abs($aListInfo['allow_view_favorite_list_to']));
+            $sTitle = $oProfileContext->getDisplayName();
+        }
+        else{
+            if (empty($aListInfo['allow_view_favorite_list_to']))
+                $aListInfo['allow_view_favorite_list_to'] = 3;
+            $aPrivaciInfo = $oPrivacy->getGroupsBy(array('type'=>'id' , 'id'=> $aListInfo['allow_view_favorite_list_to']));
+            $sTitle = _t($aPrivaciInfo['title']);
+        }
+        
+        $aListsTmpl = array();
+        
+        if (!empty($aListInfo['created'])){
+            $aListsTmpl[] = array('title' => _t('_sys_form_favorite_list_title_created'), 'value' => bx_time_js($aListInfo['created']));
+        }
+        
+        $aListsTmpl = array_merge($aListsTmpl, 
+            array(
+                array('title' => _t('_sys_form_favorite_list_title_updated'), 'value' => bx_time_js($aListInfo['updated'])),
+                array('title' => _t('_sys_form_favorite_list_title_count'), 'value' => $aListInfo['count']),
+                array('title' => _t('_sys_form_favorite_list_title_visibility'), 'value' => $sTitle)
+            )
+        );
+        
+        return $this->parseHtmlByName('favorite-list-info.html', array('bx_repeat:items' => $aListsTmpl, 'author' => $oProfile->getUnit()));
+    }
+    
+
+    function entryAllActions($sActions)
+    {
+        if(empty($sActions))
+            return '';
+
+        return $this->parseHtmlByName('entry-all-actions.html', array (
+            'actions' => $sActions
+        ));
+    }
+
+    function entryAttachments ($aData, $aParams = array())
+    {
+        return $this->entryAttachmentsByStorage($this->getModule()->_oConfig->CNF['OBJECT_STORAGE'], $aData, $aParams);
+    }
+
+    function entryAttachmentsByStorage ($mixedStorage, $aData, $aParams = array())
+    {
+        if(!is_array($mixedStorage))
+            $mixedStorage = array($mixedStorage);
+
+        $aResult = array();
+        
+        if (bx_is_api()){
+            foreach($mixedStorage as $sStorage) {
+                $aAttachments = $this->getAttachments($sStorage, $aData, $aParams);
+                if ($aAttachments){
+                    foreach($aAttachments as $aAttachment) {
+                        $aItem = [
+                            'file_name' => $aAttachment['file_name'],
+                            'mime_type' => $aAttachment['mime_type'],
+                            'ext' => $aAttachment['ext'],
+                            'type' => 'other',
+                            'size' => $aAttachment['size'],
+                            'url' => $aAttachment['bx_if:not_image']['content']['url_original']
+                        ];
+                        if ($aAttachment['bx_if:image']['condition'] == 1){
+                            $aItem['type'] = 'image';
+                            $aItem['data'] = bx_api_get_image($sStorage, $aAttachment['id']);
+                        }
+                        if ($aAttachment['bx_if:video']['condition'] == 1){
+                            $aItem['type'] = 'video';
+                            $aItem['data'] = bx_api_get_image($sStorage, $aAttachment['id']);
+                        }
+                        if ($aAttachment['bx_if:sound']['condition'] == 1){
+                            $aItem['type'] = 'sound';
+                            $aItem['data'] = bx_api_get_image($sStorage, $aAttachment['id']);
+                        }
+                        
+                        $aResult[] = $aItem;
+                    }
+                }
+            }
+            return [bx_api_get_block('entity_attachments', $aResult)];
+        }
+        
+        foreach($mixedStorage as $sStorage) {
+            $aAttachments = $this->getAttachments($sStorage, $aData, $aParams);
+            if(empty($aAttachments) || !is_array($aAttachments))
+                continue;
+
+            $aResult = array_merge($aResult, $aAttachments);
+        }
+
+        
+        
+        if(empty($aResult) || !is_array($aResult))
+            return '';
+
+        $this->addCss(BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'flickity/|flickity.css');
+    	$this->addJs(array(
+            'flickity/flickity.pkgd.min.js',
+            'pdfjs/build/pdf.min.js',
+        ));
+
+    	return $this->parseHtmlByName('attachments.html', array(
+            'bx_repeat:attachments' => $aResult,
+            'pdfjs_worker_url' => $this->getJsUrl('pdfjs/build/pdf.worker.min.js'),
+        ));
+    }
+    
+    function entryContext ($aData, $iProfileId = false, $sFuncContextDesc = 'getContextDesc', $sTemplateName = 'context.html', $sFuncContextAddon = 'getContextAddon')
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        $iContextId = $aData[$CNF['FIELD_ALLOW_VIEW_TO']];
+        if ($iContextId >= 0)
+            return '';
+        
+        $iProfileId = - $iContextId;
+
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if (!$oProfile) 
+            $oProfile = BxDolProfileUndefined::getInstance();
+
+        if (!$oProfile)
+            return '';
+
+        $sName = $oProfile->getDisplayName();
+        $sAddon = $sFuncContextAddon ? $this->$sFuncContextAddon($aData, $oProfile) : '';
+        $aVars = array (
+            'author_url' => $oProfile->getUrl(),
+            'author_thumb_url' => $oProfile->getThumb(),
+            'author_unit' => $oProfile->getUnit(0, array('template' => 'unit_wo_info')),
+            'author_title' => $sName,
+            'author_title_attr' => bx_html_attribute($sName),
+            'author_desc' => $sFuncContextDesc ? $this->$sFuncContextDesc($aData) : '',
+            'bx_if:addon' => array (
+                'condition' => (bool)$sAddon,
+                'content' => array (
+                    'content' => $sAddon,
+                ),
+            ),
+        );
+        return $this->parseHtmlByName($sTemplateName, $aVars);
+    }
+
+    protected function getAttachmentsImagesTranscoders ($sStorage = '')
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        $oTranscoder = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_PREVIEW']);
+        $oTranscoderView = !empty($CNF['OBJECT_IMAGES_TRANSCODER_PICTURE']) ? BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_PICTURE']) : null;
+
+        return array($oTranscoder, $oTranscoderView);
+    }
+
+    protected function getAttachmentsVideoTranscoders ($sStorage = '')
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        if (isset($CNF['OBJECT_VIDEOS_TRANSCODERS']) && $CNF['OBJECT_VIDEOS_TRANSCODERS'])
+            return array (
+                'poster' => BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_VIDEOS_TRANSCODERS']['poster']),
+                'mp4' => BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_VIDEOS_TRANSCODERS']['mp4']),
+                'mp4_hd' => BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_VIDEOS_TRANSCODERS']['mp4_hd']),
+            );
+
+        return false;
+    }
+    
+    protected function getAttachments ($sStorage, $aData, $aParams = array())
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        $oStorage = BxDolStorage::getObjectInstance($sStorage);
+
+        list($oTranscoder, $oTranscoderPreview) = $this->getAttachmentsImagesTranscoders($sStorage);
+        $oTranscoderSound = isset($CNF['OBJECT_SOUNDS_TRANSCODER']) && $CNF['OBJECT_SOUNDS_TRANSCODER'] ? BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_SOUNDS_TRANSCODER']) : null;
+        $aTranscodersVideo = $this->getAttachmentsVideoTranscoders($sStorage);
+
+        $aGhostFiles = $oStorage->getGhosts ($this->getModule()->serviceGetContentOwnerProfileId($aData[$CNF['FIELD_ID']]), $aData[$CNF['FIELD_ID']]);
+        if (!$aGhostFiles)
+            return false;
+
+        $sFilterField = isset($aParams['filter_field']) ? $aParams['filter_field'] : $CNF['FIELD_THUMB'];
+		if(!empty($sFilterField) && isset($aData[$sFilterField]))
+	        foreach ($aGhostFiles as $k => $a) {
+	            // don't show thumbnail in attachments
+	            if ($a['id'] == $aData[$sFilterField])
+	                unset($aGhostFiles[$k]);
+	        }
+
+        if(!$aGhostFiles)
+            return false;
+
+        $aAttachmnts = array();
+        foreach ($aGhostFiles as $k => $a) {
+            $isImage = !empty($CNF['OBJECT_STORAGE_PHOTOS']) && $sStorage == $CNF['OBJECT_STORAGE_PHOTOS'] && $oTranscoder && (0 === strncmp('image/', $a['mime_type'], 6)) && $oTranscoder->isMimeTypeSupported($a['mime_type']); // preview for images, transcoder object for preview must be defined
+            $isVideo = !empty($CNF['OBJECT_STORAGE_VIDEOS']) && $sStorage == $CNF['OBJECT_STORAGE_VIDEOS'] && $aTranscodersVideo && (0 === strncmp('video/', $a['mime_type'], 6)) && $aTranscodersVideo['poster']->isMimeTypeSupported($a['mime_type']); // preview for videos, transcoder object for video must be defined
+            $isSound = !empty($CNF['OBJECT_STORAGE_SOUNDS']) && $sStorage == $CNF['OBJECT_STORAGE_SOUNDS'] && $oTranscoderSound && (0 === strncmp('audio/', $a['mime_type'], 6)) && $oTranscoderSound->isMimeTypeSupported($a['mime_type']); // preview for sounds, transcoder object for sounds must be defined
+            $sUrlOriginal = $oStorage->getFileUrlById($a['id']);
+            $sImgPopupId = 'bx-messages-atachment-popup-' . $a['id'];
+
+            // images are displayed with preview and popup upon clicking
+            $a['bx_if:image'] = array (
+                'condition' => $isImage,
+                'content' => array (
+                    'url_original' => $isImage && $oTranscoderPreview ? $oTranscoderPreview->getFileUrl($a['id']) : $sUrlOriginal,
+                    'attr_file_name' => bx_html_attribute($a['file_name']),
+                    'popup_id' => $sImgPopupId,
+                    'url_preview' => $isImage ? $oTranscoder->getFileUrl($a['id']) : '',
+                    'popup' =>  BxTemplFunctions::getInstance()->transBox($sImgPopupId, '<img src="' . $sUrlOriginal . '" />', true, true),
+                ),
+            );
+
+            $sVideoUrlHd = '';
+            if($isVideo) {
+                $sVideoUrl = $oStorage->getFileUrlById($a['id']);
+                $aVideoFile = $oStorage->getFile($a['id']);
+
+                if (!empty($aVideoFile['dimensions']) && $aTranscodersVideo['mp4_hd']->isProcessHD($aVideoFile['dimensions']))
+                    $sVideoUrlHd = $aTranscodersVideo['mp4_hd']->getFileUrl($a['id']);
+            }
+
+            // videos are displayed inline
+            $a['bx_if:video'] = array (
+                'condition' => $isVideo,
+                'content' => array (
+                    'video' => $isVideo && $aTranscodersVideo ? BxTemplFunctions::getInstance()->videoPlayer(
+                        $aTranscodersVideo['poster']->getFileUrl($a['id']), 
+                        $aTranscodersVideo['mp4']->getFileUrl($a['id']), 
+                        $sVideoUrlHd,
+                        false, ''
+                    ) : '',
+                ),
+            );
+
+            // sounds are displayed inline
+            $a['bx_if:sound'] = array (
+                'condition' => $isSound,
+                'content' => array (
+                    'sound' => $isSound && $oTranscoderSound && ($oPlayer = BxDolPlayer::getObjectInstance()) ? $this->parseHtmlByName('attachment_sound.html', array(
+                        'file_name' => $a['file_name'],
+                        'file_url' => $oTranscoderSound ? $oTranscoderSound->getFileUrl($a['id']) : '',
+                        'player' => $oTranscoderSound && $oTranscoderSound->isFileReady($a['id']) ? 
+                            $oPlayer->getCodeAudio (BX_PLAYER_STANDARD, array(
+                                'mp3' => $oTranscoderSound->getFileUrl($a['id']),
+                            )) : _t('_sys_txt_err_sound_not_transcoded_yet'),
+                    )) : '',
+                ),
+            );
+
+            // non-images are displayed as text links to original file
+            $a['bx_if:not_image'] = array (
+                'condition' => !$isImage && !$isVideo && !$isSound,
+                'content' => array (
+                    'url_original' => $sUrlOriginal,
+                    'attr_file_name' => bx_html_attribute($a['file_name']),
+                    'file_name' => bx_process_output($a['file_name']),
+                ),
+            );
+
+            $aAttachmnts[] = $a;
+        }
+
+        return $aAttachmnts;
+    }
+
+    public function embedVideo($iFileId)
+    {
+        $CNF = $this->getModule()->_oConfig->CNF;
+        list($oPlayer, $oStorage, $aContentInfo, $a) = $this->_embedChecks('OBJECT_STORAGE_VIDEOS', $iFileId);
+
+        // check if file is really video
+        $aTranscodersVideo = $this->getAttachmentsVideoTranscoders();        
+        if (!$aTranscodersVideo || (0 !== strncmp('video/', $a['mime_type'], 6)) || !$aTranscodersVideo['poster']->isMimeTypeSupported($a['mime_type'])) {
+            $this->displayErrorOccured('', BX_PAGE_EMBED);
+            exit;
+        }
+
+        // check HD video version
+        $sVideoUrlHd = '';
+        $sVideoUrl = $oStorage->getFileUrlById($a['id']);
+        $aVideoFile = $oStorage->getFile($a['id']);
+
+        if (!empty($aVideoFile['dimensions']) && $aTranscodersVideo['mp4_hd']->isProcessHD($aVideoFile['dimensions']))
+            $sVideoUrlHd = $aTranscodersVideo['mp4_hd']->getFileUrl($a['id']);
+
+        // generate player code
+        $sCode = $oPlayer->getCodeVideo (BX_PLAYER_EMBED, array(
+            'poster' => $aTranscodersVideo['poster']->getFileUrl($a['id']),
+            'mp4' => array(
+                'sd' => $aTranscodersVideo['mp4']->getFileUrl($a['id']), 
+                'hd' => $sVideoUrlHd
+            ),
+        ));
+
+        // display page
+        $oTemplate = BxDolTemplate::getInstance();
+        $oTemplate->setPageNameIndex (BX_PAGE_EMBED);
+        $oTemplate->setPageHeader ($a['file_name']);
+        $oTemplate->setPageContent ('page_main_code', $sCode);
+        $oTemplate->getPageCode();
+        exit;
+    }
+
+    public function embedSound($iFileId)
+    {
+        $CNF = $this->getModule()->_oConfig->CNF;
+        list($oPlayer, $oStorage, $aContentInfo, $a) = $this->_embedChecks('OBJECT_STORAGE_SOUNDS', $iFileId);
+
+        // check if file is really audio
+        $oTranscoderSound = isset($CNF['OBJECT_SOUNDS_TRANSCODER']) ? BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_SOUNDS_TRANSCODER']) : null;
+        if (!$oTranscoderSound || (0 !== strncmp('audio/', $a['mime_type'], 6)) || !$oTranscoderSound->isMimeTypeSupported($a['mime_type'])) {
+            $this->displayErrorOccured('', BX_PAGE_EMBED);
+            exit;
+        }
+
+        if (!$oTranscoderSound->isFileReady($a['id'])) {
+            $oTranscoderSound->getFileUrl($a['id']); // queue for encoding
+            $this->displayMsg('_sys_txt_err_sound_not_transcoded_yet', true, BX_PAGE_EMBED);
+            exit;
+        }
+
+        // generate player code
+        $sCode = $oPlayer->getCodeAudio (BX_PLAYER_EMBED, array(
+            'mp3' => $oTranscoderSound->getFileUrl($a['id']),
+        ));
+
+        // display page
+        $oTemplate = BxDolTemplate::getInstance();
+        $oTemplate->setPageNameIndex (BX_PAGE_EMBED);
+        $oTemplate->setPageHeader ($a['file_name']);
+        $oTemplate->setPageContent ('page_main_code', $sCode);
+        $oTemplate->getPageCode();
+        exit;
+    }
+
+    protected function _embedChecks($sStorageKey, $iFileId)
+    {
+        // general checks
+        $oPlayer = BxDolPlayer::getObjectInstance();
+        $CNF = $this->getModule()->_oConfig->CNF;
+        $sStorage = isset($CNF[$sStorageKey]) ? $CNF[$sStorageKey] : false;
+        if (!$oPlayer || !$sStorage || !($oStorage = BxDolStorage::getObjectInstance($sStorage))) {
+            $this->displayErrorOccured('', BX_PAGE_EMBED);
+            exit;
+        }
+
+        // privacy check
+        $aGhost = $oStorage->getGhost($iFileId);
+        $aContentInfo = $aGhost && $aGhost['content_id'] ? $this->getModule()->_oDb->getContentInfoById($aGhost['content_id']) : false;
+        if (!$aGhost || ($aGhost['content_id'] && !$aContentInfo)) {
+            $this->displayPageNotFound('', BX_PAGE_EMBED);
+            exit;
+        }
+            
+        if (!$aGhost['content_id']) {
+            // if file is not associated with content yet, then only admin and owner can view it
+            if (!isAdmin() && CHECK_ACTION_RESULT_ALLOWED !== $this->getModule()->checkAllowedEditAnyEntry() && $aGhost['profile_id'] != bx_get_logged_profile_id()) {
+                $this->displayAccessDenied('', BX_PAGE_EMBED);
+                exit;
+            }
+        }
+        else {
+            // if file is associated with content, then check entry privacy
+            if (CHECK_ACTION_RESULT_ALLOWED !== $this->getModule()->checkAllowedView($aContentInfo)) {
+                $this->displayAccessDenied('', BX_PAGE_EMBED);
+                exit;
+            }
+        }
+
+        // get file info
+        if (!($a = $oStorage->getFile($iFileId))) {
+            $this->displayPageNotFound('', BX_PAGE_EMBED);
+            exit;
+        }
+
+        return array($oPlayer, $oStorage, $aContentInfo, $a);
+    }
+    
+    function _getImageSettings($sSettings)
+    {
+        $sCoverSettings = '';
+        $aCoverData = json_decode($sSettings, true);
+        if (!empty($aCoverData)){
+            $sCoverSettings = 'background-position: ' . $aCoverData['x'] . '% ' . $aCoverData['y'] . '%';
+        }
+        return $sCoverSettings;
+    }
+
+    function _prepareImage($aData, $sUniqId, $sUploader, $sStorage, $sField, $bAllowTweak)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+        $oUploader = null;
+        $sUploadersButtons = $sUploadersJs = '';
+
+        $aUploaders = $sUploader;
+        $sUploadersJs = '';
+        $sJsName = '';
+        foreach ($aUploaders as $sUploaderObject) {
+            $oUploader = BxDolUploader::getObjectInstance($sUploaderObject, $sStorage, $sUniqId, $this);
+            $sGhostTemplate = '{file_id}';
+
+            $aParamsJs = array_merge($oUploader->getUploaderJsParams(), 
+                [
+                    'content_id' => $aData['id'],
+                    'storage_private' => '0',
+                    'is_init_ghosts' => 0,
+                    'is_init_reordering' => 0
+                ]
+            );
+            $sUploadersJs .= $oUploader->getUploaderJs($sGhostTemplate, false, $aParamsJs);
+            $sJsName = $oUploader->getNameJsInstanceUploader();
+        }
+
+        $aParamsButtons = [
+            'content_id' => $aData['id'],
+            'storage_private' => '0',
+            'btn_class' => '',
+            'button_title' => '',
+            'attrs' => "class='hidden'"
+        ];
+        
+        $this->addJs(['BxDolUploader.js']);
+
+        $sAddCode = $this->parseHtmlByName('image_tweak.html', [
+            'id' => $aData['id'],
+            'js_object' => $sJsName,
+            'unique_id' => $sUniqId,
+            'id' => $aData['id'],
+            'allow_tweak' => $bAllowTweak,
+            'image_exists' => $aData[$sField] == 0 ? 'bx-image-edit-buttons-no-image' : '',
+            'field' => $sField,
+            'action_url' => BX_DOL_URL_ROOT . $this->_oConfig->getBaseUri(),
+            'uploader' => $oUploader->getUploaderButton($aParamsButtons),
+            'uploader_js' => $sUploadersJs,
+        ]); 
+        $this->addJsTranslation(['_sys_uploader_image_reposition_info']);        
+        return $sAddCode;
+    }
+
+    public function addCssJs()
+    {
+        $this->addCss('main.css');
+    }
+}
+
+/** @} */

+ 40 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/groups/classes/BxBaseModGroupsConnectionFans.php

@@ -0,0 +1,40 @@
+<?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;
+
+    public function __construct($aObject)
+    {
+        parent::__construct($aObject);
+
+        $this->_oModule = BxDolModule::getInstance($this->_sModule);
+    }
+
+    public function _checkAllowedConnectContent ($oContent)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        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);
+    }
+}
+
+/** @} */

+ 39 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/groups/classes/BxBaseModGroupsMenuViewMeta.php

@@ -0,0 +1,39 @@
+<?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
+ *
+ * @{
+ */
+
+/**
+ * View entry meta menu
+ */
+class BxBaseModGroupsMenuViewMeta extends BxBaseModProfileMenuViewMeta
+{
+    public function __construct($aObject, $oTemplate = false)
+    {
+        parent::__construct($aObject, $oTemplate);
+    }
+
+    protected function _getMenuItemMembers($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(!$this->_bContentPublic || !$this->_oContentProfile || empty($CNF['OBJECT_CONNECTIONS']))
+            return false;
+
+        $oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS']);
+        if(!$oConnection)
+            return false;
+        
+        $sIcon = BxTemplFunctions::getInstanceWithTemplate($this->_oTemplate)->getIconAsHtml(!empty($aItem['icon']) ? $aItem['icon'] : '');
+
+        return $oConnection->getCounter($this->_oContentProfile->id(), true, ['caption' => $aItem['title'], 'custom_icon' => $sIcon], BX_CONNECTIONS_CONTENT_TYPE_INITIATORS);
+    }
+}
+
+/** @} */

+ 1949 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/groups/classes/BxBaseModGroupsModule.php

@@ -0,0 +1,1949 @@
+<?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 serviceManageTools($sType = 'common')
+    {
+        $sResult = parent::serviceManageTools($sType);
+        if(!empty($sResult))
+            $this->_oTemplate->addJsSystem(['modules/base/groups/js/|manage_tools.js']);
+
+        return $sResult;
+    }
+
+    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);
+    }
+    
+    /**
+     * 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;
+
+        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;
+
+        return $oGrid->getCode();
+    }
+	
+    public function serviceFans ($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;
+
+        if(!$bAsArray) {
+            bx_import('BxDolConnection');
+            $mixedResult = $this->serviceBrowseConnectionsQuick ($oGroupProfile->id(), $this->_oConfig->CNF['OBJECT_CONNECTIONS'], BX_CONNECTIONS_CONTENT_TYPE_CONTENT, true);
+            if (!$mixedResult)
+                return MsgBox(_t('_sys_txt_empty'));
+        }
+        else
+            $mixedResult = BxDolConnection::getObjectInstance($this->_oConfig->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 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 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);
+    }
+
+
+    /**
+     * 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 = BxDolPermalinks::getInstance()->permalink($CNF['URL_ENTRY_FANS'], [
+                'profile_id' => $oGroupProfile->id()
+            ]);
+
+        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()
+    {
+        return BxBaseModGeneralModule::serviceGetTimelineData();
+    }
+
+    /**
+     * 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();
+
+        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'], true, 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
+    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;
+    }
+
+    protected function _getImagesForTimelinePost($aEvent, $aContentInfo, $sUrl, $aBrowseParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($aEvent['object_id'], $this->getName());
+
+        $sSrc = '';
+        if(isset($CNF['FIELD_COVER']) && !empty($aContentInfo[$CNF['FIELD_COVER']]))
+            $sSrc = $oGroupProfile->getCover();
+
+        if(empty($sSrc) && isset($CNF['FIELD_PICTURE']) && !empty($aContentInfo[$CNF['FIELD_PICTURE']]))
+            $sSrc = $oGroupProfile->getPicture();
+
+        return empty($sSrc) ? array() : array(
+            array('id' => $aContentInfo[$CNF['FIELD_PICTURE']], 'url' => $sUrl, 'src' => $sSrc, 'src_orig' => $sSrc),
+        );
+    }
+
+    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);
+    }
+}
+
+/** @} */

+ 31 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/groups/template/unit.html

@@ -0,0 +1,31 @@
+<div class="__class__ p-2 box-border">
+    <div class="bx-base-pofile-unit-cnt bx-def-box bx-def-box-round-corners bx-def-color-bg-box relative h-full overflow-hidden">
+        <div class="absolute z-0 w-full h-40 bg-gradient-to-b from-green-400 to-green-500"></div>
+        <a class="bx-base-pofile-unit-cover relative flex items-center justify-start w-full h-40 bg-no-repeat bg-center bg-cover hover:no-underline" style="background-image:url(__cover_url__); __cover_settings__" href="__content_url__" onclick="__content_click__">
+            <bx_if:show_thumbnail>
+                <div class="bx-def-unit-img bx-def-__size__-size __class_size__ relative ml-5">
+                    <bx_if:show_thumb_image>
+                        <img class="bx-base-pofile-unit-thumb bx-def-__size__ bx-def-__size__-size __class_size__ ring-4 ring-white dark:ring-gray-900 rounded-md box-border" src="__thumb_url__" />
+                    </bx_if:show_thumb_image>
+                    <bx_if:show_thumb_letter>
+                        <p class="bx-base-pofile-unit-thumb bx-def-__size__ bx-def-__size__-size __class_size__ flex items-center justify-center m-0 text-white font-bold ring-4 ring-white dark:ring-gray-900 rounded-md box-border" style="background-color:rgba(__color__)">__letter__</p>
+                    </bx_if:show_thumb_letter>
+                    <bx_if:show_online>
+                        <div class="bx-base-pofile-unit-online"></div>
+                    </bx_if:show_online>
+                </div>
+            </bx_if:show_thumbnail>
+        </a>
+        <div class="bx-base-pofile-unit-info p-4 text-left">
+            <div class="bx-base-pofile-unit-title">
+                <div class="bx-base-put-title truncate">
+                    <a href="__content_url__" onclick="__content_click__" title="__title_attr__" class="hover:underline text-gray-700 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white text-xl font-semibold">__title__</a>__badges__
+                </div>
+                <div class="bx-base-put-addon text-sm text-gray-500 dark:text-gray-400">__addon__</div>
+            </div>
+            <bx_if:meta>
+                <div class="bx-base-general-unit-meta flex flex-col items-stretch justify-center -mx-1 mt-2">__meta__</div>
+            </bx_if:meta>
+        </div>
+    </div>
+</div>

+ 260 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/profile/classes/BxBaseModProfileFormEntry.php

@@ -0,0 +1,260 @@
+<?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
+ *
+ * @{
+ */
+
+/**
+ * Create/edit profile form.
+ */
+class BxBaseModProfileFormEntry extends BxBaseModGeneralFormEntry
+{
+    protected $_iAccountProfileId = 0;
+    protected $_aImageFields = array ();
+    
+    protected $_aUploadersInfo = [];
+
+    public function __construct($aInfo, $oTemplate = false)
+    {   
+        if (!isset($this->_bAllowChangeUserForAdmins))
+            $this->_bAllowChangeUserForAdmins = false;
+        
+        parent::__construct($aInfo, $oTemplate);
+
+        $this->_sAuthorKey = 'profile_id';
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (!empty($CNF['FIELD_PICTURE']) && isset($this->aInputs[$CNF['FIELD_PICTURE']])) {
+            $this->_aImageFields[$CNF['FIELD_PICTURE']] = array (
+                'storage_object' => $CNF['OBJECT_STORAGE'],
+                'images_transcoder' => $CNF['OBJECT_IMAGES_TRANSCODER_THUMB'],
+                'uploaders' => $CNF['OBJECT_UPLOADERS_PICTURE'],
+            );
+        }
+
+        if (!empty($CNF['FIELD_COVER']) && isset($this->aInputs[$CNF['FIELD_COVER']])) {
+            $sStorage = $this->_oModule->_oConfig->getObject($CNF['OBJECT_STORAGE_COVER']);
+            $sUploadersId = genRndPwd(8, false);
+            $aUploaders = !empty($this->aInputs[$CNF['FIELD_COVER']]['value']) ? unserialize($this->aInputs[$CNF['FIELD_COVER']]['value']) : $this->_oModule->_oConfig->getUploaders($CNF['FIELD_COVER']);
+
+            foreach($aUploaders as $sUploader){
+                $this->_aUploadersInfo[$sUploader] = array(
+                    'id' => $sUploadersId, 
+                    'name' => $sUploader,
+                    'js_object' => BxDolUploader::getObjectInstance($sUploader, $sStorage, $sUploadersId)->getNameJsInstanceUploader()
+                );
+            }
+            
+            $this->_aImageFields[$CNF['FIELD_COVER']] = array (
+                'storage_object' => $CNF['OBJECT_STORAGE_COVER'],
+                'images_transcoder' => $CNF['OBJECT_IMAGES_TRANSCODER_COVER_THUMB'],
+                'uploaders_id' => $sUploadersId,
+                'uploaders' => $aUploaders
+            );
+        }
+
+        foreach ($this->_aImageFields as $sField => $aParams) {
+            $this->aInputs[$sField]['storage_object'] = $aParams['storage_object'];
+            $this->aInputs[$sField]['uploaders'] = !empty($this->aInputs[$sField]['value']) ? unserialize($this->aInputs[$sField]['value']) : $aParams['uploaders'];
+            $this->aInputs[$sField]['images_transcoder'] = $aParams['images_transcoder'];
+            $this->aInputs[$sField]['uploaders_id'] = isset($aParams['uploaders_id']) ? $aParams['uploaders_id'] : '';
+            $this->aInputs[$sField]['storage_private'] = 0;
+            $this->aInputs[$sField]['multiple'] = false;
+            $this->aInputs[$sField]['content_id'] = 0;
+            $this->aInputs[$sField]['ghost_template'] = '';
+        }
+
+        $oAccountProfile = BxDolProfile::getInstanceAccountProfile();
+        if ($oAccountProfile)
+            $this->_iAccountProfileId = $oAccountProfile->id();
+    }
+
+    public function getUploadersInfo($sField = '')
+    {
+        if(empty($sField))
+            return $this->_aUploadersInfo;
+
+        $aUploaders = !empty($this->aInputs[$sField]['value']) ? unserialize($this->aInputs[$sField]['value']) : $this->_oModule->_oConfig->getUploaders($sField);
+
+        return $this->_aUploadersInfo[array_shift($aUploaders)];
+    }
+    
+    function initChecker ($aValues = array (), $aSpecificValues = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $aContentInfo = isset($CNF['FIELD_ID']) && isset($aValues[$CNF['FIELD_ID']]) ? $this->_oModule->_oDb->getContentInfoById ($aValues[$CNF['FIELD_ID']]) : array();
+        
+        foreach ($this->_aImageFields as $sField => $aParams) {
+
+            if ($aValues && !empty($aValues[$CNF['FIELD_ID']]))
+                $this->aInputs[$sField]['content_id'] = $aValues[$CNF['FIELD_ID']];
+
+            $this->aInputs[$sField]['ghost_template'] = $this->_oModule->_oTemplate->parseHtmlByName('form_ghost_template.html', $this->_getProfilePhotoGhostTmplVars($sField, $aContentInfo));
+        }
+
+        parent::initChecker($aValues, $aSpecificValues);
+    }
+
+    function delete ($iContentId, $aContentInfo = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $iProfileId = $this->getContentOwnerProfileId($iContentId);        
+
+        foreach ($this->_aImageFields as $sField => $aParams) {
+            $oStorage = BxDolStorage::getObjectInstance($aParams['storage_object']);
+            $aFiles = $oStorage->getGhosts($iProfileId, $iContentId);
+
+            foreach ($aFiles as $aFile) {
+                if (!$oStorage->getFile($aFile['id']))
+                    continue;
+                $bRet = $oStorage->deleteFile($aFile['id'], $this->_iAccountProfileId);
+            }
+        }
+
+        return parent::delete($iContentId, $aContentInfo);
+    }
+
+    protected function genCustomViewRowValueProfileEmail($aInput)
+    {
+        return $this->genCustomViewRowValueProfileEmailOrIp($aInput);
+    }
+    
+    protected function genCustomViewRowValueProfileIp($aInput)
+    {
+        return $this->genCustomViewRowValueProfileEmailOrIp($aInput);
+    }
+    
+    protected function genCustomViewRowValueFriendsCount($aInput)
+    {
+        if (isset($this->_oModule->_oConfig->CNF['URI_VIEW_FRIENDS'])){
+            $oProfile = $this->_oModule->getProfileByCurrentUrl();
+            if ($oProfile){
+                $oConnection = BxDolConnection::getObjectInstance('sys_profiles_friends');
+                $iCount = $oConnection->getConnectedContentCount($oProfile->id(), true);
+                return $this->_oModule->_oTemplate->parseHtmlByName('name_link.html', array(
+                    'href' => bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $this->_oModule->_oConfig->CNF['URI_VIEW_FRIENDS'] . '&profile_id=' . $oProfile->id())),
+                    'title' => '',
+                    'content' => $iCount
+                ));
+            }
+        }
+        return '';
+    }
+    
+    protected function genCustomViewRowValueFollowersCount($aInput)
+    {
+        if (isset($this->_oModule->_oConfig->CNF['URI_VIEW_SUBSCRIPTIONS'])){
+            $oProfile = $this->_oModule->getProfileByCurrentUrl();
+            if ($oProfile){
+                $oConnectionFollow = BxDolConnection::getObjectInstance('sys_profiles_subscriptions');
+                $iCount = $oConnectionFollow->getConnectedInitiatorsCount($oProfile->id());
+                return $this->_oModule->_oTemplate->parseHtmlByName('name_link.html', array(
+                    'href' => bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $this->_oModule->_oConfig->CNF['URI_VIEW_SUBSCRIPTIONS'] . '&profile_id=' . $oProfile->id())),
+                    'title' => '',
+                    'content' => $iCount
+                ));
+            }
+        }
+        return '';
+    }
+    
+    private function genCustomViewRowValueProfileEmailOrIp($aInput)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        if(empty($aInput['value']))
+            return '';
+
+        $sValue = $aInput['value'];
+
+        $sModuleAccounts = 'bx_accounts';
+    	if(!BxDolModuleQuery::getInstance()->isEnabledByName($sModuleAccounts))
+    		return $sValue;
+
+		$oModuleAccounts = BxDolModule::getInstance($sModuleAccounts);
+		if(!$oModuleAccounts || empty($oModuleAccounts->_oConfig->CNF['URL_MANAGE_ADMINISTRATION']))
+			return $sValue;
+
+        return $this->_oModule->_oTemplate->parseHtmlByName('name_link.html', array(
+            'href' => bx_absolute_url(BxDolPermalinks::getInstance()->permalink($oModuleAccounts->_oConfig->CNF['URL_MANAGE_ADMINISTRATION'], array(
+            	'filter' => urlencode($sValue)
+            ))),
+            'title' => '',
+            'content' => $sValue
+        ));
+    }
+    
+    protected function genCustomViewRowValueProfileStatus($aInput)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        if(empty($aInput['value']))
+            return '';
+
+        $sStatus = _t('_sys_profile_status_' . $aInput['value']);
+        if(empty($CNF['URL_MANAGE_ADMINISTRATION']) || empty($CNF['FIELD_TITLE']) || empty($this->aInputs[$CNF['FIELD_TITLE']]['value']))
+            return $sStatus;
+
+        return $this->_oModule->_oTemplate->parseHtmlByName('name_link.html', array(
+            'href' => bx_absolute_url(BxDolPermalinks::getInstance()->permalink($CNF['URL_MANAGE_ADMINISTRATION'], array(
+            	'filter' => urlencode($this->aInputs[$CNF['FIELD_TITLE']]['value'])
+            ))),
+            'title' => '',
+            'content' => $sStatus
+        ));
+    }
+
+    protected function _associalFileWithContent($oStorage, $iFileId, $iProfileId, $iContentId, $sPictureField = '')
+    {
+        $oStorage->updateGhostsContentId ($iFileId, $iProfileId, $iContentId, $this->_isAdmin($iContentId));
+
+        $bResult = (int)$this->_oModule->_oDb->updateContentPictureById($iContentId, 0/*$iProfileId*/, $iFileId, $sPictureField) > 0;
+        if(!$bResult) 
+            return;
+
+        $this->_oModule->onUpdateImage($iContentId, $sPictureField, $iFileId, $iProfileId);
+    }
+
+    protected function _getProfilePhotoGhostTmplVars($sField, $aContentInfo = array())
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+    	return [
+            'name' => $this->aInputs[$sField]['name'],
+            'content_id' => $this->aInputs[$sField]['content_id'],
+            'bx_if:set_thumb' => [
+                'condition' => false,
+                'content' => [],
+            ]
+        ];
+    }
+
+    protected function _isAdmin ($iContentId = 0)
+    {
+        if (parent::_isAdmin ($iContentId))
+            return true;
+        if (!$iContentId || !($aDataEntry = $this->_oModule->_oDb->getContentInfoById((int)$iContentId)))
+            return false;
+        return CHECK_ACTION_RESULT_ALLOWED == $this->_oModule->checkAllowedEdit ($aDataEntry);        
+    }
+
+    protected function _getPrivacyFields($aKeysF2O = array())
+    {
+        if(empty($aKeysF2O))
+            $aKeysF2O = array(
+                'FIELD_ALLOW_VIEW_TO' => 'OBJECT_PRIVACY_VIEW',
+                'FIELD_ALLOW_POST_TO' => 'OBJECT_PRIVACY_POST',
+                'FIELD_ALLOW_CONTACT_TO' => 'OBJECT_PRIVACY_CONTACT'
+            );
+
+        return parent::_getPrivacyFields($aKeysF2O);
+    }
+}
+
+/** @} */

+ 272 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/profile/classes/BxBaseModProfileMenuSnippetMeta.php

@@ -0,0 +1,272 @@
+<?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 BxBaseModProfileMenuSnippetMeta extends BxBaseModGeneralMenuSnippetMeta
+{
+    protected $_bContentPublic;
+    protected $_oContentProfile;
+
+    protected $_aConnectionToFunctionCheck;
+    protected $_aConnectionToFunctionTitle;
+
+    public function __construct($aObject, $oTemplate = false)
+    {
+        parent::__construct($aObject, $oTemplate);
+
+        $this->_bShowZeros = true;
+        $this->_bContentPublic = false;
+
+        $this->_aConnectionToFunctionCheck = array(
+            'sys_profiles_friends' => array(
+            	'add' => 'checkAllowedFriendAdd', 
+            	'remove' => 'checkAllowedFriendRemove'
+            ),
+            'sys_profiles_subscriptions' => array(
+                'add' => 'checkAllowedSubscribeAdd',
+                'remove' => 'checkAllowedSubscribeRemove'
+            )
+        );
+
+        $this->_aConnectionToFunctionTitle = array(
+            'sys_profiles_friends' => '_getMenuItemProfilesFriendsTitle',
+            'sys_profiles_subscriptions' => '_getMenuItemProfilesSubscriptionsTitle'
+        );
+    }
+
+    public function setContentId($iContentId)
+    {
+        parent::setContentId($iContentId);
+
+        if(!empty($this->_iContentId))
+            $this->_oContentProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->_sModule);
+    }
+
+    protected function getMenuItemConnectionJsCode($sConnection, $sAction, $iContentProfile, $aItem)
+    {
+        return 'bx_conn_action(this, \'' . $sConnection . '\', \'' . $sAction . '\', \'' . $iContentProfile . '\', false, function(oData, eLink) {$(eLink).parents(\'.bx-menu-item:first\').remove();})';
+    }
+
+    public function setContentPublic($bContentPublic)
+    {
+        $this->_bContentPublic = $bContentPublic;
+    }
+
+    protected function _getMenuItemBefriend($aItem)
+    {
+        return $this->_getMenuItemConnection('sys_profiles_friends', 'add', $aItem);
+    }
+
+    protected function _getMenuItemUnfriend($aItem)
+    {
+        return $this->_getMenuItemConnection('sys_profiles_friends', 'remove', $aItem);
+    }
+
+    protected function _getMenuItemSubscribe($aItem)
+    {
+        return $this->_getMenuItemConnection('sys_profiles_subscriptions', 'add', $aItem);
+    }
+
+    protected function _getMenuItemUnsubscribe($aItem)
+    {
+        return $this->_getMenuItemConnection('sys_profiles_subscriptions', 'remove', $aItem);
+    }
+
+    protected function _getMenuItemFriends($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(!$this->_bContentPublic || !$this->_oContentProfile)
+            return false;
+
+        $oConnection = BxDolConnection::getObjectInstance('sys_profiles_friends');
+        if(!$oConnection)
+            return false;
+
+        $iFriends = $oConnection->getConnectedInitiatorsCount($this->_oContentProfile->id(), true);
+        if(!$iFriends && !$this->_bShowZeros)
+            return false;
+
+        $sTitle = _t('_sys_menu_item_title_sm_friends', $iFriends);
+
+        if($this->_bIsApi)
+            return $this->_getMenuItemAPI($aItem, 'text', [
+                'title' => $sTitle
+            ]);
+
+        return $this->getUnitMetaItemText($sTitle);
+    }
+
+    protected function _getMenuItemFriendsMutual($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(!$this->_bContentPublic || !$this->_oContentProfile)
+            return false;
+
+        $oConnection = BxDolConnection::getObjectInstance('sys_profiles_friends');
+        if(!$oConnection)
+            return false;
+
+        $iFriends = $oConnection->getCommonContentCount($this->_oContentProfile->id(), bx_get_logged_profile_id(), true);
+        if(!$iFriends && !$this->_bShowZeros)
+            return false;
+
+        $sTitle = _t('_sys_menu_item_title_sm_friends_mutual', $iFriends);
+
+        if($this->_bIsApi)
+            return $this->_getMenuItemAPI($aItem, 'text', [
+                'title' => $sTitle
+            ]);
+
+        return $this->getUnitMetaItemText($sTitle);
+    }
+
+    protected function _getMenuItemSubscribers($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(!$this->_bContentPublic || !$this->_oContentProfile)
+            return false;
+
+        $oConnection = BxDolConnection::getObjectInstance('sys_profiles_subscriptions');
+        if(!$oConnection)
+            return false;
+
+        $iSubscribers = $oConnection->getConnectedInitiatorsCount($this->_oContentProfile->id());
+        if(!$iSubscribers && !$this->_bShowZeros)
+            return false;
+
+        $sIcon = BxTemplFunctions::getInstanceWithTemplate($this->_oTemplate)->getIconAsHtml(!empty($aItem['icon']) ? $aItem['icon'] : '');
+
+        if($this->_bIsApi)
+            return false;
+
+        return $this->getUnitMetaItemCustom($oConnection->getCounter($this->_oContentProfile->id(), false, [
+            'caption' => '_sys_menu_item_title_sm_subscribers', 
+            'custom_icon' => $sIcon
+        ], BX_CONNECTIONS_CONTENT_TYPE_INITIATORS));
+    }
+
+    protected function _getMenuItemConnection($sConnection, $sAction, &$aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(!isLogged() || $this->_oModule->{$this->_aConnectionToFunctionCheck[$sConnection][$sAction]}($this->_aContentInfo) !== CHECK_ACTION_RESULT_ALLOWED)
+            return false;
+
+        $iContentProfile = $this->_oContentProfile->id();
+
+        $oConnection = BxDolConnection::getObjectInstance($sConnection);
+        if(!$oConnection)
+            return false;
+
+        $sTitle = $this->{$this->_aConnectionToFunctionTitle[$sConnection]}($sAction, $oConnection);
+        if(empty($sTitle))
+            return false;
+
+        if($this->_bIsApi)
+            return $this->_getMenuItemAPI($aItem, ['display' => 'element'], [
+                'title' => $sTitle,
+                'data' => [
+                    'type' => 'connections',
+                    'o' => $sConnection,
+                    'a' => $sAction,
+                    'iid' => bx_get_logged_profile_id(),
+                    'cid' => $iContentProfile,
+                    'title' => $sTitle,
+                    'primary' => !empty($aItem['primary']),
+                ]
+            ]);
+
+        return [
+            $this->getUnitMetaItemButton($sTitle, array(
+                'class' => !empty($aItem['primary']) ? 'bx-btn-primary' : '',
+                'onclick' => $this->getMenuItemConnectionJsCode($sConnection, $sAction, $iContentProfile, $aItem)
+            )),
+            'bx-menu-item-button'
+        ];
+    }
+
+    protected function _getMenuItemProfilesFriendsTitle($sAction, &$oConnection)
+    {
+        $iProfile = bx_get_logged_profile_id();
+        $iContentProfile = $this->_oContentProfile->id();
+
+        $aResult = array();
+        if($oConnection->isConnectedNotMutual($iProfile, $iContentProfile))
+            $aResult = array(
+                'add' => '',
+                'remove' => _t('_sys_menu_item_title_sm_unfriend_cancel'),
+            );
+        else if($oConnection->isConnectedNotMutual($iContentProfile, $iProfile))
+            $aResult = array(
+                'add' => _t('_sys_menu_item_title_sm_befriend_confirm'),
+                'remove' => _t('_sys_menu_item_title_sm_unfriend_reject'),
+            );
+        else if($oConnection->isConnected($iProfile, $iContentProfile, true))
+            $aResult = array(
+                'add' => '',
+                'remove' => _t('_sys_menu_item_title_sm_unfriend'),
+            );
+        else
+            $aResult = array(
+                'add' => _t('_sys_menu_item_title_sm_befriend'),
+                'remove' => '',
+            );
+
+        return !empty($sAction) && isset($aResult[$sAction]) ? $aResult[$sAction] : $aResult;
+    }
+
+    protected function _getMenuItemProfilesSubscriptionsTitle($sAction, &$oConnection)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $iProfile = bx_get_logged_profile_id();
+        $iContentProfile = $this->_oContentProfile->id();
+
+        $aResult = array();
+        if($oConnection->isConnected($iProfile, $iContentProfile))
+            $aResult = array(
+                'add' => '',
+                'remove' => _t(!empty($CNF['T']['menu_item_title_unsubscribe']) ? $CNF['T']['menu_item_title_unsubscribe'] : '_sys_menu_item_title_sm_unsubscribe'),
+            );
+        else
+            $aResult = array(
+                '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;
+    }
+
+    protected function _getMenuItemMembership($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(!$this->_bContentPublic || !$this->_oContentProfile)
+            return false;
+
+        $aMembership = BxDolAcl::getInstance()->getMemberMembershipInfo($this->_oContentProfile->id());
+        if(empty($aMembership) || !is_array($aMembership))
+            return false;
+
+        $sTitle = _t($aMembership['name']);
+
+        if($this->_bIsApi)
+            return $this->_getMenuItemAPI($aItem, 'text', [
+                'title' => $sTitle
+            ]);
+
+        return $this->getUnitMetaItemText($sTitle);
+    }
+}
+
+/** @} */

+ 217 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/profile/classes/BxBaseModProfileMenuViewMeta.php

@@ -0,0 +1,217 @@
+<?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
+ *
+ * @{
+ */
+
+/**
+ * View entry meta menu
+ */
+class BxBaseModProfileMenuViewMeta extends BxTemplMenuUnitMeta
+{
+    protected $_sModule;
+    protected $_oModule;
+
+    protected $_bShowZeros;
+
+    protected $_iContentId;
+    protected $_aContentInfo;
+    protected $_bContentPublic;
+    protected $_oContentProfile;
+    protected $_aContentProfileInfo;
+
+    public function __construct($aObject, $oTemplate = false)
+    {
+        $this->_oModule = BxDolModule::getInstance($this->_sModule);
+
+        parent::__construct($aObject, $oTemplate);
+
+        $this->_bShowZeros = false;
+
+        $this->_iContentId = 0;
+        $this->_aContentInfo = [];
+        $this->_bContentPublic = false;
+        $this->_oContentProfile = null;
+        $this->_aContentProfileInfo = [];
+    }
+
+    public function setContentId($iContentId)
+    {
+        $this->_iContentId = $iContentId;
+        $this->_aContentInfo = $this->_oModule->_oDb->getContentInfoById($this->_iContentId);
+        if(empty($this->_aContentInfo) || !is_array($this->_aContentInfo))
+            return;
+
+        $this->_oContentProfile = BxDolProfile::getInstanceByContentAndType($this->_iContentId, $this->_sModule);
+        if(!$this->_oContentProfile) 
+            return;
+
+        $this->_aContentProfileInfo = $this->_oContentProfile->getInfo();     
+
+        $this->addMarkers($this->_aContentProfileInfo);
+        $this->addMarkers(array(
+            'profile_id' => $this->_oContentProfile->id()
+        ));
+    }
+
+    public function setContentPublic($bContentPublic)
+    {
+        $this->_bContentPublic = $bContentPublic;
+    }
+
+    public function getCode()
+    {
+        if(empty($this->_iContentId))
+            $this->_retrieveContentId();
+
+        return parent::getCode();
+    }
+
+    protected function _getMenuItemMembership($aItem)
+    {
+        if(!$this->_bContentPublic || !$this->_oContentProfile)
+            return false;
+
+        $oAcl = BxDolAcl::getInstance();
+        $oTemplate = BxDolTemplate::getInstance();
+        $aMembership =  $oAcl->getMemberMembershipInfo($this->_oContentProfile->id());
+        $aLevelInfo =  $oAcl->getMembershipInfo($aMembership['id']);
+        return $aMembership ? $this->getUnitMetaItemText($oTemplate->parseHtmlByName('menu_meta_item.html', ['icon' => $oTemplate->getImage($aLevelInfo['icon'], array('class' => 'bx-acl-m-thumbnail')), 'caption' => _t($aMembership['name'])])): false;
+                                                                                            
+    }
+    
+    protected function _getMenuItemBadges($aItem)
+    {
+        $sResult = $this->_oModule->serviceGetBadges($this->_iContentId);
+        if(!empty($sResult))
+            $sResult = $this->getUnitMetaItemText($sResult, ['class' => 'bx-base-bages-container']);
+
+        return $sResult;
+    }
+
+    protected function _getMenuItemFriends($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(!$this->_bContentPublic || !$this->_oContentProfile)
+            return false;
+
+        $oConnection = BxDolConnection::getObjectInstance('sys_profiles_friends');
+        if(!$oConnection)
+            return false;
+
+        $sIcon = BxTemplFunctions::getInstanceWithTemplate($this->_oTemplate)->getIconAsHtml(!empty($aItem['icon']) ? $aItem['icon'] : '');
+
+        return $oConnection->getCounter($this->_oContentProfile->id(), true, ['caption' => $aItem['title'], 'custom_icon' => $sIcon], BX_CONNECTIONS_CONTENT_TYPE_INITIATORS);
+    }
+
+    protected function _getMenuItemSubscribers($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(!$this->_bContentPublic || !$this->_oContentProfile)
+            return false;
+
+        $oConnection = BxDolConnection::getObjectInstance('sys_profiles_subscriptions');
+        if(!$oConnection)
+            return false;
+
+        $sIcon = BxTemplFunctions::getInstanceWithTemplate($this->_oTemplate)->getIconAsHtml(!empty($aItem['icon']) ? $aItem['icon'] : '');
+        
+        return $oConnection->getCounter($this->_oContentProfile->id(), false, ['caption' => $aItem['title'], 'custom_icon' => $sIcon], BX_CONNECTIONS_CONTENT_TYPE_INITIATORS);
+    }
+
+    protected function _getMenuItemViews($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($CNF['OBJECT_VIEWS']) || empty($CNF['FIELD_VIEWS']) || (empty($this->_aContentInfo[$CNF['FIELD_VIEWS']]) && !$this->_bShowZeros))
+            return false;
+
+        $oObject = isset($CNF['OBJECT_VIEWS']) ? BxDolView::getObjectInstance($CNF['OBJECT_VIEWS'], $this->_aContentInfo[$CNF['FIELD_ID']]) : null;
+        
+        $sIcon = BxTemplFunctions::getInstanceWithTemplate($this->_oTemplate)->getIconAsHtml(!empty($aItem['icon']) ? $aItem['icon'] : '');
+        
+        return $oObject ? $oObject->getCounter(['show_counter_empty' => false, 'show_counter_in_brackets' => false, 'dynamic_mode' => true, 'caption' => '_view_n_views', 'custom_icon' => $sIcon]) : '';
+    }
+
+    protected function _getMenuItemVotes($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($CNF['OBJECT_VOTES']) || empty($CNF['FIELD_VOTES']) || (empty($this->_aContentInfo[$CNF['FIELD_VOTES']]) && !$this->_bShowZeros))
+            return false;
+        
+        $sIcon = BxTemplFunctions::getInstanceWithTemplate($this->_oTemplate)->getIconAsHtml(!empty($aItem['icon']) ? $aItem['icon'] : '');
+        
+        $oObject = isset($CNF['OBJECT_VOTES']) ? BxDolVote::getObjectInstance($CNF['OBJECT_VOTES'], $this->_aContentInfo[$CNF['FIELD_ID']]) : null;
+        return $oObject ? $oObject->getCounter(['show_counter_label_icon' => true, 'show_counter_empty' => false, 'dynamic_mode' => true, 'caption' => '_vote_n_votes', 'custom_icon' => $sIcon]) : '';
+    }
+
+    protected function _getMenuItemReactions($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($CNF['OBJECT_REACTIONS']))
+            return false;
+
+        $oVotes = BxDolVote::getObjectInstance($CNF['OBJECT_REACTIONS'], $this->_aContentInfo[$CNF['FIELD_ID']]);
+        if(!$oVotes)
+            return false;
+
+        return $this->getUnitMetaItemCustom($oVotes->getElementInline(array('show_counter' => false)));
+    }
+    
+    protected function _getMenuItemScores($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($CNF['OBJECT_SCORES']))
+            return false;
+
+        $oScores = BxDolScore::getObjectInstance($CNF['OBJECT_SCORES'], $this->_aContentInfo[$CNF['FIELD_ID']]);
+        if(!$oScores)
+            return false;
+
+        return $this->getUnitMetaItemCustom($oScores->getElementInline(array('show_counter' => false)));
+    }
+
+    protected function _getMenuItemComments($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($CNF['OBJECT_COMMENTS']) || empty($CNF['FIELD_COMMENTS']) || (empty($this->_aContentInfo[$CNF['FIELD_COMMENTS']]) && !$this->_bShowZeros))
+            return false;
+
+        $oComments = BxDolCmts::getObjectInstance($CNF['OBJECT_COMMENTS'], $this->_aContentInfo[$CNF['FIELD_ID']]);
+        if(!$oComments || !$oComments->isEnabled())
+            return false;
+
+        $oObject = isset($CNF['OBJECT_COMMENTS']) ? BxDolCmts::getObjectInstance($CNF['OBJECT_COMMENTS'], $this->_aContentInfo[$CNF['FIELD_ID']]) : null;
+
+        return $oObject ? $oObject->getCounter([
+            'overwrite_counter_link_onclick' => 'javascript:void(0)', 
+            'show_counter_empty' => false, 
+            'recalculate_counter' => true,
+            'dynamic_mode' => true, 
+            'caption' => '_cmt_txt_n_comments', 
+            'custom_icon' => BxTemplFunctions::getInstanceWithTemplate($this->_oTemplate)->getIconAsHtml(!empty($aItem['icon']) ? $aItem['icon'] : '')
+        ]) : '';
+    }
+
+    protected function _retrieveContentId()
+    {
+        if(bx_get('id') !== false)
+            $this->setContentId(bx_process_input(bx_get('id'), BX_DATA_INT));
+
+        if(empty($this->_iContentId) && bx_get('profile_id') !== false)
+            $this->setContentId(BxDolProfile::getInstance(bx_process_input(bx_get('profile_id'), BX_DATA_INT))->getContentId());
+    }
+}
+
+/** @} */

+ 2138 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/profile/classes/BxBaseModProfileModule.php

@@ -0,0 +1,2138 @@
+<?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' => '',
+            '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);
+            return bx_api_get_image($CNF['OBJECT_STORAGE'], $aContentInfo[$CNF['FIELD_COVER']]);
+        }
+
+        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 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()
+	{
+		bx_import('SearchResult', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'SearchResult';
+        $o = new $sClass();
+        $o->fillFilters(array(
+			'account_id' => getLoggedId(),
+        	'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)
+    {
+        return $this->_oTemplate->getSnippetMenuVars($iProfileId, $bPublic);
+    }
+        
+	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 = array())
+    {
+        $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;
+
+        $sTemplate = 'unit.html';
+        $sTemplateSize = false;
+        $aTemplateVars = array();
+        if(!empty($aParams['template'])) {
+            if(is_string($aParams['template']))
+                $sTemplate = $aParams['template'] . '.html';
+            else if(is_array($aParams['template'])) {
+                if(!empty($aParams['template']['name']))
+                    $sTemplate = $aParams['template']['name'] . '.html';
+
+                if(!empty($aParams['template']['size']))
+                    $sTemplateSize = $aParams['template']['size'];
+
+                if(!empty($aParams['template']['vars']))
+                    $aTemplateVars = $aParams['template']['vars'];
+            }
+        }
+
+        return $this->_oTemplate->unit($aContentInfo, $bCheckPrivateContent, array($sTemplate, $sTemplateSize, $aTemplateVars));
+    }
+
+    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 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',
+            array(
+                '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);
+            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 array();
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return array();
+            
+        $aSubcontentInfo = BxDolService::call('bx_timeline', 'get_info', array((int)$aEvent['subobject_id'], false));
+        if(empty($aSubcontentInfo) || !is_array($aSubcontentInfo))
+            return array();
+
+        $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, '...');
+
+        return array(
+            '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_author' => $oGroupProfile->id(),
+            'subentry_sample' => $sSubentrySample,
+            'subentry_url' => $sSubentryUrl,
+            '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 serviceGetConnectionButtonsTitles($iProfileId, $sConnectionsObject = 'sys_profiles_friends')
+    {
+        if (!isLogged())
+            return array();
+
+        if (!($oConn = BxDolConnection::getObjectInstance($sConnectionsObject)))
+            return array();
+
+        $CNF = $this->_oConfig->CNF;
+
+        if ($oConn->isConnectedNotMutual(bx_get_logged_profile_id(), $iProfileId)) {
+            return array(
+                'add' => _t($CNF['T']['menu_item_title_befriend_sent']),
+                'remove' => _t($CNF['T']['menu_item_title_unfriend_cancel_request']),
+            );
+        } elseif ($oConn->isConnectedNotMutual($iProfileId, bx_get_logged_profile_id())) {
+            return array(
+                'add' => _t($CNF['T']['menu_item_title_befriend_confirm']),
+                'remove' => _t($CNF['T']['menu_item_title_unfriend_reject_request']),
+            );
+        } elseif ($oConn->isConnected($iProfileId, bx_get_logged_profile_id(), true)) {
+            return array(
+                'add' => '',
+                'remove' => _t($CNF['T']['menu_item_title_unfriend']),
+            );
+        } else {
+            return array(
+                'add' => _t($CNF['T']['menu_item_title_befriend']),
+                'remove' => '',
+            );
+        }
+    }
+
+
+    // ====== 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', true, 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_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']]);
+    }
+
+
+    // ====== 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 = array('id' => $aFileInfo['id'], 'url' => $sUrl, 'src' => $sImage);
+            if (CHECK_ACTION_RESULT_ALLOWED === $this->checkAllowedView($aContentInfo))
+                $a['src_orig'] = $sImageOrig;
+            $aImages = array($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);
+    }
+}
+
+/** @} */

+ 287 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/profile/classes/BxBaseModProfileSearchResult.php

@@ -0,0 +1,287 @@
+<?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 BxBaseModProfileSearchResult extends BxBaseModGeneralSearchResult
+{      
+    protected $bRecommendedView = false;
+    
+    public function __construct($sMode = '', $aParams = array())
+    {
+        parent::__construct($sMode, $aParams);
+
+        $this->aUnitViews = [
+            'unit' => 'unit_wo_cover.html', 
+            'unit_wo_info' => 'unit_wo_info.html', 
+            'gallery' => 'unit_with_cover.html', 
+            'showcase' => 'unit_with_cover_showcase.html', 
+            'showcase_wo_info' => 'unit_wo_info_showcase.html', 
+            'simple' => 'unit_wo_links.html'
+        ];
+        if (!empty($aParams['unit_views']) && is_array($aParams['unit_views']))
+            $this->aUnitViews = array_merge ($this->aUnitViews, $aParams['unit_views']);
+
+        if (!empty($aParams['unit_view']))
+            $this->sUnitViewDefault = $aParams['unit_view'];
+        
+        $this->sUnitTemplate = $this->aUnitViews[$this->sUnitViewDefault];
+        $this->sCenterContentUnitSelector = '.bx-base-pofile-unit';
+        $this->addContainerClass (array(
+            'bx-base-pofile-units-wrapper', 
+            'bx-base-puw-' . str_replace('_' ,  '-', $this->sUnitViewDefault),
+            'bx-def-margin-sec-neg'
+        ));
+
+        if(in_array($this->sUnitTemplate, ['unit_wo_cover.html', 'unit_wo_info.html', 'unit_wo_info_showcase.html']))
+            $this->removeContainerClass('bx-def-margin-sec-neg');
+
+        if(in_array($this->sUnitTemplate, ['unit_with_cover_showcase.html', 'unit_wo_info_showcase.html'])) {
+            $this->bShowcaseView = true;
+
+            if($this->sUnitTemplate == 'unit_wo_info_showcase.html')
+                $this->addContainerAttribute(array('bx-sc-group-cells' => 3));
+        }
+
+        if ($sMode == 'recommended')
+            $this->bRecommendedView=true;
+    }
+
+    function getRssUnitImage (&$a, $sField)
+    {
+        $CNF = &$this->oModule->_oConfig->CNF;
+
+        if(empty($sField) || empty($a[$sField]))
+            return '';
+
+        $sTranscoder = '';
+        if(!empty($CNF['OBJECT_IMAGES_TRANSCODER_PICTURE']))
+            $sTranscoder = $CNF['OBJECT_IMAGES_TRANSCODER_PICTURE'];
+        else if(!empty($CNF['OBJECT_IMAGES_TRANSCODER_GALLERY']))
+            $sTranscoder = $CNF['OBJECT_IMAGES_TRANSCODER_GALLERY'];
+        else 
+            return '';
+
+        $oTranscoder = BxDolTranscoderImage::getObjectInstance($sTranscoder);
+        if(!$oTranscoder)
+            return '';
+
+        return $oTranscoder->getFileUrl($a[$sField]);      
+    }
+
+    protected function _setAccountConfirmationConditions($aParams)
+    {
+        $sConfirmationType = getParam('sys_account_confirmation_type');
+        $bHideUnconfirmed = getParam('sys_account_hide_unconfirmed_accounts') == 'on';
+        if($sConfirmationType == BX_ACCOUNT_CONFIRMATION_NONE || !$bHideUnconfirmed) 
+            return;
+
+        $this->aCurrent['restriction'] = array_merge($this->aCurrent['restriction'], array(
+            'account_confirmed_email' => array('value' => '', 'field' => 'email_confirmed', 'operator' => '=', 'table' => 'sys_accounts'),
+            'account_confirmed_phone' => array('value' => '', 'field' => 'phone_confirmed', 'operator' => '=', 'table' => 'sys_accounts'),
+            'account_confirmed_email_and_phone' => array('value' => '', 'field' => 'phone_confirmed` | `sys_accounts`.`email_confirmed', 'operator' => '=', 'table' => 'sys_accounts'),
+        ));
+
+        switch($sConfirmationType) {
+            case BX_ACCOUNT_CONFIRMATION_EMAIL:
+                $this->aCurrent['restriction']['account_confirmed_email']['value'] = 1;
+                break;
+
+            case BX_ACCOUNT_CONFIRMATION_PHONE:
+                $this->aCurrent['restriction']['account_confirmed_phone']['value'] = 1;
+                break;
+
+            case BX_ACCOUNT_CONFIRMATION_EMAIL_PHONE:
+                $this->aCurrent['restriction']['account_confirmed_email']['value'] = 1;
+                $this->aCurrent['restriction']['account_confirmed_phone']['value'] = 1;
+                break;
+                
+            case BX_ACCOUNT_CONFIRMATION_EMAIL_OR_PHONE:
+                $this->aCurrent['restriction']['account_confirmed_email_and_phone']['value'] = 1;
+                break;
+        }
+    }
+
+    protected function _setConnectionsConditions ($aParams)
+    {
+        $oConnection = isset($aParams['object']) ? BxDolConnection::getObjectInstance($aParams['object']) : false;
+        if (!$oConnection || !isset($aParams['profile']) || !(int)$aParams['profile'])
+            return false;
+
+        $sContentType = isset($aParams['type']) ? $aParams['type'] : BX_CONNECTIONS_CONTENT_TYPE_CONTENT;
+        $isMutual = isset($aParams['mutual']) ? $aParams['mutual'] : false;
+        $a = $oConnection->getConnectionsAsCondition ($sContentType, 'id', (int)$aParams['profile'], (int)$aParams['profile2'], $isMutual);
+
+        $this->aCurrent['restriction'] = array_merge($this->aCurrent['restriction'], $a['restriction']);
+        $this->aCurrent['join'] = array_merge($this->aCurrent['join'], $a['join']);
+
+        return true;
+    }
+    
+    protected function _setFamiliarConnectionsConditions ($aParams)
+    {
+        $oConnection = isset($aParams['object']) ? BxDolConnection::getObjectInstance($aParams['object']) : false;
+        if (!$oConnection || !isset($aParams['profile']) || !(int)$aParams['profile'])
+            return false;
+
+        $iProfile = (int)$aParams['profile'];
+        $sContentType = isset($aParams['type']) ? $aParams['type'] : BX_CONNECTIONS_CONTENT_TYPE_CONTENT;
+        $isMutual = isset($aParams['mutual']) ? $aParams['mutual'] : false;
+
+        $aIds = $oConnection->getConnectionsAsArray($sContentType, $iProfile, false, $isMutual);
+
+        $aCondition = $oConnection->getConnectionsAsCondition ($sContentType, 'id', $aIds, false, $isMutual);
+
+        $this->aCurrent['distinct'] = true;
+        $this->aCurrent['join'] = array_merge($this->aCurrent['join'], $aCondition['join']);
+        $this->aCurrent['restriction'] = array_merge($this->aCurrent['restriction'], $aCondition['restriction'], [
+            'connected' => [
+                'value' => array_merge([$iProfile], $aIds),
+                'field' => 'id',
+                'operator' => 'not in',
+            ]
+        ]);
+
+        return true;
+    }
+    
+    protected function _setConditionsForRecommended ()
+    {
+        $oConnection = BxDolConnection::getObjectInstance('sys_profiles_subscriptions'); 
+        $a = $oConnection->getConnectedContentAsSQLPartsExt ($this->aCurrent['table'], '', '');
+        
+        $aTmp = array(
+            'recommended' => array(
+            'type' => 'LEFT',
+            'table' =>  $a['join']['table'],
+            'mainField' => 'id` AND ' . bx_get_logged_profile_id() . ' = `' . $a['join']['table'] . '`.`initiator',
+            'onField' => 'content',
+            'joinFields' => array(),
+            )
+        );
+        $this->aCurrent['join'] = array_merge($this->aCurrent['join'], $aTmp);
+        
+        if (isset($this->aCurrent['restriction_sql'])) 
+            $this->aCurrent['restriction_sql'] .= ' AND `' . $a['join']['table'] . '`.`initiator` IS NULL AND ' . bx_get_logged_profile_id() . ' <> `sys_profiles`.`id` ' ;
+        else
+            $this->aCurrent['restriction_sql'] = ' AND `' . $a['join']['table'] . '`.`initiator` IS NULL AND ' . bx_get_logged_profile_id() . ' <> `sys_profiles`.`id` ' ;
+    }
+
+	protected function _setAclConditions ($aParams)
+    {
+        $oAcl = BxDolAcl::getInstance();
+        if(!$oAcl || empty($aParams['level']))
+            return false;
+
+		if(!is_array($aParams['level']))
+			$aParams['level'] = array($aParams['level']);
+
+		$this->aCurrent['title'] = array();
+		foreach($aParams['level'] as $iLevelId) {
+			$aInfo = $oAcl->getMembershipInfo($iLevelId);
+			if(empty($aInfo) || !is_array($aInfo))
+				continue;
+
+			$this->aCurrent['title'][] = _t($aInfo['name']);
+		}
+
+        $aCondition = $oAcl->getContentByLevelAsCondition('id', $aParams['level']);        
+        $this->aCurrent['restriction_sql'] = (!empty($this->aCurrent['restriction_sql']) ? $this->aCurrent['restriction_sql'] : '') . $aCondition['restriction_sql'];
+        $this->aCurrent['restriction'] = array_merge($this->aCurrent['restriction'], $aCondition['restriction']);
+        $this->aCurrent['join'] = array_merge($this->aCurrent['join'], $aCondition['join']);
+
+        return true;
+    }
+
+    protected function _setFavoriteConditions($sMode, $aParams, &$oProfileAuthor)
+    {
+        $CNF = &$this->oModule->_oConfig->CNF;
+
+        $oProfileAuthor = BxDolProfile::getInstance((int)$aParams['user']);
+        if(!$oProfileAuthor) 
+            return false;
+
+        $iListId = 0;
+        if(isset($aParams['list_id']))
+            $iListId = (int)$aParams['list_id'];
+
+        $iProfileAuthor = $oProfileAuthor->id();
+        $oFavorite = $this->oModule->getObjectFavorite();
+        if(!$oFavorite->isPublic() && $iProfileAuthor != bx_get_logged_profile_id()) 
+            return false;
+
+        $aConditions = $oFavorite->getConditionsTrack($CNF['TABLE_ENTRIES'], 'id', $iProfileAuthor, $iListId);
+        if(!empty($aConditions) && is_array($aConditions)) {
+            if(empty($this->aCurrent['restriction']) || !is_array($this->aCurrent['restriction']))
+                $this->aCurrent['restriction'] = array();
+            $this->aCurrent['restriction'] = array_merge($this->aCurrent['restriction'], $aConditions['restriction']);
+
+            if(empty($this->aCurrent['join']) || !is_array($this->aCurrent['join']))
+                $this->aCurrent['join'] = array();
+            $this->aCurrent['join'] = array_merge($this->aCurrent['join'], $aConditions['join']);
+        }
+
+        $this->sBrowseUrl = 'page.php?i=' . $CNF['URI_VIEW_FAVORITES'] . '&profile_id={profile_id}';
+        $this->aCurrent['title'] = _t($CNF['T']['txt_browse_favorites']);
+        $this->aCurrent['rss']['link'] = 'modules/?r=' . $this->oModule->_oConfig->getUri() . '/rss/' . $sMode . '/' . $iProfileAuthor;
+
+        return true;
+    }
+    
+    function decodeData ($a)
+    {
+        $CNF = $this->oModule->_oConfig->CNF;
+
+        $oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW']);
+        $bPrivacy = $oPrivacy !== false;
+
+        $oMetaMenu = !empty($CNF['OBJECT_MENU_SNIPPET_META']) ? BxDolMenu::getObjectInstance($CNF['OBJECT_MENU_SNIPPET_META'], $this->oModule->_oTemplate) : false;
+        $bMetaMenu = $oMetaMenu !== false;
+
+        $oContentInfo = $this->getContentInfoObject();
+
+        foreach ($a as $i => $r) {
+            if (isset($r['author']))
+                $a[$i]['author_data'] = BxDolProfile::getData($r[$CNF['FIELD_AUTHOR']]);
+
+            $a[$i]['url'] = $this->decodeDataUrl($oContentInfo, $r);
+            $a[$i]['image'] = $oContentInfo->getContentThumb($r['id']);
+            $a[$i]['cover'] = $oContentInfo->getContentCover($r['id']);
+            $a[$i]['summary_plain'] = $this->decodeDataSummaryPlain($oContentInfo, $r);
+
+            if($bMetaMenu) {
+                $bPublic = !$bPrivacy || $oPrivacy->check($r[$CNF['FIELD_ID']]) || $oPrivacy->isPartiallyVisible($r[$CNF['FIELD_ALLOW_VIEW_TO']]);
+                
+                $oMetaMenu->setContentId($r['id']);
+                $oMetaMenu->setContentPublic($bPublic);
+                $a[$i]['meta'] = $oMetaMenu->getCodeAPI();
+            }
+        }
+
+        return $a;
+    }
+	
+	function getItemPerPageInShowCase ()
+    {
+        $iPerPageInShowCase = parent::getItemPerPageInShowCase();
+        $CNF = &$this->oModule->_oConfig->CNF;
+		if ($this->bRecommendedView && isset($CNF['PARAM_PER_PAGE_BROWSE_RECOMMENDED'])){
+			$iPerPageInShowCase =  getParam($CNF['PARAM_PER_PAGE_BROWSE_RECOMMENDED']);
+        }
+        return $iPerPageInShowCase;
+    }
+    
+    function displayResultBlock ()
+    {
+		$this->oModule->_oTemplate->addJs(array('modules/base/profile/js/|searchresult.js'));
+		return parent::displayResultBlock ();
+	}
+}
+
+/** @} */

+ 757 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/profile/classes/BxBaseModProfileTemplate.php

@@ -0,0 +1,757 @@
+<?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 based modules representation.
+ */
+class BxBaseModProfileTemplate extends BxBaseModGeneralTemplate
+{
+    protected $_sUnitDefault;    
+    protected $_sUnitSizeDefault;
+
+    protected $_sUnitClass;
+    protected $_sUnitClassWithCover;
+    protected $_sUnitClassWoInfo;
+    protected $_sUnitClassWoInfoShowCase;
+    protected $_sUnitClassShowCase;
+
+    /*
+     * Enable/Disable Letter based thumbnail in "Unit with Cover" only.
+     */
+    protected $_bLetterAvatar;
+    
+    function __construct(&$oConfig, &$oDb)
+    {
+        parent::__construct($oConfig, $oDb);
+
+        $this->_sUnitDefault = 'unit.html';
+        $this->_sUnitSizeDefault = 'thumb';
+                
+        $this->_sUnitClass = 'bx-base-pofile-unit';
+        $this->_sUnitClassWithCover = 'bx-base-pofile-unit-with-cover';
+        $this->_sUnitClassWoInfo = 'bx-base-pofile-unit-wo-info';
+        $this->_sUnitClassWoInfoShowCase = 'bx-base-pofile-unit-wo-info bx-base-unit-showcase bx-base-pofile-unit-wo-info-showcase';
+        $this->_sUnitClassShowCase = 'bx-base-pofile-unit-with-cover bx-base-unit-showcase bx-base-pofile-unit-showcase';
+
+        $this->_bLetterAvatar = true;
+    }
+
+    public function addLocationBase()
+    {
+        parent::addLocationBase();
+
+        $this->addLocation('mod_profile', BX_DIRECTORY_PATH_MODULES . 'base' . DIRECTORY_SEPARATOR . 'profile' . DIRECTORY_SEPARATOR, BX_DOL_URL_MODULES . 'base/profile/');
+    }
+
+    /**
+     * Get profile unit
+     */
+    function unit ($aData, $isCheckPrivateContent = true, $mixedTemplate = false, $aParams = array())
+    {
+        list($sTemplate) = is_array($mixedTemplate) ? $mixedTemplate : array($mixedTemplate);
+
+        if(!empty($aParams['template_name']))
+            $sTemplate = $aParams['template_name'];
+        if(empty($sTemplate))
+            $sTemplate = $this->_sUnitDefault;
+        
+        /**
+         * Allow use separate template for private profiles. 
+         * These templates will be used if privacy field "Visible to" don't allow to view content.
+         * To use them you need to create a template with a postfix "_private" and put it in specified module
+         * Example: "unit_private.html" for "unit.html" or "unit_wo_links_private.html" for "unit_wo_links.html"
+         */ 
+        $oModule = $this->getModule();
+        if ($oModule->serviceCheckAllowedViewForProfile($aData) !== 0){
+            $sTemplatePrivate = str_replace('.html', '_private.html' , $sTemplate);
+            if($this->parseHtmlByName($sTemplatePrivate, array())){
+                $sTemplate = $sTemplatePrivate;
+            }
+        }
+
+        $aVars = $this->unitVars($aData, $isCheckPrivateContent, $mixedTemplate, $aParams);
+        
+        $aExtras = array(
+            'module' => $oModule->getName(),
+            'data' => $aData,
+            'check_private_content' => $isCheckPrivateContent,
+            'template' => $mixedTemplate,
+            'params' => $aParams,
+            'tmpl_name' => &$sTemplate,
+            'tmpl_vars' => &$aVars
+        );
+        bx_alert('profile', 'unit', 0, 0, $aExtras);
+        bx_alert($aExtras['module'], 'unit', 0, 0, $aExtras);
+ 
+        return $this->parseHtmlByName($sTemplate, $aVars);
+    }
+
+    function unitVars ($aData, $isCheckPrivateContent = true, $mixedTemplate = false, $aParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        list($sTemplate, $sTemplateSize, $aTemplateVars) = is_array($mixedTemplate) ? $mixedTemplate : array($mixedTemplate, false, array());
+
+        if(!empty($aParams['template_name']))
+            $sTemplate = $aParams['template_name'];
+        if(empty($sTemplate))
+            $sTemplate = $this->_sUnitDefault;
+
+        if(!empty($aParams['template_size']))
+            $sTemplateSize = $aParams['template_size'];
+        if(empty($sTemplateSize))
+            $sTemplateSize = $this->_getUnitSize($aData, $sTemplate);
+
+        if(!empty($aParams['template_vars']) && is_array($aParams['template_vars']))
+            $aTemplateVars = $aParams['template_vars'];
+        if(empty($aTemplateVars) || !is_array($aTemplateVars))
+            $aTemplateVars = array();
+
+        $oModule = $this->getModule();
+        $iContentId = (int)$aData[$CNF['FIELD_ID']];
+
+        $bPublic = $isCheckPrivateContent && !empty($CNF['OBJECT_PRIVACY_VIEW']) ? $this->isProfilePublic($aData) : true;
+
+        $bPublicThumb = true;
+        if($isCheckPrivateContent && $oModule->checkAllowedViewProfileImage($aData) !== CHECK_ACTION_RESULT_ALLOWED)
+            $bPublicThumb = false;
+
+        $bPublicCover = true;
+        if($isCheckPrivateContent && $oModule->checkAllowedViewCoverImage($aData) !== CHECK_ACTION_RESULT_ALLOWED)
+            $bPublicCover = false;
+
+        $oProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->MODULE);
+        $iProfile = $oProfile->id();
+
+        // get profile's url
+        $sUrl = bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $iContentId));
+
+        // get profile's title
+        $sTitle = $oModule->serviceProfileName($iContentId);
+
+        $sText = $sSummary = '';
+        if(!empty($CNF['FIELD_TEXT']) && !empty($aData[$CNF['FIELD_TEXT']])) {
+            $sText = $this->getText($aData);
+            $sSummary = $this->getSummary($aData, $sTitle, $sText, $sUrl);
+        }
+        
+        $sCoverUrl = $bPublicCover ? $this->urlCoverUnit($aData, false) : '';
+        $bCoverUrl = !empty($sCoverUrl);
+
+        if(empty($sCoverUrl) && ($iCoverId = (int)getParam('sys_unit_cover_profile')) != 0)
+            $sCoverUrl = BxDolTranscoder::getObjectInstance(BX_DOL_TRANSCODER_OBJ_COVER_UNIT_PROFILE)->getFileUrlById($iCoverId);
+        if(empty($sCoverUrl))
+            $sCoverUrl = $this->getImageUrl('cover.svg');
+        
+        $sThumbUrl = $bPublicThumb ? $this->_getUnitThumbUrl($sTemplateSize, $aData, false) : '';
+        $bThumbUrl = !empty($sThumbUrl);
+
+        if(substr($sTemplate, 0, 13) == 'unit_wo_cover' && !$bThumbUrl && $bCoverUrl) {
+            $bThumbUrl = true;
+            $sThumbUrl = $sCoverUrl;
+        }
+
+        $aTmplVarsThumbnail = array(
+            'class_size' => '',
+            'bx_if:show_thumb_image' => array(
+                'condition' => $bThumbUrl,
+                'content' => array(
+                    'class_size' => '',
+                    'size' => $sTemplateSize,
+                    'thumb_url' => $sThumbUrl
+                )
+            ),
+            'bx_if:show_thumb_letter' => array(
+                'condition' => !$bThumbUrl,
+                'content' => array(
+                    'class_size' => '',
+                    'size' => $sTemplateSize,
+                    'color' => implode(', ', BxDolTemplate::getColorCode($iProfile, 1.0)),
+                    'letter' => mb_strtoupper(mb_substr($sTitle, 0, 1))
+                )
+            ),
+            'bx_if:show_online' => array(
+                'condition' => $oProfile->isOnline(),
+                'content' => array()
+            ),
+            'size' => $sTemplateSize,
+            'badges' => $oModule->serviceGetBadges($iContentId, false, true),
+            'thumb_url' => $bThumbUrl ? $sThumbUrl : $this->getImageUrl('no-picture-thumb.png'),
+        );
+
+        $aTmplVarsMeta = array();
+        if(substr($sTemplate, 0, 8) != 'unit_wo_')
+            $aTmplVarsMeta = $this->getSnippetMenuVars ($iProfile, $bPublic);
+        
+        $sCoverData = isset($aData['cover_data']) ? $aData['cover_data'] : '';
+
+        return array_merge(array (
+            'class' => $this->_getUnitClass($aData, $sTemplate) . (!$bCoverUrl ? ' bx-cover-empty' : ''),
+            'id' => $iContentId,
+            'public' => $bPublic,
+            'bx_if:show_thumbnail' => array(
+                'condition' => $bThumbUrl || $this->_bLetterAvatar,
+                'content' => $aTmplVarsThumbnail
+            ),
+            'cover_url' => $sCoverUrl,
+            'cover_settings' => $this->_getImageSettings($sCoverData),
+            'bx_if:show_cover' => array(
+                'condition' => $bCoverUrl,
+                'content' => array(
+                    'cover_url' => $sCoverUrl,
+                    'cover_settins' => $this->_getImageSettings($sCoverData),
+                    'title' => $sTitle,
+                )
+            ),
+            'content_url' => $bPublic ? $sUrl : 'javascript:void(0);',
+            'content_click' => !$bPublic ? 'javascript:bx_alert(' . bx_js_string('"' . _t('_sys_access_denied_to_private_content') . '"') . ');' : '',
+            'title' => $sTitle,
+            'title_attr' => bx_html_attribute($sTitle),
+            'addon' => !empty($aData['addon']) ? $aData['addon'] : '',
+            'module_name' => _t($CNF['T']['txt_sample_single']),
+            'ts' => $aData[$CNF['FIELD_ADDED']],
+            'bx_if:meta' => array(
+                'condition' => !empty($aTmplVarsMeta),
+                'content' => $aTmplVarsMeta
+            ),
+            'text' => $sText,
+            'summary' => $sSummary,
+        ), $aTmplVarsThumbnail, $aTemplateVars);
+    }
+
+    function isProfilePublic($aData)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW']);
+        if ($oPrivacy && !$oPrivacy->check($aData[$CNF['FIELD_ID']]) && !$oPrivacy->isPartiallyVisible($aData[$CNF['FIELD_ALLOW_VIEW_TO']]))
+            return false;
+        return true;
+    }
+
+    function getSnippetMenuVars ($iProfileId, $bPublic = null)
+    {
+        if (!($oProfile = BxDolProfile::getInstance($iProfileId)))
+            return array();
+        
+        $CNF = &$this->_oConfig->CNF;
+        
+        if (null === $bPublic) {
+            $aData = $this->getModule()->serviceGetContentInfoById($oProfile->getContentId());
+            $bPublic = $this->isProfilePublic($aData);
+        }
+        
+        $aTmplVarsMeta = array();
+        if (!empty($CNF['OBJECT_MENU_SNIPPET_META'])) {
+            $oMenuMeta = BxDolMenu::getObjectInstance($CNF['OBJECT_MENU_SNIPPET_META'], $this);
+            if($oMenuMeta) {
+                $oMenuMeta->setContentId($oProfile->getContentId());
+                $oMenuMeta->setContentPublic($bPublic);
+                $aTmplVarsMeta = array(
+                    'meta' => $oMenuMeta->getCode()
+                );
+            }
+        }
+
+        return $aTmplVarsMeta;
+    }
+
+    function getBlockCover($aData, $aParams = [])
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aData[$CNF['FIELD_ID']], $this->MODULE);
+        if(!$oProfile)
+            return '';
+
+        $aTmplVars = $this->prepareCover($aData, array_merge($aParams, [
+            'use_as_block' => true
+        ]));
+    
+        $this->addCss(array('cover.css'));
+        
+        return $this->parseHtmlByName('cover_block.html', $aTmplVars);
+    }
+    
+    function prepareCover($aData, $aParams = [])
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $oModule = $this->getModule();
+        $sClass = isset($aParams['class']) ? $aParams['class'] : '';
+        $sShowData = isset($aParams['show_data']) ? $aParams['show_data'] : '';
+        $bShowCover = !isset($aParams['show_cover']) || $aParams['show_cover'] === true;
+        $bShowAvatar = !isset($aParams['show_avatar']) || $aParams['show_avatar'] === true;
+        $sAddCode = "";
+        
+
+        $bUseAsAuthor = isset($aParams['use_as_author']) && $aParams['use_as_author'] === true;
+        $bUseAsBlock = $bUseAsAuthor || (isset($aParams['use_as_block']) && $aParams['use_as_block'] === true);
+
+        if($bUseAsAuthor)
+            $sClass .= ' bx-base-author';
+        else
+            BxDolTemplate::getInstance()->addInjection('injection_main_class', 'text', 'bx-base-profile-view');
+
+        $bProfileViewAllowed = $oModule->checkAllowedView($aData) === CHECK_ACTION_RESULT_ALLOWED;
+
+        if($oModule->checkAllowedViewProfileImage($aData) !== CHECK_ACTION_RESULT_ALLOWED)
+            $aData[$CNF['FIELD_PICTURE']] = 0;
+
+        if($oModule->checkAllowedViewCoverImage($aData) !== CHECK_ACTION_RESULT_ALLOWED)
+            $aData[$CNF['FIELD_COVER']] = 0;
+
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aData[$CNF['FIELD_ID']], $this->MODULE);
+        $iProfile = $oProfile->id();
+
+        $sUrl = bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aData[$CNF['FIELD_ID']]));
+        $sTitle = bx_process_output($oProfile->getDisplayName());
+
+        //--- Process Cover
+        $bTmplVarsShowCover = $bShowCover;
+        $aTmplVarsShowCover = [];
+
+        if($bTmplVarsShowCover) {
+            $sCoverPopupId = $this->MODULE . '-popup-cover';
+            $bIsAllowEditCover = $oModule->checkAllowedChangeCover($aData) === CHECK_ACTION_RESULT_ALLOWED;
+
+            $oPage = false;
+            if(isset($aParams['page']) && is_a($aParams['page'], 'BxDolPage'))
+                $oPage = $aParams['page'];
+
+            $bUrlCover = false;
+            $sUrlCover = $this->urlCover($aData, false);
+            if(!$sUrlCover && $oPage !== false && $oPage->isPageCover()) {
+                $aCover = $oPage->getPageCoverImage();
+                
+                if(!empty($aCover))
+                    $sUrlCover = BxDolCover::getCoverImageUrl($aCover);
+            }
+
+            if(!empty($sUrlCover)) {
+                if(!$bUseAsAuthor)
+                    BxDolTemplate::getInstance()->addPageMetaImage($sUrlCover);
+
+                $bUrlCover = true;
+            }
+            else
+                $sUrlCover = $this->getImageUrl('cover.svg');
+
+            $sAddClassCover = "";
+            if(isset($CNF['FIELD_COVER']) && isset($CNF['OBJECT_UPLOADERS_COVER']) && isset($CNF['OBJECT_STORAGE_COVER']) && isset($CNF['OBJECT_IMAGES_TRANSCODER_COVER'])){
+                bx_alert('system', 'image_editor', 0, 0, array(
+                   'module' => $oModule->getName(),
+                   'image_type' => 'cover',
+                   'is_allow_edit' => $bIsAllowEditCover,
+                   'image_url' => $aData[$CNF['FIELD_COVER']] ? $sUrlCover : '',
+                   'content_id' => $aData[$CNF['FIELD_ID']],
+                   'uploader' => $CNF['OBJECT_UPLOADERS_COVER'][0],
+                   'storage' => $CNF['OBJECT_STORAGE_COVER'],
+                   'transcoder' => $CNF['OBJECT_IMAGES_TRANSCODER_COVER'],
+                   'field' => $CNF['FIELD_COVER'],
+                   'is_background' => true,
+                   'add_class' => &$sAddClassCover,
+                   'add_code' => &$sAddCode
+                ));
+            }
+            
+            $sCoverTweak = '';
+            $sUniqIdCover = genRndPwd (8, false);
+            if ($bIsAllowEditCover && empty($sAddCode)){
+                $sCoverTweak = $this->_prepareImage($aData, $sUniqIdCover, $CNF['OBJECT_UPLOADERS_COVER'], $CNF['OBJECT_STORAGE_COVER'], $CNF['FIELD_COVER'], true);
+            }
+            
+            $aTmplVarsShowCover = [
+                'cover_popup_id' => $sCoverPopupId,
+                'cover_url' => $sUrlCover,
+                'unique_id' => $sUniqIdCover,
+                'cover_settins' => isset($CNF['FIELD_COVER_POSITION']) ? $this->_getImageSettings($aData[$CNF['FIELD_COVER_POSITION']]) : '',
+                'add_class' => $sAddClassCover,
+                'img_class' => $sAddClassCover != '' ? 'bx-media-editable-src' : '',
+            ];
+        }
+        else
+            $sClass .= ' bx-no-cover';
+
+        //--- Process Avatar
+        $sUrlAvatar = $this->urlAvatarBig($aData, false);
+        $bUrlAvatar = !empty($sUrlAvatar);
+
+        $bTmplVarsShowAvatar = $bShowAvatar && ($bUrlAvatar || $this->_bLetterAvatar);
+        $aTmplVarsShowAvatar = [];
+        $sPicturePopup = '';
+
+        if($bTmplVarsShowAvatar) {
+            $sPicturePopupId = $this->MODULE . '-popup-picture';
+            $bIsAllowEditPicture = $oModule->checkAllowedEdit($aData) === CHECK_ACTION_RESULT_ALLOWED;
+
+            $sUrlPicture = $this->urlPicture ($aData);
+            $sUrlPictureChange = bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_EDIT_ENTRY'] . '&id=' . $aData[$CNF['FIELD_ID']]));
+
+            $sAddClassPicture = "";
+            if(isset($CNF['FIELD_PICTURE']) && isset($CNF['OBJECT_UPLOADERS_PICTURE']) && isset($CNF['OBJECT_STORAGE']) && isset($CNF['OBJECT_IMAGES_TRANSCODER_THUMB'])){
+                bx_alert('system', 'image_editor', 0, 0, array(
+                   'module' => $oModule->getName(),
+                   'image_type' => 'avatar',
+                   'is_allow_edit' => $bIsAllowEditPicture,
+                   'image_url' =>  $aData[$CNF['FIELD_PICTURE']] ? $sUrlPicture : '',
+                   'content_id' => $aData[$CNF['FIELD_ID']],
+                   'uploader' => $CNF['OBJECT_UPLOADERS_PICTURE'][0],
+                   'storage' => $CNF['OBJECT_STORAGE'],
+                   'transcoder' => $CNF['OBJECT_IMAGES_TRANSCODER_AVATAR_BIG'],
+                   'field' => $CNF['FIELD_PICTURE'],
+                   'is_background' => false,
+                   'add_class' => &$sAddClassPicture,
+                   'add_code' => &$sAddCode
+                )); 
+            }
+            
+            $sPictureTweak = '';
+            $sUniqIdPicture = genRndPwd (8, false);
+            if ($bIsAllowEditPicture && empty($sAddCode)){
+                $sPictureTweak = $this->_prepareImage($aData, $sUniqIdPicture, $CNF['OBJECT_UPLOADERS_PICTURE'], $CNF['OBJECT_STORAGE'], $CNF['FIELD_PICTURE'], false);
+            }
+
+            $aTmplVarsShowAvatar = array(
+                'add_class' => $sAddClassPicture,
+                'letter' => mb_strtoupper(mb_substr($sTitle, 0, 1)),
+                'img_class' => $sAddClassPicture != '' ? 'bx-media-editable-src' : '',
+                'ava_url' => $sUrlAvatar,
+                'color' => implode(', ', BxDolTemplate::getColorCode($iProfile, 1.0)),
+                
+                'bx_if:show_ava_image' => array(
+                    'condition' => $bUrlAvatar,
+                    'content' => []
+                ),
+                'bx_if:show_ava_letter' => array(
+                    'condition' => !$bUrlAvatar,
+                    'content' => []
+                ),
+                'bx_if:show_online' => array(
+                    'condition' => $oProfile->isOnline(),
+                    'content' => []
+                ),
+                'bx_if:is_avatar' => array(
+                    'condition' => $bUrlAvatar,
+                    'content' => [
+                        'picture_popup_id' => $sPicturePopupId,
+                        'picture_url' => $sUrlPicture,
+                    ]
+                ),
+                'picture_avatar_url' => $bUrlAvatar ? $sUrlAvatar : $this->getImageUrl('no-picture-preview.png'),
+                'unique_id' => $sUniqIdPicture,
+                'picture_tweak' => $sPictureTweak, 
+                'cover_settins' => isset($CNF['FIELD_PICTURE_POSITION']) ? $this->_getImageSettings($aData[$CNF['FIELD_PICTURE_POSITION']]) : '',
+                'picture_href' => !$aData[$CNF['FIELD_PICTURE']] && CHECK_ACTION_RESULT_ALLOWED === $oModule->checkAllowedEdit($aData) ? $sUrlPictureChange : 'javascript:void(0);',
+            );
+            
+            if($bProfileViewAllowed && $aData[$CNF['FIELD_PICTURE']]) {
+                $sPicturePopup = BxTemplFunctions::getInstance()->transBox($sPicturePopupId, $this->parseHtmlByName('image_popup.html', array (
+                    'image_url' => $sUrlPicture,
+                    'bx_if:owner' => array (
+                        'condition' => false,
+                        'content' => array (
+                            'change_image_url' => $sUrlPictureChange,
+                        ),
+                    ),
+                )), true, true);
+            }
+        }
+
+        //--- Process Actions menu
+        $sActionsMenu = '';
+        if(!isset($aParams['show_menu_actions']) || $aParams['show_menu_actions'] === true) {
+            $sActionsMenu = $oModule->serviceEntityAllActions([$aData[$CNF['FIELD_ID']], $aData]);
+            if(!$sActionsMenu && ($oMenu = BxTemplMenu::getObjectInstance($CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY'], $this)) !== false)
+                $sActionsMenu = $oMenu->getCode();
+        }
+
+        //--- Process Meta menu
+        $sMetaMenu = '';
+        if((!isset($aParams['show_menu_meta']) || $aParams['show_menu_meta'] === true) && !empty($CNF['OBJECT_MENU_VIEW_ENTRY_META'])) {
+            $oMetaMenu = BxTemplMenu::getObjectInstance($CNF['OBJECT_MENU_VIEW_ENTRY_META']);
+            if($oMetaMenu !== false) {
+                $oMetaMenu->setContentId($aData[$CNF['FIELD_ID']]);
+                $oMetaMenu->setContentPublic($this->isProfilePublic($aData));
+                $sMetaMenu = $oMetaMenu->getCode();
+            }
+        }
+
+        $aTmplVars = [
+            'module' => $this->_oConfig->getName(),
+            'class' => trim($sClass),
+            'id' => $aData[$CNF['FIELD_ID']],
+            'content_url' => $sUrl,
+            
+            'title' => $sTitle,
+            'bx_if:show_title_as_tag' => [
+                'condition' => !$bUseAsAuthor,
+                'content' => [
+                    'title' => $sTitle
+                ]
+            ],
+            'bx_if:show_title_as_text' => [
+                'condition' => $bUseAsAuthor,
+                'content' => [
+                    'title' => $sTitle
+                ]
+            ],
+            'bx_if:show_avatar' => [
+                'condition' => $bTmplVarsShowAvatar,
+                'content' => $aTmplVarsShowAvatar
+            ],
+            'bx_if:show_avatar_placeholder' => [
+                'condition' => !$bTmplVarsShowAvatar,
+                'content' => []
+            ],
+            'badges' => $oModule->serviceGetBadges($aData[$CNF['FIELD_ID']]),
+            'action_menu' => $sActionsMenu,
+            'meta' => $sMetaMenu,
+            'show_data' => $sShowData,
+            'picture_popup' => $sPicturePopup,
+            'additional_code' => $sAddCode,
+            'cover_tweak' => $sCoverTweak,
+        ];
+        
+        $bShowClickable = !isset($aParams['show_clickable']) || $aParams['show_clickable'] === true; //--- Is available for UseAsBlock appearance only.
+        
+        if($bUseAsBlock && $bShowClickable)
+            $sClass .= ' bx-clickable';
+
+        if($bUseAsBlock)
+            $aTmplVars = array_merge($aTmplVars, [
+                'bx_if:show_clickable' => [
+                    'condition' => $bShowClickable,
+                    'content' => [
+                        'content_url' => $sUrl
+                    ]
+                ],
+                'bx_if:show_cover' => [
+                    'condition' => $bTmplVarsShowCover,
+                    'content' => $aTmplVarsShowCover
+                ],
+                'bx_if:show_text' => [
+                    'condition' => !isset($aParams['show_text']) || $aParams['show_text'] === true,
+                    'content' => [
+                        'text' => isset($CNF['FIELD_TEXT']) ? strmaxtextlen(strip_tags($aData[$CNF['FIELD_TEXT']]), 150) : ''
+                    ]
+                ]
+            ]);
+        else
+            $aTmplVars = array_merge($aTmplVars, $aTmplVarsShowCover, [
+                'bx_if:show_cover' => [
+                    'condition' => $bUrlCover,
+                    'content' => []
+                ],
+                'bx_if:show_cover_placeholder' => [
+                    'condition' => !$bUrlCover,
+                    'content' => []
+                ],
+            ]);
+
+        return $aTmplVars;
+    }
+    /**
+     * Get profile cover
+     */
+    function setCover ($oPage, $aData, $sTemplateName = 'cover.html')
+    {
+        BxDolCover::getInstance($this)->set($this->prepareCover($aData, ['page' => $oPage]), $sTemplateName);
+    }
+
+    /**
+     * Get profile picture thumb url
+     * @deprecated since 11.0.0 use BxBaseModProfileTemplate::urlAvatar instead.
+     */
+    function avatar ($aData, $bSubstituteNoImage = true)
+    {
+        return $this->urlAvatar($aData, $bSubstituteNoImage);
+    }
+
+    /**
+     * Get profile picture thumb url
+     * @deprecated since 11.0.0 use BxBaseModProfileTemplate::urlThumb instead.
+     */
+    function thumb ($aData, $bSubstituteNoImage = true)
+    {
+        return $this->urlThumb($aData, $bSubstituteNoImage);
+    }
+
+    /**
+     * Get profile picture thumb url
+     * @deprecated since 11.0.0 use BxBaseModProfileTemplate::urlIcon instead.
+     */
+    function icon ($aData, $bSubstituteNoImage = true)
+    {
+        return $this->urlIcon($aData, $bSubstituteNoImage);
+    }
+
+    /**
+     * Get profile picture icon url
+     */
+    function urlIcon ($aData, $bSubstituteNoImage = true)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        return $this->_image ($CNF['FIELD_PICTURE'], $CNF['OBJECT_IMAGES_TRANSCODER_ICON'], 'no-picture-icon.png', $aData, $bSubstituteNoImage);
+    }
+
+    /**
+     * Get profile thumb url
+     */
+    function urlThumb ($aData, $bSubstituteNoImage = true)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        return $this->_image ($CNF['FIELD_PICTURE'], $CNF['OBJECT_IMAGES_TRANSCODER_THUMB'], 'no-picture-thumb.png', $aData, $bSubstituteNoImage);
+    }
+
+    /**
+     * Get profile avatar url
+     */
+    function urlAvatar ($aData, $bSubstituteNoImage = true)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        return $this->_image ($CNF['FIELD_PICTURE'], $CNF['OBJECT_IMAGES_TRANSCODER_AVATAR'], 'no-picture-preview.png', $aData, $bSubstituteNoImage);
+    }
+
+    /**
+     * Get profile avatar big url
+     */
+    function urlAvatarBig ($aData, $bSubstituteNoImage = true)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        return $this->_image ($CNF['FIELD_PICTURE'], $CNF['OBJECT_IMAGES_TRANSCODER_AVATAR_BIG'], 'no-picture-preview.png', $aData, $bSubstituteNoImage);
+    }
+
+    /**
+     * Get profile picture url
+     */
+    function urlPicture ($aData, $bSubstituteNoImage = true)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        return $this->_image ($CNF['FIELD_PICTURE'], $CNF['OBJECT_IMAGES_TRANSCODER_PICTURE'], 'no-picture-preview.png', $aData, $bSubstituteNoImage);
+    }
+
+    /**
+     * Get profile cover image url
+     */
+    function urlCover ($aData, $bSubstituteNoImage = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sImageUrl = $this->_image($CNF['FIELD_COVER'], $CNF['OBJECT_IMAGES_TRANSCODER_COVER'], '', $aData, $bSubstituteNoImage);
+
+        return $sImageUrl;
+    }
+
+    /**
+     * Get profile cover image url for browse unit
+     */
+    function urlCoverUnit ($aData, $bSubstituteNoImage = true)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $sImageUrl = $this->_image ($CNF['FIELD_COVER'], $CNF['OBJECT_IMAGES_TRANSCODER_GALLERY'], '', $aData, false);
+
+        if(!$bSubstituteNoImage || $sImageUrl)
+            return $sImageUrl;
+
+        $iImageId = (int)getParam('sys_unit_cover_profile');
+        $oImageTranscoder = BxDolTranscoderImage::getObjectInstance(BX_DOL_TRANSCODER_OBJ_COVER_UNIT_PROFILE);
+        if($oImageTranscoder && $iImageId != 0)
+            $sImageUrl = $oImageTranscoder->getFileUrl($iImageId);
+
+        if(!$sImageUrl)
+            $sImageUrl = $this->getImageUrl('cover.svg');
+
+        return $sImageUrl;
+    }
+
+    /**
+     * Get profile picture icon url
+     */
+    function _image ($sField, $sTranscodeObject, $sNoImage, $aData, $bSubstituteNoImage = true)
+    {
+        $sImageUrl = false;
+        if ($aData[$sField]) {
+            $oImagesTranscoder = BxDolTranscoderImage::getObjectInstance($sTranscodeObject);
+            if ($oImagesTranscoder)
+                $sImageUrl = $oImagesTranscoder->getFileUrl($aData[$sField]);
+        }
+
+        if(!$bSubstituteNoImage || $sImageUrl)
+            return $sImageUrl;
+
+        $sImageUrl = $this->getImageUrl($sNoImage);
+        if(!$sImageUrl)
+            $sImageUrl = bx_is_api() ? '' : $this->getImageUrl(substr($sNoImage, 0, strrpos($sNoImage, '-')) . '.svg');
+
+        return $sImageUrl;
+    }
+
+    protected function _getUnitClass($aData, $sTemplateName = 'unit.html')
+    {
+        $sResult = '';
+
+        switch($sTemplateName) {
+            default:
+            case 'unit.html':
+                $sResult = $this->_sUnitClass;
+                break;
+
+            case 'unit_with_cover.html':
+                $sResult = $this->_sUnitClassWithCover;
+                break;
+
+            case 'unit_wo_info.html':
+            case 'unit_wo_info_links.html':
+                $sResult = $this->_sUnitClassWoInfo;
+                break;
+                
+            case 'unit_with_cover_showcase.html':
+                $sResult = $this->_sUnitClassShowCase;
+                break;
+
+            case 'unit_wo_info_showcase.html':
+                $sResult = $this->_sUnitClassWoInfoShowCase;
+                break;
+        }
+
+        return $sResult;
+    }
+
+    protected function _getUnitSize($aData, $sTemplateName = 'unit.html')
+    {
+        $sResult = '';
+
+        switch($sTemplateName) {
+            case 'unit_with_cover.html':
+            case 'unit_with_cover_showcase.html':
+                $sResult = 'ava';
+                break;
+
+            default:
+                $sResult = $this->_sUnitSizeDefault;
+                break;
+        }
+
+        return $sResult;
+    }
+
+    protected function _getUnitThumbUrl($sSize, $aData, $bSubstituteNoImage = true)
+    {
+        $sMethod = 'url' . bx_gen_method_name(str_replace('ava', 'avatar', $sSize), array('_', '-'));
+
+        if(method_exists($this, $sMethod))
+            return $this->$sMethod($aData, $bSubstituteNoImage);
+        else
+            return $this->_getUnitThumbUrl($this->_sUnitSizeDefault, $aData, $bSubstituteNoImage);
+    }
+}
+
+/** @} */

+ 1099 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/text/classes/BxBaseModTextModule.php

@@ -0,0 +1,1099 @@
+<?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);
+
+        $sHeader = 'Content-Type';
+        $aHeaders = @get_headers($sUrl, 1);
+        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('_bx_posts_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()
+	{
+		bx_import('SearchResult', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'SearchResult';
+        $o = new $sClass();
+        $o->fillFilters(array(
+			'author' => bx_get_logged_profile_id()
+        ));
+        $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(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 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'] : _t('_Empty');
+        $sDescription = !empty($aSiteInfo['description']) ? $aSiteInfo['description'] : _t('_Empty');
+
+        $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_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() . '_' . (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;
+    }
+
+
+    // ====== 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;
+    }
+}
+
+/** @} */

+ 279 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/text/classes/BxBaseModTextSearchResult.php

@@ -0,0 +1,279 @@
+<?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
+ *
+ * @{
+ */
+
+class BxBaseModTextSearchResult extends BxBaseModGeneralSearchResult
+{
+    protected $sUnitViewParamName = 'unit_view';
+
+    function __construct($sMode = '', $aParams = array())
+    {
+        parent::__construct($sMode, $aParams);
+        $this->aUnitViews = array('extended' => 'unit.html', 'gallery' => 'unit_gallery.html', 'full' => 'unit_full.html', 'showcase' => 'unit_showcase.html');
+		if (!empty($aParams['unit_views']))
+            $this->aUnitViews = $aParams['unit_views'];
+        if (!empty($aParams['unit_view']))
+            $this->sUnitViewDefault = $aParams['unit_view'];
+        $this->aGetParams = array($this->sUnitViewParamName);
+        $this->sUnitTemplate = $this->aUnitViews[$this->sUnitViewDefault];
+        if (isset($this->aUnitViews[bx_get($this->sUnitViewParamName)]))
+            $this->sUnitTemplate = $this->aUnitViews[bx_get($this->sUnitViewParamName)];
+		$this->addContainerClass ('bx-def-margin-bottom-neg');
+        if ('unit_gallery.html' == $this->sUnitTemplate){
+			$this->removeContainerClass ('bx-def-margin-bottom-neg');		
+            $this->addContainerClass (array('bx-base-text-unit-gallery-wrapper', 'bx-def-margin-sec-neg'));
+		}
+		if ('unit_showcase.html' == $this->sUnitTemplate){
+			$this->bShowcaseView = true;
+		}
+    }
+
+    protected function checkRestrictionsForContext($sMode, $aParams, $oProfileAuthor){
+        if(bx_get('context_id')){
+            $aParams['context'] = bx_get('context_id');
+            $oProfileAuthor = null;
+            $this->_updateCurrentForContext($sMode, $aParams, $oProfileAuthor);
+        }
+    }
+    
+    function getAlterOrder()
+    {
+        $CNF = &$this->oModule->_oConfig->CNF;
+
+        $sTable = $this->aCurrent['table'];
+
+        $aSql = array();
+        switch ($this->aCurrent['sorting']) {
+            case 'last':
+                $aSql['order'] = ' ORDER BY `' . $sTable . '`.`' . $CNF['FIELD_ADDED'] . '` DESC';
+                break;
+
+            case 'updated':
+                $aSql['order'] = ' ORDER BY `' . $sTable . '`.`' . $CNF['FIELD_CHANGED'] . '` DESC';
+                break;
+
+            case 'featured':
+                $aSql['order'] = ' ORDER BY `' . $sTable . '`.`featured` DESC';
+                break;
+
+            case 'popular':
+                $aSql['order'] = ' ORDER BY `' . $sTable . '`.`' . $CNF['FIELD_VIEWS'] . '` DESC';
+                break;
+
+            case 'top':
+                $aSql['order'] = '';
+
+                $aPartsUp = $aPartsDown = array(0);
+                if(!empty($CNF['OBJECT_VOTES']) && ($oVote = BxDolVote::getObjectInstance($CNF['OBJECT_VOTES'], 0, false)) !== false && $oVote->isEnabled()) {
+                    $aVote = $oVote->getSystemInfo();
+                    if(!empty($aVote['trigger_table']) && !empty($aVote['trigger_field_count']))
+                        $aPartsUp[] = '`' . $aVote['trigger_table'] . '`.`' . $aVote['trigger_field_count'] . '`';
+                }
+
+                if(!empty($CNF['OBJECT_REACTIONS']) && ($oReaction = BxDolVote::getObjectInstance($CNF['OBJECT_REACTIONS'], 0, false)) !== false && $oReaction->isEnabled()) {
+                    $aReaction = $oReaction->getSystemInfo();
+                    if(!empty($aReaction['trigger_table']) && !empty($aReaction['trigger_field_count']))
+                        $aPartsUp[] = '`' . $aReaction['trigger_table'] . '`.`' . $aReaction['trigger_field_count'] . '`';
+                }
+
+                if(!empty($CNF['OBJECT_SCORES']) && ($oScore = BxDolScore::getObjectInstance($CNF['OBJECT_SCORES'], 0, false)) !== false && $oScore->isEnabled()) {
+                    $aScore = $oScore->getSystemInfo();
+                    if(!empty($aScore['trigger_table']) && !empty($aScore['trigger_field_cup']) && !empty($aScore['trigger_field_cdown'])) {
+                        $aPartsUp[] = '`' . $aScore['trigger_table'] . '`.`' . $aScore['trigger_field_cup'] . '`';
+                        $aPartsDown[] = '`' . $aScore['trigger_table'] . '`.`' . $aScore['trigger_field_cdown'] . '`';
+                    }
+                }
+
+                if(empty($aPartsUp) && empty($aPartsDown))
+                    break;
+
+                $aSql['order'] = ' ORDER BY ' . pow(10, 8) . ' * ((' . implode(' + ', $aPartsUp) . ') - (' . implode(' + ', $aPartsDown) . ')) / (UNIX_TIMESTAMP() - `' . $sTable . '`.`' . $CNF['FIELD_ADDED'] . '`) DESC';
+                break;
+        }
+
+        return $aSql;
+    }
+
+    function getRssUnitImage (&$a, $sField)
+    {
+        $CNF = &$this->oModule->_oConfig->CNF;
+
+        if(empty($sField) || empty($a[$sField]) || empty($CNF['OBJECT_IMAGES_TRANSCODER_GALLERY']))
+            return '';
+
+        $oTranscoder = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_GALLERY']);
+        if(!$oTranscoder)
+            return '';
+
+        return $oTranscoder->getFileUrl($a[$sField]);      
+    }
+
+    protected function processReplaceableMarkers($oProfileAuthor) 
+    {
+        if ($oProfileAuthor) {
+            $this->addMarkers($oProfileAuthor->getInfo()); // profile info is replaceable
+            $this->addMarkers(array('profile_id' => $oProfileAuthor->id())); // profile id is replaceable
+            $this->addMarkers(array('display_name' => $oProfileAuthor->getDisplayName())); // profile display name is replaceable
+        }
+
+        $this->sBrowseUrl = $this->_replaceMarkers($this->sBrowseUrl);
+        $this->aCurrent['title'] = $this->_replaceMarkers($this->aCurrent['title']);
+    }
+
+    protected function getCurrentOnclick($aAdditionalParams = array(), $bReplacePagesParams = true) 
+    {
+        // always add UnitView as additional param
+        $sUnitView = bx_process_input(bx_get($this->sUnitViewParamName));
+        if ($sUnitView && isset($this->aUnitViews[$sUnitView]))
+            $aAdditionalParams = array_merge(array($this->sUnitViewParamName => $sUnitView), $aAdditionalParams);
+
+        return parent::getCurrentOnclick($aAdditionalParams, $bReplacePagesParams);
+    }
+
+    protected function _updateCurrentForAuthor($sMode, $aParams, &$oProfileAuthor)
+    {
+        $CNF = &$this->oModule->_oConfig->CNF;
+
+        $oProfileAuthor = BxDolProfile::getInstance((int)$aParams['author']);
+        if (!$oProfileAuthor) 
+            return false;
+
+        $iProfileAuthor = $oProfileAuthor->id();
+        if (bx_get_logged_profile_id() == $iProfileAuthor || $this->oModule->_isModerator()) {
+            // for real owner and for moderators show anonymous posts
+            $this->aCurrent['restriction']['author']['operator'] = 'in';
+            $this->aCurrent['restriction']['author']['value'] = array($iProfileAuthor, -$iProfileAuthor);
+        } 
+        else {
+           $this->aCurrent['restriction']['author']['value'] = $iProfileAuthor;
+        }
+
+        if(!empty($aParams['except']))
+        	$this->aCurrent['restriction']['except']['value'] = is_array($aParams['except']) ? $aParams['except'] : array($aParams['except']); 
+
+        if(!empty($aParams['per_page']))
+        	$this->aCurrent['paginate']['perPage'] = is_numeric($aParams['per_page']) ? (int)$aParams['per_page'] : (int)getParam($aParams['per_page']);
+
+        $this->sBrowseUrl = 'page.php?i=' . $CNF['URI_AUTHOR_ENTRIES'] . '&profile_id={profile_id}';
+        $this->aCurrent['title'] = _t($CNF['T']['txt_all_entries_by_author']);
+        $this->aCurrent['rss']['link'] = 'modules/?r=' . $this->oModule->_oConfig->getUri() . '/rss/' . $sMode . '/' . $iProfileAuthor;
+
+        return true;
+    }
+
+    protected function _updateCurrentForContext($sMode, $aParams, &$oProfileContext)
+    {
+        $CNF = &$this->oModule->_oConfig->CNF;
+            
+        $oProfileContext = BxDolProfile::getInstance((int)$aParams['context']);
+        if (!$oProfileContext) 
+            return false;
+
+        $iProfileIdContext = $oProfileContext->id();
+        $this->aCurrent['restriction']['context'] = array(
+            'value' => -$iProfileIdContext,
+            'field' => $CNF['FIELD_ALLOW_VIEW_TO'],
+            'operator' => '=',
+        );
+
+        if(!empty($aParams['per_page']))
+        	$this->aCurrent['paginate']['perPage'] = is_numeric($aParams['per_page']) ? (int)$aParams['per_page'] : (int)getParam($aParams['per_page']);
+
+        $this->sBrowseUrl = 'page.php?i=' . $CNF['URI_ENTRIES_BY_CONTEXT'] . '&profile_id={profile_id}';
+        $this->aCurrent['title'] = _t($CNF['T']['txt_all_entries_by_context']);
+        $this->aCurrent['rss']['link'] = 'modules/?r=' . $this->oModule->_oConfig->getUri() . '/rss/' . $sMode . '/' . $iProfileIdContext;
+
+        return true;
+    }
+    
+    protected function _updateCurrentForFavorite($sMode, $aParams, &$oProfileAuthor)
+    {
+        $CNF = &$this->oModule->_oConfig->CNF;
+        
+        $sSystem = '';
+        $iListId = 0;
+        
+        if(!empty($aParams['system'])) {
+            $sSystem = $aParams['system'];
+            unset($aParams['system']);
+        }
+        
+        if(!empty($aParams['list_id'])) {
+            $iListId = $aParams['list_id'];
+            unset($aParams['list_id']);
+        }
+
+        $oProfileAuthor = BxDolProfile::getInstance((int)$aParams['user']);
+        if(!$oProfileAuthor) 
+            return false;
+
+        $iProfileAuthor = $oProfileAuthor->id();
+        $oFavorite = $this->oModule->getObjectFavorite($sSystem);
+        if(!$oFavorite->isPublic() && $iProfileAuthor != bx_get_logged_profile_id()) 
+            return false;
+
+        $aConditions = $oFavorite->getConditionsTrack($this->aCurrent['table'], 'id', $iProfileAuthor, $iListId);
+        if(!empty($aConditions) && is_array($aConditions)) {
+            if(empty($this->aCurrent['restriction']) || !is_array($this->aCurrent['restriction']))
+                $this->aCurrent['restriction'] = array();
+            $this->aCurrent['restriction'] = array_merge($this->aCurrent['restriction'], $aConditions['restriction']);
+
+            if(empty($this->aCurrent['join']) || !is_array($this->aCurrent['join']))
+                $this->aCurrent['join'] = array();
+            $this->aCurrent['join'] = array_merge($this->aCurrent['join'], $aConditions['join']);
+        }
+        
+        $this->sBrowseUrl = 'page.php?i=' . $CNF['URI_AUTHOR_ENTRIES'] . '&profile_id={profile_id}';
+        $this->aCurrent['title'] = _t($CNF['T']['txt_all_entries_by_author']);
+        $this->aCurrent['rss']['link'] = 'modules/?r=' . $this->oModule->_oConfig->getUri() . '/rss/' . $sMode . '/' . $iProfileAuthor;
+
+        return true;
+    }
+    
+    function decodeData ($a)
+    {
+        $CNF = &$this->oModule->_oConfig->CNF;
+
+        $oMetaMenu = !empty($CNF['OBJECT_MENU_SNIPPET_META']) ? BxDolMenu::getObjectInstance($CNF['OBJECT_MENU_SNIPPET_META'], $this->oModule->_oTemplate) : false;
+        $bMetaMenu = $oMetaMenu !== false;
+
+        $oContentInfo = $this->getContentInfoObject();
+
+        foreach ($a as $i => $r) {
+            if (isset($r['author']))
+                $a[$i]['author_data'] = BxDolProfile::getData($r[$CNF['FIELD_AUTHOR']]);
+
+            $a[$i]['url'] = $this->decodeDataUrl($oContentInfo, $r);
+            $a[$i]['image'] = $oContentInfo->getContentThumb($r['id']);
+            $a[$i]['summary_plain'] = $this->decodeDataSummaryPlain($oContentInfo, $r);
+
+            if($bMetaMenu) {
+                $oMetaMenu->setContentId($r['id']);
+                $a[$i]['meta'] = $oMetaMenu->getCodeAPI();
+            }
+        }
+
+        return $a;
+    }
+
+    function _getPseud ()
+    {
+        return array(
+            'id' => 'id',
+            'title' => 'title',
+            'text' => 'text',
+            'added' => 'added',
+            'author' => 'author',
+            'photo' => 'photo',
+        );
+    }
+}
+
+/** @} */

+ 1102 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/text/classes/BxBaseModTextTemplate.php

@@ -0,0 +1,1102 @@
+<?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']];
+        
+        $oProfile = BxDolProfile::getInstanceMagic($iProfileId);
+        $sName = $oProfile->getDisplayName();
+        $sAddon = $sFuncAuthorAddon && is_a($oProfile, 'BxDolProfile') ? $this->$sFuncAuthorAddon($aData, $oProfile) : '';        
+
+        $bIsApi = bx_is_api();
+        if($bIsApi)
+            $sFuncAuthorDesc .= 'API';
+
+        $aVars = [
+            'author_url' => $oProfile->getUrl(),
+            'author_thumb_url' => $oProfile->getThumb(),
+            'author_unit' => $bIsApi ? BxDolProfile::getData($oProfile, ['display_type' => 'unit_wo_info']) : $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,
+                ],
+            ],
+        ];
+
+        return $bIsApi ? [bx_api_get_block('entity_author', [
+            'author' => $aVars, 
+            'menu_manage' => $oModule->getEntryAllActions()
+        ])] : $this->parseHtmlByName($sTemplateName, $aVars);
+    }
+
+    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']);
+
+            $aTmplVarsItems[] = array(
+                'url' => $oCategory->getCategoryUrl($aContentInfo[$CNF['FIELD_CATEGORY']]),
+                '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 = strmaxtextlen($aContentInfo[$CNF['FIELD_TITLE']], 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()->_oDb->getEntriesNumByAuthor($oProfile->id()))
+                )
+            )));
+
+        return $this->parseHtmlByName('author_desc.html', array(
+            'bx_repeat:items' => $aTmplVarsItems
+        ));
+    }
+
+    function getAuthorDescAPI($aData, $oProfile)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        $aResult = ['object' => '', 'items' => []];
+        if(!empty($CNF['FIELD_ADDED']) && !empty($aData[$CNF['FIELD_ADDED']]))
+            $aResult['items'][] = [
+                'id' => 1,
+                'content_type' => 'time',
+                'title' => $aData[$CNF['FIELD_ADDED']]
+            ];
+
+        if(!empty($CNF['URI_AUTHOR_ENTRIES']))
+            $aResult['items'][] = [
+                'id' => 2,
+                'link' => BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_AUTHOR_ENTRIES'] . '&profile_id=' . $oProfile->id()),
+                'title' => _t($CNF['T']['txt_all_entries_by'], $this->getModule()->_oDb->getEntriesNumByAuthor($oProfile->id()))
+            ];
+
+        return $aResult;
+    }
+
+    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 = bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $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 = array();
+        if(!empty($CNF['OBJECT_MENU_SNIPPET_META'])) {
+            $oMenuMeta = BxDolMenu::getObjectInstance($CNF['OBJECT_MENU_SNIPPET_META'], $this);
+            if($oMenuMeta) {
+                $oMenuMeta->setContentId($aData[$CNF['FIELD_ID']]);
+                $aTmplVarsMeta = array(
+                    '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'] : '')),
+            'id' => $aData[$CNF['FIELD_ID']],
+            'content_url' => $sUrl,
+            '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,
+                    '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,
+                    'summary_plain' => $sSummaryPlain,
+                    'strecher' => mb_strlen($sSummaryPlain) > 240 ? '' : str_repeat('&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ', round((240 - mb_strlen($sSummaryPlain)) / 6)),
+                ),
+            ),
+        );
+
+        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 _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));
+    }
+}
+
+/** @} */

+ 263 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/text/js/links.js

@@ -0,0 +1,263 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseText Base classes for text modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+function BxBaseModTextLinks(oOptions) {
+    this._sActionsUri = oOptions.sActionUri;
+    this._sActionsUrl = oOptions.sActionUrl;
+    this._sObjName = oOptions.sObjName == undefined ? 'oBxBaseModTextLinks' : oOptions.sObjName;
+    this._iOwnerId = oOptions.iOwnerId == undefined ? 0 : oOptions.iOwnerId;
+    this._iContentId = oOptions.iContentId == undefined ? 0 : oOptions.iContentId;
+    this._sFormId = oOptions.sFormId == undefined ? 0 : oOptions.sFormId;
+    this._sEditorId = oOptions.sEditorId == undefined ? '' : oOptions.sEditorId;
+    this._aHtmlIds = oOptions.aHtmlIds == undefined ? {} : oOptions.aHtmlIds;
+    this._iLimitAttachLinks = oOptions.iLimitAttachLinks == undefined ? 0 : oOptions.iLimitAttachLinks;
+    this._sLimitAttachLinksErr = oOptions.sLimitAttachLinksErr == undefined ? '' : oOptions.sLimitAttachLinksErr;
+    this._oAttachedLinks = oOptions.oAttachedLinks == undefined ? {} : oOptions.oAttachedLinks;
+    
+    this._sPregTag = "(<([^>]+bx-tag[^>]+)>)";
+    this._sPregMention = "(<([^>]+bx-mention[^>]+)>)";
+    this._sPregUrl = "(([A-Za-z]{3,9}:(?:\\/\\/)?)(?:[\\-;:&=\\+\\$,\\w]+@)?[A-Za-z0-9\\.\\-]+|(?:www\\.|[\\-;:&=\\+\\$,\\w]+@)[A-Za-z0-9\\.\\-]+)((?:\\/[\\+~%#\\/\\.\\w\\-_!\\(\\)]*)?\\??(?:[\\-\\+=&;%@\\.\\w_]*)#?(?:[\\.\\!\\/\\w]*))?";
+    
+    var $this = this;
+    
+    $(document).ready(function () {
+        $this.initFormPost($this._sFormId);
+    });
+}
+
+BxBaseModTextLinks.prototype.initFormPost = function(sFormId)
+{
+    var $this = this;
+    var oForm = $('#' + sFormId);
+    var oTextarea = oForm.find('textarea');
+
+    if (typeof window.glOnSpaceEnterInEditor === 'undefined')
+        window.glOnSpaceEnterInEditor = [];    
+
+    window.glOnSpaceEnterInEditor.push(function (sData, sSelector) {
+        sSelector = '#' + $this._sFormId + ' [name=text]';
+        if(!oTextarea.is(sSelector))
+            return;
+
+        $this.parseContent(oForm, sData, true);
+    });
+
+    var sContent = oTextarea.val();
+    if(sContent && sContent.length > 0)
+        this.parseContent(oForm, sContent, false);
+
+    if(this._bAutoAttach) {
+        if (typeof window.glOnInsertImageInEditor === 'undefined')
+            window.glOnInsertImageInEditor = [];
+
+        window.glOnInsertImageInEditor.push(function (oFile, sSelector) {
+            
+            if(!oTextarea.is(sSelector))
+                return false;
+            
+            const oFormData = new FormData();
+            oFormData.append('file', oFile);
+            oFormData.append('u', $this._sAutoUploader);
+            oFormData.append('uid', $this._sAutoUploaderId);
+
+            fetch($this._sActionsUrl + 'auto_attach_insertion/', {method: "POST", body: oFormData})
+            .then(response => response.json())
+            .then(result => {
+                processJsonData(result)
+            });
+            return true;
+        });
+   }
+};
+
+BxBaseModTextLinks.prototype.parseContent = function(oForm, sData, bPerformAttach)
+{
+    var oExp, aMatch = null;
+
+    oExp = new RegExp(this._sPregTag , "ig");
+    sData = sData.replace(oExp, '');
+
+    oExp = new RegExp(this._sPregMention , "ig");
+    sData = sData.replace(oExp, '');
+
+    oExp = new RegExp(this._sPregUrl , "ig");
+    while(aMatch = oExp.exec(sData)) {
+        var sUrl = aMatch[0].replace(/^(\s|(&nbsp;))+|(\s|(&nbsp;))+$/gm,'');
+        if(!sUrl.length || this._oAttachedLinks[sUrl] != undefined || (this._iLimitAttachLinks != 0 && Object.keys(this._oAttachedLinks).length >= this._iLimitAttachLinks))
+            continue;
+
+        //--- Mark that 'attach link' process was started.
+        this._oAttachedLinks[sUrl] = 0;
+
+        if(bPerformAttach)
+            this.addAttachLink(oForm, sUrl);
+    }
+};
+
+BxBaseModTextLinks.prototype.addAttachLink = function(oElement, sUrl)
+{
+    if(!sUrl || (this._iLimitAttachLinks != 0 && Object.keys(this._oAttachedLinks).length > this._iLimitAttachLinks))
+        return;
+
+    var $this = this;
+    var oData = this._getDefaultData();
+    oData['url'] = sUrl;
+    oData['content_id'] = $this._iContentId;
+    
+
+    jQuery.post (
+        this._sActionsUrl + 'add_attach_link/',
+        oData,
+        function(oData) {
+            if(!oData.id || !oData.item || !$.trim(oData.item).length)
+                return;
+
+            //--- Mark that 'attach link' process was finished.
+            $this._oAttachedLinks[sUrl] = oData.id;
+
+            var iContentId = 0;
+            if(oData && oData.content_id != undefined)
+                iContentId = parseInt(oData.content_id);
+
+            var oItem = $(oData.item).hide();
+            $('#' + $this._aHtmlIds['attach_link_form_field'] + iContentId).prepend(oItem).find('#' + oItem.attr('id')).bx_anim('show', $this._sAnimationEffect, $this._sAnimationSpeed, function() {
+                $(this).bxProcessHtml();
+            });
+        },
+        'json'
+    );
+};
+
+BxBaseModTextLinks.prototype.showAttachLink = function(oLink)
+{
+    if(this._iLimitAttachLinks != 0 && Object.keys(this._oAttachedLinks).length >= this._iLimitAttachLinks) {
+        bx_alert(this._sLimitAttachLinksErr);
+        return false;
+    }
+
+    var oData = this._getDefaultData();
+    oData['content_id'] = this._iContentId;
+
+    $(window).dolPopupAjax({
+        id: {value: this._aHtmlIds['attach_link_popup'], force: true},
+        url: bx_append_url_params(this._sActionsUri + 'get_attach_link_form/', oData),
+        closeOnOuterClick: false,
+        removeOnClose: true
+    });
+
+    return false;
+};
+
+BxBaseModTextLinks.prototype.initFormAttachLink = function(sFormId)
+{
+    var $this = this;
+    var oForm = $('#' + sFormId);
+
+    oForm.ajaxForm({
+        dataType: "json",
+        clearForm: true,
+        beforeSubmit: function (formData, jqForm, options) {
+            window[$this._sObjName].beforeFormAttachLinkSubmit(oForm);
+        },
+        success: function (oData) {
+            window[$this._sObjName].afterFormAttachLinkSubmit(oForm, oData);
+        }
+    });
+};
+
+BxBaseModTextLinks.prototype.beforeFormAttachLinkSubmit = function(oForm)
+{
+    this.loadingInButton($(oForm).children().find(':submit'), true);
+};
+
+BxBaseModTextLinks.prototype.afterFormAttachLinkSubmit = function (oForm, oData)
+{
+    var $this = this;
+    var fContinue = function() {
+        if(oData && oData.item != undefined) {
+            $('#' + $this._aHtmlIds['attach_link_popup']).dolPopupHide({});
+
+            if(!$.trim(oData.item).length)
+                return;
+
+            var iContentId = 0;            
+            if(oData && oData.content_id != undefined)
+                iContentId = parseInt(oData.content_id);
+            
+            var oItem = $(oData.item).hide();
+            $('#' + $this._aHtmlIds['attach_link_form_field'] + iContentId).prepend(oItem).find('#' + oItem.attr('id')).bx_anim('show', $this._sAnimationEffect, $this._sAnimationSpeed, function() {
+                $(this).bxProcessHtml();
+            });
+
+            $this._oAttachedLinks[oData.url] = oData.id;
+            return;
+        }
+
+        if(oData && oData.form != undefined && oData.form_id != undefined) {
+            $('#' + oData.form_id).replaceWith(oData.form);
+            $this.initFormAttachLink(oData.form_id);
+
+            return;
+        }
+    };
+
+    this.loadingInButton($(oForm).find(':submit'), false);
+
+    if(oData && oData.message != undefined)
+        bx_alert(oData.message, fContinue);
+    else
+        fContinue();
+};
+
+BxBaseModTextLinks.prototype.deleteAttachLink = function(oLink, iId)
+{
+    var $this = this;
+    var oData = this._getDefaultData();
+    oData['id'] = iId;
+
+    var oAttachLink = $('#' + this._aHtmlIds['attach_link_item'] + iId);
+    bx_loading(oAttachLink, true);
+    
+    jQuery.post (
+        this._sActionsUrl + 'delete_attach_link/',
+        oData,
+        function(oData) {
+            var fContinue = function() {
+                if(oData && oData.code != undefined && oData.code == 0) {
+                    oAttachLink.bx_anim('hide', $this._sAnimationEffect, $this._sAnimationSpeed, function() {
+                        $(this).remove;
+                    });
+                }
+            };
+
+            bx_loading(oAttachLink, false);
+
+            if(oData && oData.message != undefined)
+                bx_alert(oData.message, fContinue);
+            else
+                fContinue();
+        },
+        'json'
+    );
+
+    return false;
+};
+
+BxBaseModTextLinks.prototype._getDefaultData = function() {
+    var oDate = new Date();
+    return jQuery.extend({}, this._oRequestParams, {_t:oDate.getTime()});
+};
+
+BxBaseModTextLinks.prototype.loadingInButton = function(e, bShow) {
+    if($(e).length)
+        bx_loading_btn($(e), bShow);
+    else
+        bx_loading($('body'), bShow);	
+};

+ 27 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/modules/base/text/template/unit_gallery.html

@@ -0,0 +1,27 @@
+<div class="__class__ bx-base-text-unit bx-base-text-unit-gallery bx-def-gal-unit-size flex flex-col w-full p-2 box-border">
+    <div class="bx-base-text-unit-cnt bx-def-box bx-def-box-round-corners bx-def-color-bg-box h-full">
+        <bx_if:thumb>
+            <a href="__content_url__" title="__summary_attr__">
+                <div class="shrink-0 m-1">
+                    <img class="aspect-video object-cover bg-cover w-full rounded" alt="__title__" style="background-image:url(__gallery_url__); __image_settings__" src="<bx_icon_url:spacer.gif />"  />
+                </div>
+            </a>
+        </bx_if:thumb>
+        <bx_if:no_thumb>
+            <div class="bx-def-gal-img-column shrink-0 m-1">
+                <div class="bx-base-text-unit-no-thumb flex flex-col items-center justify-center aspect-video p-4 bg-gradient-to-b from-red-400 to-red-500 box-border rounded"></div>
+            </div>
+        </bx_if:no_thumb>
+        <div class="bx-base-text-unit-text flex flex-col flex-1 p-3 md:p-4 lg:p-6 pt-1 md:pt-2 lg:pt-3 box-border">
+            <div class="bx-base-text-unit-badges pb-1 lg:pb-2">__badges__</div>
+            <div class="bx-base-text-unit-title flex-auto max-h-14">
+                <div class="bx-base-tut-title truncate text-left">
+                    <a href="__content_url__" class="text-left">__title__</a>
+                </div>
+            </div>
+            <div class="bx-base-text-unit-info flex flex-auto flex-col justify-end text-sm text-gray-500 dark:text-gray-400">
+                <bx_if:meta><div class="bx-base-general-unit-meta pt-2">__meta__</div></bx_if:meta>
+            </div>
+        </div>
+    </div>
+</div>

+ 7 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/plugins/autoload.php

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

+ 75 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/plugins/composer/autoload_real.php

@@ -0,0 +1,75 @@
+<?php
+
+// autoload_real.php @generated by Composer
+
+class ComposerAutoloaderInitddef6a94b127e79526354a3d2201274f
+{
+    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('ComposerAutoloaderInitddef6a94b127e79526354a3d2201274f', 'loadClassLoader'), true, true);
+        self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
+        spl_autoload_unregister(array('ComposerAutoloaderInitddef6a94b127e79526354a3d2201274f', '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\ComposerStaticInitddef6a94b127e79526354a3d2201274f::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\ComposerStaticInitddef6a94b127e79526354a3d2201274f::$files;
+        } else {
+            $includeFiles = require __DIR__ . '/autoload_files.php';
+        }
+        foreach ($includeFiles as $fileIdentifier => $file) {
+            composerRequireddef6a94b127e79526354a3d2201274f($fileIdentifier, $file);
+        }
+
+        return $loader;
+    }
+}
+
+function composerRequireddef6a94b127e79526354a3d2201274f($fileIdentifier, $file)
+{
+    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
+        require $file;
+
+        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
+    }
+}

+ 224 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/plugins/composer/autoload_static.php

@@ -0,0 +1,224 @@
+<?php
+
+// autoload_static.php @generated by Composer
+
+namespace Composer\Autoload;
+
+class ComposerStaticInitddef6a94b127e79526354a3d2201274f
+{
+    public static $files = array (
+        '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
+        '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
+        'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php',
+        '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
+        'c65d09b6820da036953a371c8c73a9b1' => __DIR__ . '/..' . '/facebook/graph-sdk/src/Facebook/polyfills.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 (
+            '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,
+        ),
+        'F' => 
+        array (
+            'Facebook\\' => 9,
+        ),
+        'E' => 
+        array (
+            'Embera\\' => 7,
+        ),
+        'C' => 
+        array (
+            'ChargeBee\\' => 10,
+        ),
+        'A' => 
+        array (
+            'AvtoDev\\Composer\\Cleanup\\' => 25,
+            'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\' => 38,
+        ),
+    );
+
+    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',
+        ),
+        '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',
+        ),
+        'Facebook\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/facebook/graph-sdk/src/Facebook',
+        ),
+        '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\\Engine\\Postproc\\Connector\\S3v4\\' => 
+        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 = ComposerStaticInitddef6a94b127e79526354a3d2201274f::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInitddef6a94b127e79526354a3d2201274f::$prefixDirsPsr4;
+            $loader->prefixesPsr0 = ComposerStaticInitddef6a94b127e79526354a3d2201274f::$prefixesPsr0;
+            $loader->classMap = ComposerStaticInitddef6a94b127e79526354a3d2201274f::$classMap;
+
+        }, null, ClassLoader::class);
+    }
+}

+ 348 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/plugins/composer/installed.php

@@ -0,0 +1,348 @@
+<?php return array(
+    'root' => array(
+        'pretty_version' => 'dev-master',
+        'version' => 'dev-master',
+        'type' => 'library',
+        'install_path' => __DIR__ . '/../../',
+        'aliases' => array(),
+        'reference' => 'd61b587727b37716bee10b5a5cb8af48cceb8865',
+        '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' => '6448ddf99541f3ba50f97e373ceeb5bfd42a0167',
+            '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.12.1',
+            'version' => '1.12.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../bshaffer/oauth2-server-php',
+            'aliases' => array(),
+            'reference' => '2bfaf9d7bbebe2ba1c1deb48e756ba0b3af4e985',
+            'dev_requirement' => false,
+        ),
+        'chargebee/chargebee-php' => array(
+            'pretty_version' => 'v3.11.0',
+            'version' => '3.11.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../chargebee/chargebee-php',
+            'aliases' => array(),
+            'reference' => '2b74d71005693f8f11cc2ef1aea07c48a8f8e155',
+            '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' => '6598f3860c2698fe2f0f1bc98212fc01d0a1893c',
+            'dev_requirement' => false,
+        ),
+        'ezyang/htmlpurifier' => array(
+            'pretty_version' => 'v4.16.0',
+            'version' => '4.16.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../ezyang/htmlpurifier',
+            'aliases' => array(),
+            'reference' => '523407fb06eb9e5f3d59889b3978d5bfe94299c8',
+            'dev_requirement' => false,
+        ),
+        'facebook/graph-sdk' => array(
+            'pretty_version' => '5.7.0',
+            'version' => '5.7.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../facebook/graph-sdk',
+            'aliases' => array(),
+            'reference' => '2d8250638b33d73e7a87add65f47fabf91f8ad9b',
+            '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.5.0',
+            'version' => '7.5.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/guzzle',
+            'aliases' => array(),
+            'reference' => 'b50a2a1251152e43f6a37f0fa053e730a67d25ba',
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/promises' => array(
+            'pretty_version' => '1.5.2',
+            'version' => '1.5.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/promises',
+            'aliases' => array(),
+            'reference' => 'b94b2807d85443f9719887892882d0329d1e2598',
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/psr7' => array(
+            'pretty_version' => '2.4.1',
+            'version' => '2.4.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/psr7',
+            'aliases' => array(),
+            'reference' => '69568e4293f4fa993f3b0e51c9723e1e17c41379',
+            '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.69',
+            'version' => '1.3.69.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../matthiasmullie/minify',
+            'aliases' => array(),
+            'reference' => 'a61c949cccd086808063611ef9698eabe42ef22f',
+            '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.27',
+            'version' => '2.0.27.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../mpratt/embera',
+            'aliases' => array(),
+            'reference' => '9c16a9559fa75d9a9d5efddb17cd34555a6dff5e',
+            '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.6.4',
+            'version' => '6.6.4.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../phpmailer/phpmailer',
+            'aliases' => array(),
+            'reference' => 'a94fdebaea6bd17f51be0c2373ab80d3d681269b',
+            'dev_requirement' => false,
+        ),
+        'psr/http-client' => array(
+            'pretty_version' => '1.0.1',
+            'version' => '1.0.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-client',
+            'aliases' => array(),
+            'reference' => '2dfb5f6c5eff0e91e20e913f8c5452ed95b86621',
+            'dev_requirement' => false,
+        ),
+        'psr/http-client-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        'psr/http-factory' => array(
+            'pretty_version' => '1.0.1',
+            'version' => '1.0.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-factory',
+            'aliases' => array(),
+            'reference' => '12ac7fcd07e5b077433f5f2bee95b3a771bf61be',
+            'dev_requirement' => false,
+        ),
+        'psr/http-factory-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        'psr/http-message' => array(
+            'pretty_version' => '1.0.1',
+            'version' => '1.0.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-message',
+            'aliases' => array(),
+            'reference' => 'f6561bf28d520154e4b0ec72be95418abe6d9363',
+            'dev_requirement' => false,
+        ),
+        'psr/http-message-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        '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' => '621d6d857400983d0fd6cd291f105a835a317748',
+            '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' => 'd61b587727b37716bee10b5a5cb8af48cceb8865',
+            'dev_requirement' => false,
+        ),
+        'wikimedia/less.php' => array(
+            'pretty_version' => 'v3.1.0',
+            'version' => '3.1.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../wikimedia/less.php',
+            'aliases' => array(),
+            'reference' => 'a486d78b9bd16b72f237fc6093aa56d69ce8bd13',
+            'dev_requirement' => false,
+        ),
+    ),
+);

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/plugins_public/tailwind/css/tailwind.min.css


+ 206 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/plugins_public/tailwind/js/tailwind.config.js

@@ -0,0 +1,206 @@
+/**
+ * 
+ * Note. For now only UNA core, Artificer and Messenger modules were included in minifying process.
+ * 
+ */
+
+module.exports = {
+  content: [
+    './inc/js/*.js',
+    './inc/js/classes/*.js',
+
+    './modules/base/**/js/*.js',
+    './modules/base/**/template/*.html',
+
+    './modules/boonex/**/js/*.js',
+    './modules/boonex/**/template/*.html',
+
+    './modules/boonex/**/data/template/**/*.html',
+
+    './studio/template/*.html',
+
+    './template/*.html',
+  ],
+  safelist: [
+    'w-8', 'w-10', 'w-16', 'w-24', 'w-48', 'w-4/6', 
+    'h-8', 'h-10', 'h-24', 'h-48',
+    '-m-2',
+    'text-red-400', 
+    'ring-blue-500', 'ring-opacity-20', 
+    'focus:ring-blue-500', 'focus:ring-opacity-20',
+    'border-blue-500', 'border-opacity-70', 
+    'focus:border-blue-500', 'focus:border-opacity-70',
+
+    'aspect-w-1', 'aspect-w-3', 'aspect-w-4', 'aspect-w-9', 'aspect-w-16',
+    'aspect-h-1', 'aspect-h-3', 'aspect-h-4', 'aspect-h-9', 'aspect-h-16',
+    'aspect-none',
+
+    'col-red1', 'col-red1-dark', 'col-red2', 'col-red2-dark', 'col-red3', 'col-red3-dark',
+    'bg-col-red1', 'bg-col-red1-dark', 'bg-col-red2', 'bg-col-red2-dark', 'bg-co3-red1', 'bg-col-red3-dark',
+    'col-green1', 'col-green1-dark', 'col-green2', 'col-green2-dark', 'col-green3', 'col-green3-dark',
+    'bg-col-green1', 'bg-col-green1-dark', 'bg-col-green2', 'bg-col-green2-dark', 'bg-co3-green1', 'bg-col-green3-dark',
+    'col-blue1', 'col-blue1-dark', 'col-blue2', 'col-blue2-dark', 'col-blue3', 'col-blue3-dark',
+    'bg-col-blue1', 'bg-col-blue1-dark', 'bg-col-blue2', 'bg-col-blue2-dark', 'bg-co3-blue1', 'bg-col-blue3-dark',
+    'col-gray', 'col-gray-dark',
+    'bg-col-gray', 'bg-col-gray-dark',
+
+    'bx-def-box-sizing', 'bx-def-align-center',  'bx-def-valign-center', 'bx-def-centered', 
+    'bx-def-margin', 'bx-def-margin-neg', 'bx-def-margin-left', 'bx-def-margin-left-auto', 'bx-def-margin-right', 'bx-def-margin-top', 'bx-def-margin-top-auto', 'bx-def-margin-bottom', 'bx-def-margin-leftright', 'bx-def-margin-leftright-neg', 'bx-def-margin-topbottom', 'bx-def-margin-topbottom-neg', 'bx-def-margin-lefttopright', 'bx-def-margin-rightbottomleft', 
+    'bx-def-margin-sec', 'bx-def-margin-sec-neg', 'bx-def-margin-sec-left', 'bx-def-margin-sec-left-auto', 'bx-def-margin-sec-right', 'bx-def-margin-sec-top', 'bx-def-margin-sec-top-auto', 'bx-def-margin-sec-bottom', 'bx-def-margin-sec-leftright', 'bx-def-margin-sec-leftright-neg', 'bx-def-margin-sec-topbottom', 'bx-def-margin-sec-topbottom-neg', 'bx-def-margin-sec-lefttopright', 'bx-def-margin-sec-rightbottomleft', 
+    'bx-def-margin-thd', 'bx-def-margin-thd-neg', 'bx-def-margin-thd-left', 'bx-def-margin-thd-left-auto', 'bx-def-margin-thd-right', 'bx-def-margin-thd-top', 'bx-def-margin-thd-top-auto', 'bx-def-margin-thd-bottom', 'bx-def-margin-thd-leftright', 'bx-def-margin-thd-leftright-neg', 'bx-def-margin-thd-topbottom', 'bx-def-margin-thd-topbottom-neg', 'bx-def-margin-thd-lefttopright', 'bx-def-margin-thd-rightbottomleft', 
+    'bx-def-padding', 'bx-def-padding-left', 'bx-def-padding-left-auto', 'bx-def-padding-right', 'bx-def-padding-top', 'bx-def-padding-top-auto', 'bx-def-padding-bottom', 'bx-def-padding-leftright', 'bx-def-padding-topbottom', 'bx-def-padding-lefttopright', 'bx-def-padding-rightbottomleft', 
+    'bx-def-padding-sec', 'bx-def-padding-sec-left', 'bx-def-padding-sec-left-auto', 'bx-def-padding-sec-right', 'bx-def-padding-sec-top', 'bx-def-padding-sec-top-auto', 'bx-def-padding-sec-bottom', 'bx-def-padding-sec-leftright', 'bx-def-padding-sec-topbottom', 'bx-def-padding-sec-lefttopright', 'bx-def-padding-sec-rightbottomleft', 
+    'bx-def-padding-thd', 'bx-def-padding-thd-left', 'bx-def-padding-thd-left-auto', 'bx-def-padding-thd-right', 'bx-def-padding-thd-top', 'bx-def-padding-thd-top-auto', 'bx-def-padding-thd-bottom', 'bx-def-padding-thd-leftright', 'bx-def-padding-thd-topbottom', 'bx-def-padding-thd-lefttopright', 'bx-def-padding-thd-rightbottomleft', 
+    'bx-def-font-small', 'bx-def-font-middle', 'bx-def-font-large', 'bx-def-font-h3', 'bx-def-font-h2', 'bx-def-font-h1', 
+    'bx-def-a-colored',
+    'bx-def-unit-alert', 'bx-def-unit-alert-small', 'bx-def-unit-alert-middle',
+    'bx-def-label',
+    'bx-def-icon-size', 'bx-def-thumb-size', 'bx-def-ava-size', 'bx-def-ava-big-size',
+    'bx-def-color-bg-box-active', 
+
+    'bx-form-caption', 'bx-form-value', 'bx-form-required', 'bx-form-warn', 
+    'bx-form-input-wrapper-checkbox_set', 'bx-form-input-wrapper-radio_set',
+    '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-text', 'bx-form-input-price', 'bx-form-input-checkbox', 'bx-form-input-radio', 
+    'bx-switcher-cont', 
+
+    'bx-popup-full-screen', 'bx-popup-fog',
+
+    'bx-stl-mil', 'bx-stl-mii', 'bx-stl-mit', 'bx-stl-mia', 
+
+    'bx-informer-msg-info', 'bx-informer-msg-alert', 'bx-informer-msg-error',
+
+    'sys-action-counter', 'sys-ac-only', 'sys-ac-link',
+
+    'bx-vote-bls-submenu-cnt', 
+    
+    'bx-base-general-unit-meta-username', 'bx-base-text-unit-gallery-wrapper', 'aspect-video', 
+
+    'bx-tl-overflow',
+
+    'sys-auth-block', 'sys-auth-compact-container',
+
+    'flickity-button',
+	  
+    'ql-editor', 'space-x-4', 'filepond--drop-label',
+      
+    'bg-slate-600','bg-gray-600','bg-zinc-600','bg-neutral-600','bg-stone-600','bg-red-600','bg-orange-600','bg-amber-600','bg-yellow-600','bg-lime-600','bg-green-600','bg-emerald-600','bg-teal-600','bg-cyan-600','bg-sky-600','bg-indigo-600','bg-violet-600','bg-blue-600','bg-purple-600','bg-fuchsia-600','bg-pink-600','bg-rose-600',
+      
+    'text-slate-600','text-gray-600','text-zinc-600','text-neutral-600','text-stone-600','text-red-600','text-orange-600','text-amber-600','text-yellow-600','text-lime-600','text-green-600','text-emerald-600','text-teal-600','text-cyan-600','text-sky-600','text-indigo-600','text-violet-600','text-blue-600','text-purple-600','text-fuchsia-600','text-pink-600', 'text-rose-600'
+  ],
+  darkMode: 'class', // false or 'media' or 'class'
+  theme: {
+    fontFamily: {
+        'inter': ['ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', '"Noto Sans"', 'sans-serif', '"Apple Color Emoji"', '"Segoe UI Emoji"', '"Segoe UI Symbol"', '"Noto Color Emoji"'],
+    },
+    extend: {
+        width: {
+            '21': '5.25rem',
+            '46': '11.5rem',
+            '50': '12.5rem',
+            '112': '28rem',
+            '128': '32rem',
+            '144': '36rem',
+            '1/10': '10%',
+            '2/10': '20%',
+            '3/10': '30%',
+            '4/10': '40%',
+            '5/10': '50%',
+            '6/10': '60%',
+            '7/10': '70%',
+            '8/10': '80%',
+            '9/10': '90%'
+        },
+        minWidth: {
+            4: '1rem',
+            6: '1.5rem',
+            48: '12rem',
+            88: '22rem'
+        },
+        maxWidth: {
+            32: '8rem',
+        },
+        height: {
+            46: '11.5rem',
+            50: '12.5rem',
+            112: '28rem',
+            128: '32rem',
+            144: '36rem'
+        },
+        maxHeight: {
+            128: '32rem',
+        },
+        lineHeight: {
+            11: '2.75rem',
+            12: '3rem',
+            13: '3.25rem',
+            14: '3.5rem',
+            15: '3.75rem',
+            16: '4rem',
+        },
+        zIndex: {
+            1: 1,
+            2: 2,
+            3: 3,
+            4: 4,
+            5: 5,
+        },
+        flex: {
+            2: '2 2 0%',
+        },
+        translate: {
+            '5/4': '125%',
+        },
+        animation: {
+          goo: "goo 8s infinite",
+        },
+        aspectRatio: {
+          auto: 'auto',
+          square: '1 / 1',
+          video: '16 / 9',
+          1: '1',
+          2: '2',
+          3: '3',
+          4: '4',
+          5: '5',
+          6: '6',
+          7: '7',
+          8: '8',
+          9: '9',
+          10: '10',
+          11: '11',
+          12: '12',
+          13: '13',
+          14: '14',
+          15: '15',
+          16: '16',
+        },
+        keyframes: {
+          goo: {
+            "0%": {
+              transform: "translate(0px, 0px) scale(1)",
+            },
+            "33%": {
+              transform: "translate(30px, -50px) scale(1.2)",
+            },
+            "66%": {
+              transform: "translate(-20px, 20px) scale(0.8)",
+            },
+            "100%": {
+              transform: "translate(0px, 0px) scale(1)",
+            },
+          },
+        },
+    },
+  },
+  variants: {
+    aspectRatio: ['responsive', 'hover']
+  },
+  corePlugins: {
+    aspectRatio: true,
+  },
+  plugins: [
+    require('@tailwindcss/typography'),
+    require('@tailwindcss/aspect-ratio'),
+    require('@tailwindcss/container-queries'),
+  ],
+}

+ 38 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/api.php

@@ -0,0 +1,38 @@
+<?php
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaEndAdmin UNA Studio End Admin Pages
+ * @ingroup     UnaStudio
+ * @{
+ */
+
+require_once('./../inc/header.inc.php');
+require_once(BX_DIRECTORY_PATH_INC . 'design.inc.php');
+
+bx_import('BxDolLanguages');
+
+bx_require_authentication(true);
+
+$sPage = bx_get('page');
+$sPage = $sPage !== false ? bx_process_input($sPage) : '';
+
+$oPage = new BxTemplStudioAPI($sPage);
+
+$oTemplate = BxDolStudioTemplate::getInstance();
+
+$sPageCode = $oPage->getPageCode();
+if($sPageCode === false)
+    $oTemplate->displayMsg(($sError = $oPage->getError(false)) !== false ? $sError : '_sys_txt_error_occured', true);
+
+$oTemplate->setPageNameIndex($oPage->getPageIndex());
+$oTemplate->setPageHeader($oPage->getPageHeader());
+$oTemplate->setPageContent('page_caption_code', $oPage->getPageCaption());
+$oTemplate->setPageContent('page_attributes', $oPage->getPageAttributes());
+$oTemplate->setPageContent('page_menu_code', $oPage->getPageMenu());
+$oTemplate->setPageContent('page_main_code', $sPageCode . $oPage->getPageJsCode());
+$oTemplate->addCss($oPage->getPageCss());
+$oTemplate->addJs($oPage->getPageJs());
+$oTemplate->getPageCode();
+/** @} */

+ 30 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/classes/BxDolStudioAPI.php

@@ -0,0 +1,30 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaStudio UNA Studio
+ * @{
+ */
+
+define('BX_DOL_STUDIO_API_TYPE_SETTINGS', 'settings');
+define('BX_DOL_STUDIO_API_TYPE_KEYS', 'keys');
+define('BX_DOL_STUDIO_API_TYPE_ORIGINS', 'origins');
+
+define('BX_DOL_STUDIO_API_TYPE_DEFAULT', BX_DOL_STUDIO_API_TYPE_SETTINGS);
+
+class BxDolStudioAPI extends BxTemplStudioWidget
+{
+    protected $sPage;
+
+    function __construct($sPage = "")
+    {
+        parent::__construct('api');
+
+        $this->sPage = BX_DOL_STUDIO_API_TYPE_DEFAULT;
+        if(is_string($sPage) && !empty($sPage))
+            $this->sPage = $sPage;
+    }
+}
+
+/** @} */

+ 148 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/classes/BxDolStudioApiKeys.php

@@ -0,0 +1,148 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaStudio UNA Studio
+ * @{
+ */
+
+class BxDolStudioApiKeys extends BxTemplStudioGrid
+{
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        parent::__construct ($aOptions, $oTemplate);
+    }
+
+    public function _performActionAdd($aForm, $aCustomValues = []) 
+    {
+        $oForm = new BxTemplFormView($aForm);
+        $oForm->initChecker();
+
+        if ($oForm->isSubmittedAndValid()) { // if form is submitted and all fields are valid
+
+            $iRecentId = $oForm->insert ($aCustomValues, true); // insert new record
+
+            if ($iRecentId)
+                $aRes = array('grid' => $this->getCode(false), 'blink' => $iRecentId); // if record is successfully added, reload grid and highlight added row
+            else
+                $aRes = array('msg' => _t('_sys_txt_error_occured')); // if record adding failed, display error message
+
+            echoJson($aRes);
+
+        } else { // if form is not submitted or some fields are invalid, display popup with form
+
+            $s = BxTemplFunctions::getInstance()->popupBox($oForm->getId() . '_form', '', $oForm->getCode() . '
+                <script>
+                    $(document).ready(function () {
+                        $("#' . $oForm->getId() . '").ajaxForm({
+                            dataType: "json",
+                            beforeSubmit: function (formData, jqForm, options) {
+                                bx_loading($("#' . $oForm->getId() . '"), true);
+                            },
+                            success: function (data) {
+                                $(".bx-popup-applied:visible").dolPopupHide();
+                                glGrids.' . $this->_sObject . '.processJson(data, "add");
+                            }
+                        });
+                    });
+                </script>');
+
+            echoJson(array('popup' => array('html' => $s, 'options' => array('closeOnOuterClick' => false))));
+
+        }
+    }
+
+    public function performActionAdd()
+    {
+        $aForm = array(
+            'form_attrs' => array(
+                'id' => 'bx_studio_api_keys',
+                'action' => BX_DOL_URL_ROOT . 'grid.php?o=sys_studio_api_keys&a=add',
+                'method' => 'post',
+            ),
+            'params' => array (
+                'db' => array(
+                    'table' => 'sys_api_keys',
+                    'key' => 'id',
+                    'submit_name' => 'do_submit',
+                ),
+            ),
+            'inputs' => array(
+
+                'title' => array(
+                    'type' => 'text',
+                    'name' => 'title',
+                    'caption' => _t('_Name'),
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+
+                'submit' => array(
+                    'type' => 'input_set',
+                    0 => array (
+                        'type' => 'submit',
+                        'name' => 'do_submit',
+                        'value' => _t('_sys_submit'),
+                    ),
+                    1 => array (
+                        'type' => 'reset',
+                        'name' => 'close',
+                        'value' => _t('_sys_close'),
+                        'attrs' => array('class' => 'bx-def-margin-sec-left', 'onclick' => '$(\'.bx-popup-applied:visible\').dolPopupHide();'),
+                    ),
+                ),
+
+            ),
+        );
+
+        $aCustomValues = array(
+            'key' => genRndPwd(48),
+        );
+        return $this->_performActionAdd($aForm, $aCustomValues);
+/*
+        $oForm = new BxTemplFormView($aForm);
+        $oForm->initChecker();
+
+        if ($oForm->isSubmittedAndValid()) { // if form is submitted and all fields are valid
+
+            $aCustomValues = array(
+                'key' => genRndPwd(48),
+            );
+
+            $iRecentId = $oForm->insert ($aCustomValues, true); // insert new record
+
+            if ($iRecentId)
+                $aRes = array('grid' => $this->getCode(false), 'blink' => $iRecentId); // if record is successfully added, reload grid and highlight added row
+            else
+                $aRes = array('msg' => _t('_sys_txt_error_occured')); // if record adding failed, display error message
+
+            echoJson($aRes);
+
+        } else { // if form is not submitted or some fields are invalid, display popup with form
+
+            $s = BxTemplFunctions::getInstance()->popupBox($oForm->getId() . '_form', '', $oForm->getCode() . '
+                <script>
+                    $(document).ready(function () {
+                        $("#' . $oForm->getId() . '").ajaxForm({
+                            dataType: "json",
+                            beforeSubmit: function (formData, jqForm, options) {
+                                bx_loading($("#' . $oForm->getId() . '"), true);
+                            },
+                            success: function (data) {
+                                $(".bx-popup-applied:visible").dolPopupHide();
+                                glGrids.' . $this->_sObject . '.processJson(data, "add");
+                            }
+                        });
+                    });
+                </script>');
+
+            echoJson(array('popup' => array('html' => $s, 'options' => array('closeOnOuterClick' => false))));
+
+        }
+*/
+    }
+}
+
+/** @} */

+ 65 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/classes/BxDolStudioApiOrigins.php

@@ -0,0 +1,65 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaStudio UNA Studio
+ * @{
+ */
+
+class BxDolStudioApiOrigins extends BxDolStudioApiKeys
+{
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        parent::__construct ($aOptions, $oTemplate);
+    }
+
+    public function performActionAdd()
+    {
+        $aForm = array(
+            'form_attrs' => array(
+                'id' => 'bx_studio_api_origins',
+                'action' => BX_DOL_URL_ROOT . 'grid.php?o=sys_studio_api_origins&a=add',
+                'method' => 'post',
+            ),
+            'params' => array (
+                'db' => array(
+                    'table' => 'sys_api_origins',
+                    'key' => 'id',
+                    'submit_name' => 'do_submit',
+                ),
+            ),
+            'inputs' => array(
+
+                'url' => array(
+                    'type' => 'text',
+                    'name' => 'url',
+                    'caption' => _t('_sys_txt_url'),
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+
+                'submit' => array(
+                    'type' => 'input_set',
+                    0 => array (
+                        'type' => 'submit',
+                        'name' => 'do_submit',
+                        'value' => _t('_sys_submit'),
+                    ),
+                    1 => array (
+                        'type' => 'reset',
+                        'name' => 'close',
+                        'value' => _t('_sys_close'),
+                        'attrs' => array('class' => 'bx-def-margin-sec-left', 'onclick' => '$(\'.bx-popup-applied:visible\').dolPopupHide();'),
+                    ),
+                ),
+
+            ),
+        );
+
+        return $this->_performActionAdd($aForm);
+    }
+}
+
+/** @} */

+ 50 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/classes/BxDolStudioJson.php

@@ -0,0 +1,50 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaStudio UNA Studio
+ * @{
+ */
+
+class BxDolStudioJson extends BxDolFactory 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 ();
+    }
+
+    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 BxDolStudioJson();
+        }
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    public function load($sUrl, $aParams = array())
+    {
+        $sContent = bx_file_get_contents($sUrl, $aParams, 'post');
+        if(empty($sContent))
+            return false;
+
+        //echo $sContent; exit;		//--- Uncomment to debug
+        $mixedResult = json_decode($sContent, true);
+        if(is_null($mixedResult))
+            return false;
+
+        return $mixedResult;
+    }
+}
+
+/** @} */

+ 229 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/classes/BxDolStudioOAuthOAuth2.php

@@ -0,0 +1,229 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaStudio UNA Studio
+ * @{
+ */
+
+class BxDolStudioOAuthOAuth2 extends BxDolStudioOAuth implements iBxDolSingleton
+{
+    protected $sSessionKeyStateToken;
+    protected $sSessionKeyCsrfToken;
+    protected $sSessionKeyCsrfTokenTime;
+
+    protected $sApiUrl;
+    protected $sScope;
+    protected $sPageHandle;
+
+    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->sSessionKeyStateToken = 'bx_studio_store_state_token';
+        $this->sSessionKeyCsrfToken = 'bx_studio_store_csrf_token';
+        $this->sSessionKeyCsrfTokenTime = 'bx_studio_store_csrf_token_time';
+
+        $this->sKey = $this->_getCredential('key');
+        $this->sSecret = $this->_getCredential('secret');
+        $this->sApiUrl = BX_DOL_MARKET_URL_ROOT . 'm/oauth2/';
+        $this->sScope = 'market';
+        $this->sPageHandle = BX_DOL_URL_STUDIO . 'store.php?page=goodies';
+        $this->sDataRetrieveMethod = 'post';
+    }
+
+    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__])) {
+            $sClass = __CLASS__;
+            $GLOBALS['bxDolClasses'][__CLASS__] = new $sClass();
+        }
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    protected function authorize()
+    {
+		if($this->isAuthorized())
+			return true;
+
+		$bCode = bx_get('code') !== false;
+		$bState = bx_get('state') !== false;
+	    if($bCode && $bState && $this->_getState() != bx_get('state'))
+            return _t('_adm_err_oauth_cannot_read_answer');
+
+		//--- Get access token.
+		if($bCode)
+			return $this->getAccessToken(bx_get('code'));
+
+		//--- Get request token and redirect to authorize.
+        return $this->getRequestToken();
+    }
+
+    protected function getRequestToken()
+    {
+    	$sUrl = bx_append_url_params($this->sApiUrl . 'auth', array(
+			'response_type' => 'code',
+			'client_id' => $this->sKey,
+			'redirect_uri' => $this->sPageHandle,
+			'scope' => $this->sScope,
+			'state' => $this->_genState(),
+		));
+
+		return array(
+		    'redirect' => $sUrl, 
+		    'message' => _t('_adm_msg_oauth_need_authorize', $sUrl)
+		);
+    }
+
+    protected function getAccessToken($sCode)
+    {
+    	$sResponse = bx_file_get_contents($this->sApiUrl . 'token', array(
+            'client_id' => $this->sKey,
+            'client_secret' => $this->sSecret,
+            'grant_type'    => 'authorization_code',
+            'code' => $sCode,
+            'redirect_uri'  => $this->sPageHandle,
+            'scope' => $this->sScope,
+        ), $this->sDataRetrieveMethod);
+
+        if (!$sResponse || ($aResponse = json_decode($sResponse, true)) === NULL || !isset($aResponse['access_token']) || isset($aResponse['error']))
+            return isset($aResponse['error_description']) ? $aResponse['error_description'] : _t('_error occured');
+
+        // get access_token 
+        $sAccessToken = $aResponse['access_token'];
+
+        // request info about profile
+        $sResponse = bx_file_get_contents($this->sApiUrl . 'api/me', array(), 'get', array(
+            'Authorization: Bearer ' . $sAccessToken,
+        ));
+
+        // handle error
+        if (!$sResponse || ($aResponse = json_decode($sResponse, true)) === NULL || !$aResponse || isset($aResponse['error']))
+            return isset($aResponse['error_description']) ? $aResponse['error_description'] : _t('_error occured');
+
+        $this->oSession->setValue(self::$sSessionKeyToken, $sAccessToken);
+        $this->oSession->setValue(self::$sSessionKeyAuthorized, 1);
+        $this->oSession->setValue(self::$sSessionKeyAuthorizedUser, (int)$aResponse['id']);
+        $this->oSession->setValue(self::$sSessionKeyAuthorizedOwner, (bool)$aResponse['owner'] ? 1 : 0);
+
+        return true;
+    }
+
+    protected function fetch($aParams = array())
+    {
+        if(!$this->isAuthorized())
+            return [];
+
+        $iTimeout = 120;
+        if(ini_get('safe_mode') == 0) {
+            ini_set('max_execution_time', $iTimeout);
+            ini_set('memory_limit', '512M');
+        }
+
+        $sHttpCode = null;
+        $sResponse = bx_file_get_contents($this->sApiUrl . 'api/market', $aParams, 'post', [
+            'Authorization: Bearer ' . $this->oSession->getValue(self::$sSessionKeyToken),
+        ], $sHttpCode, [], $iTimeout);
+
+        //echo $sResponse; exit;		//--- Uncomment to debug
+        if (!$sResponse || ($aResponse = json_decode($sResponse, true)) === NULL || !$aResponse || isset($aResponse['error'])) {
+            if(isset($aResponse['error']) && $this->isReloginRequired($aResponse['error']))
+                $this->unsetAuthorizedUser();
+
+            return isset($aResponse['error_description']) ? $aResponse['error_description'] : _t('_error occured');
+        }
+
+        return $aResponse['data'];
+    }
+
+    protected function isReloginRequired($sError)
+    {
+    	if(in_array($sError, array('invalid_token', 'expired_token')))
+    		return true;
+
+    	return false;
+    }
+
+    protected function _getCredential($sName)
+    {
+        $sResult = getParam('sys_oauth_' . $sName);
+        return $sResult !== false ? trim($sResult) : '';
+    }
+
+    protected function _genState()
+    {
+        $sResult = $this->_genCsrfToken();
+        if($sResult !== false)
+            return $sResult;
+
+        return $this->_genStateToken();
+    }
+
+    protected function _getState()
+    {
+        $sResult = $this->_getCsrfToken();
+        if($sResult !== false)
+            return $sResult;
+
+        return $this->_getStateToken();
+    }
+
+    protected function _genCsrfToken()
+    {
+        if(getParam('sys_security_form_token_enable') != 'on' || defined('BX_DOL_CRON_EXECUTE'))
+            return false;
+
+        $oSession = BxDolSession::getInstance();
+
+        $iCsrfTokenLifetime = (int)getParam('sys_security_form_token_lifetime');
+        $sToken = $oSession->getValue($this->sSessionKeyCsrfToken);
+        if($sToken !== false && ($iCsrfTokenLifetime == 0 || time() - (int)$oSession->getValue($this->sSessionKeyCsrfTokenTime) < $iCsrfTokenLifetime))
+            return $sToken;
+
+        $sToken = genRndPwd(20, false);
+        $oSession->setValue($this->sSessionKeyCsrfToken, $sToken);
+        $oSession->setValue($this->sSessionKeyCsrfTokenTime, time());
+
+        return $sToken;
+    }
+
+    protected function _getCsrfToken()
+    {
+        if(getParam('sys_security_form_token_enable') != 'on' || defined('BX_DOL_CRON_EXECUTE'))
+            return false;
+
+        return BxDolSession::getInstance()->getValue($this->sSessionKeyCsrfToken);
+    }
+
+    protected function _genStateToken()
+    {
+        $oSession = BxDolSession::getInstance();
+
+        $sToken = $oSession->getValue($this->sSessionKeyStateToken);
+        if($sToken !== false)
+            return $sToken;
+
+        $sToken = genRndPwd(20, false);
+        $oSession->setValue($this->sSessionKeyStateToken, $sToken);
+
+        return $sToken;
+    }
+
+    protected function _getStateToken()
+    {
+        return BxDolSession::getInstance()->getValue($this->sSessionKeyStateToken);
+    }
+}
+
+/** @} */

+ 439 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/classes/BxDolStudioOptions.php

@@ -0,0 +1,439 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaStudio UNA Studio
+ * @{
+ */
+
+define('BX_DOL_STUDIO_STG_TYPE_SYSTEM', 'system');
+define('BX_DOL_STUDIO_STG_TYPE_DEFAULT', BX_DOL_STUDIO_STG_TYPE_SYSTEM);
+
+define('BX_DOL_STUDIO_STG_GROUP_SYSTEM', 'system');
+define('BX_DOL_STUDIO_STG_GROUP_MODULES', 'modules');
+define('BX_DOL_STUDIO_STG_GROUP_LANGUAGES', 'languages');
+define('BX_DOL_STUDIO_STG_GROUP_TEMPLATES', 'templates');
+
+define('BX_DOL_STUDIO_STG_CATEGORY_SYSTEM', 'system');
+define('BX_DOL_STUDIO_STG_CATEGORY_LANGUAGES', 'languages');
+define('BX_DOL_STUDIO_STG_CATEGORY_TEMPLATES', 'templates');
+define('BX_DOL_STUDIO_STG_CATEGORY_AUDIT', 'audit');
+
+define('BX_DOL_STUDIO_STG_MIX_SYSTEM', 'system');
+define('BX_DOL_STUDIO_STG_MIX_DEFAULT', BX_DOL_STUDIO_STG_MIX_SYSTEM);
+
+class BxDolStudioOptions extends BxDol
+{
+    protected $oDb;
+    protected $sParamPrefix;
+    protected $sBaseUrl;
+
+    protected $sType;
+    protected $sCategory;
+    protected $aCategories;
+    protected $aCustomCategories;
+
+    protected $bReadOnly;	//--- Enable/Disable ReadOnly mode for system settings. Note. It doesn't affect Editable mixes.
+
+    protected $bMixes;	//--- Enable/Disable mixes feature.
+    protected $sMix;
+    protected $aMix;
+
+    protected $sStorage;
+    protected $sTranscoder;
+
+    protected $sErrorMessage;
+
+    function __construct($sType = '', $mixedCategory = '', $sMix = '')
+    {
+        parent::__construct();
+
+        $this->oDb = new BxDolStudioOptionsQuery();
+
+        $this->sParamPrefix = 'opt';
+        $this->sBaseUrl = BX_DOL_URL_STUDIO . 'options.php';
+
+        $this->sType = BX_DOL_STUDIO_STG_TYPE_DEFAULT;
+        if(!empty($sType) && is_string($sType))
+            $this->sType = $sType;
+
+        $this->sCategory = '';
+        if(!empty($mixedCategory)) {
+            if(is_array($mixedCategory))
+                $this->sCategory = $mixedCategory;
+            else if(is_string($mixedCategory)) {
+                $this->sCategory = json_decode($mixedCategory);
+                if(empty($this->sCategory))
+                    $this->sCategory = $mixedCategory;
+            }
+        }
+
+        $this->bReadOnly = false;
+
+        $this->bMixes = false;
+        $this->sMix = $sMix;
+        $this->aMix = [];
+
+        $this->sStorage = 'sys_images_custom';
+        $this->sTranscoder = 'sys_custom_images';
+
+        $this->sErrorMessage = '';
+    }
+
+    public function checkAction()
+    {
+        $sAction = bx_get($this->sParamPrefix . '_action');
+    	if($sAction === false)
+            return false;
+
+        $sAction = bx_process_input($sAction);
+
+        $sValue = '';
+        if(bx_get($this->sParamPrefix . '_value') !== false)
+            $sValue = bx_process_input(bx_get($this->sParamPrefix . '_value'));
+
+        $aResult = ['code' => 0, 'message' => ''];
+        switch($sAction) {
+            case 'select-mix':
+                $aResult = array_merge($aResult, $this->selectMix($sValue));
+                break;
+
+            case 'create-mix':
+                $aResult = array_merge($aResult, $this->getPopupCodeCreateMix());
+                break;
+
+            case 'import-mix':
+                $aResult = array_merge($aResult, $this->getPopupCodeImportMix());
+                break;
+
+            case 'export-mix':
+                $aResult = array_merge($aResult, $this->exportMix((int)$sValue));
+                break;
+
+            case 'download-mix':
+                $aResult = array_merge($aResult, $this->downloadMix((int)$sValue));
+                break;
+
+            case 'publish-mix':
+                $aResult = array_merge($aResult, $this->publishMix((int)$sValue));
+                break;
+
+            case 'hide-mix':
+                $aResult = array_merge($aResult, $this->hideMix((int)$sValue));
+                break;
+
+            case 'delete-mix':
+                $aResult = array_merge($aResult, $this->deleteMix((int)$sValue));
+                break;
+
+            case 'get-page-by-type':
+                $this->sType = $sValue;
+                $aResult['content'] = $this->getPageCode();
+                break;
+        }
+
+        return $aResult;
+    }
+
+    public function getType()
+    {
+        return $this->sType;
+    }
+
+    public function enableReadOnly($bReadOnly = true)
+    {
+    	$this->bReadOnly = $bReadOnly;
+    }
+
+    public function enableMixes($bMixes = true)
+    {
+    	$this->bMixes = $bMixes;
+    }
+
+    public function selectMix($sName)
+    {
+        $this->oDb->updateMixes(['active' => 0], [
+            'type' => $this->sType,
+            'category' => is_string($this->sCategory) ? $this->sCategory : '',
+            'active' => 1
+        ]);
+
+        $aResult = [];
+        if($sName == BX_DOL_STUDIO_STG_MIX_SYSTEM || $this->oDb->updateMixes(['active' => 1], ['name' => $sName])) {
+            $this->clearCache();
+
+            $aResult = ['eval' => $this->getJsObject() . '.onMixSelect(oData);'];
+        }
+        else 
+            $aResult = ['message' => _t('_adm_stg_err_cannot_perform')];
+
+        return $aResult;
+    }
+
+    public function exportMix($iId)
+    {
+    	$aMix = [];
+    	$this->oDb->getMixes(['type' => 'by_id', 'value' => $iId], $aMix, false);
+    	if(empty($aMix) || !is_array($aMix))
+            return array('message' => _t('_adm_stg_err_cannot_perform'));
+
+        return [
+            'url' => bx_append_url_params($this->sBaseUrl, [
+                $this->sParamPrefix . '_action' => 'download-mix',
+                $this->sParamPrefix . '_value' => $iId,
+            ]),
+            'eval' => $this->getJsObject() . '.onMixExport(oData);'
+        ];
+    }
+
+    public function downloadMix($iId)
+    {
+    	$aMix = [];
+    	$this->oDb->getMixes(['type' => 'by_id', 'value' => $iId], $aMix, false);
+    	if(empty($aMix) || !is_array($aMix))
+            BxDolStudioTemplate::getInstance()->displayPageNotFound();
+
+        $aOptions = [];
+        $this->oDb->getMixesOptions(['type' => 'by_mix_id_pair_option_value', 'value' => $iId, 'for_export' => 1], $aOptions, false);
+
+    	$sContent = json_encode([
+            'mix' => [
+                'type' => $aMix['type'],
+                'category' => $aMix['category'],
+                'name' => $aMix['name'],
+                'title' => $aMix['title']
+            ],
+            'options' => $aOptions
+    	]);
+
+    	header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
+        header("Content-type: application/json");
+        header("Content-Length: " . strlen($sContent));
+        header("Content-Disposition: attachment; filename=\"". strtolower($aMix['name']) . ".json\"");
+
+        echo $sContent;
+        exit;
+    }
+
+    public function publishMix($iId)
+    {
+    	$aResult = [];
+        if($this->oDb->updateMixes(['published' => 1], ['id' => $iId])) {
+            $this->clearCache();
+
+            $aResult = ['eval' => $this->getJsObject() . '.onMixPublish(oData);'];
+        }
+        else 
+            $aResult = ['message' => _t('_adm_stg_err_cannot_perform')];
+
+        return $aResult;
+    }
+
+    public function hideMix($iId)
+    {
+    	$aResult = [];
+        if($this->oDb->updateMixes(['published' => 0], ['id' => $iId])) {
+            $this->clearCache();
+
+            $aResult = ['eval' => $this->getJsObject() . '.onMixHide(oData);'];
+        }
+        else 
+            $aResult = ['message' => _t('_adm_stg_err_cannot_perform')];
+
+        return $aResult;
+    }
+
+    public function deleteMix($iId)
+    {
+    	$aResult = [];
+    	if($this->oDb->deleteMixesOptions(['mix_id' => $iId]) && $this->oDb->deleteMixes(['id' => $iId])) {
+    	    $this->clearCache();
+
+            $aResult = ['eval' => $this->getJsObject() . '.onMixDelete(oData);'];
+    	}
+    	else 
+            $aResult = ['message' => _t('_adm_stg_err_cannot_perform')]; 
+
+    	return $aResult;
+    }
+
+    public function saveChanges(&$oForm)
+    {
+    	$iMixId = $oForm->getCleanValue('mix_id');
+        $aCategories = explode(',', $oForm->getCleanValue('categories'));
+        $sEvalRenewToken = $this->getJsObject() . ".onSubmitted('" . $oForm->getId() . "', '" . $oForm->getCsrfToken() . "', oData);";
+
+        foreach ($aCategories as $sCategory) {
+            $aOptions = [];
+            $iOptions = $this->oDb->getOptions(['type' => 'by_category_name_full', 'value' => $sCategory], $aOptions);
+
+            $aData = [];
+            foreach($aOptions as $aOption) {
+                $aData[$aOption['name']] = $this->getSubmittedValue($aOption, $oForm);
+                if($aData[$aOption['name']] === false && !empty($this->sErrorMessage)) {
+                    $this->sCategory = $sCategory;
+                    return bx_get_js_result([
+                        'code' => 1,
+                        'message' => _t('_adm_stg_err_save_error_message', _t($aOption['caption']), _t($this->sErrorMessage)),
+                        'translate' => false,
+                        'eval' => $sEvalRenewToken
+                    ]);
+                }
+
+                if(!empty($aOption['check'])) {
+                    $sCheckerHelper = '';
+                    if(!empty($aOption['type_name']) && BxDolRequest::serviceExists($aOption['type_name'], 'get_settings_checker_helper'))
+                        $sCheckerHelper = BxDolService::call($aOption['type_name'], 'get_settings_checker_helper');
+
+                    if($sCheckerHelper == '') {
+                        bx_import('BxDolStudioForm');
+                        $sCheckerHelper = 'BxDolStudioFormCheckerHelper';
+                    }
+
+                    $oChecker = new $sCheckerHelper();
+                    $aCheckFunction = [$oChecker, 'check' . bx_gen_method_name($aOption['check'])];
+                    $aCheckFunctionParams = [$aData[$aOption['name']]];
+                    if(!empty($aOption['check_params']))
+                        $aCheckFunctionParams = array_merge($aCheckFunctionParams, array_values(unserialize($aOption['check_params'])));
+
+                    if(is_callable($aCheckFunction) && !call_user_func_array($aCheckFunction, $aCheckFunctionParams)) {
+                        $this->sCategory = $sCategory;
+                        return bx_get_js_result([
+                            'code' => 2,
+                            'message' => _t('_adm_stg_err_save_error_message', _t($aOption['caption']), _t($aOption['check_error'])), 
+                            'translate' => false, 
+                            'eval' => $sEvalRenewToken
+                        ]);
+                    }
+                }
+
+                if(isset($aData[$aOption['name']]))
+                    $aData[$aOption['name']] = $this->getProcessedValue($aOption, $aData[$aOption['name']]);
+                else
+                    $aData[$aOption['name']] = $this->getEmptyValue($aOption);
+
+                $mixedValue = $this->oDb->getParam($aOption['name']);
+                if($this->oDb->setParam($aOption['name'], $aData[$aOption['name']], $iMixId)) {
+                    $aCategoryInfo = [];
+                    $this->oDb->getCategories(['type' => 'by_name', 'value' => $sCategory], $aCategoryInfo, false);
+
+                    bx_alert('system', 'save_setting', 0, 0, [
+                        'category' => $aCategoryInfo, 
+                        'option' => $aOption['name'], 
+                        'value' => $aData[$aOption['name']],
+                        'value_prior' => $mixedValue
+                    ]);
+                }
+            }
+        }
+
+        $this->clearCache();
+        return bx_get_js_result([
+            'code' => 0,
+            'message' => '_adm_stg_scs_save',
+            'eval' => $sEvalRenewToken
+        ]);
+    }
+
+    protected function isReadOnly()
+    {
+    	$bMix = !empty($this->aMix);
+    	return (!$bMix && $this->bReadOnly) || ($bMix && (int)$this->aMix['editable'] == 0);
+    }
+
+    protected function getSubmittedValue($aOption, &$oForm)
+    {
+    	$mixedValue = '';
+
+    	switch($aOption['type']) {
+            case 'image':
+                $mixedValue = (int)getParam($aOption['name']);
+
+                $mixedIds = $oForm->getCleanValue($aOption['name']);
+                if(empty($mixedIds)) {
+                    if(!empty($mixedValue))
+                        $mixedValue = 0;
+
+                    break;
+                }
+
+                $oStorage = BxDolStorage::getObjectInstance($this->sStorage);
+                if(!$oStorage)
+                    break;
+
+                //--- Concatenation integer values as strings is required to get unique content id
+                $iContentId = (int)($aOption['id'] . (int)$oForm->getCleanValue('mix_id'));
+                $iImageId = is_array($mixedIds) ? (int)array_shift($mixedIds) : (int)$mixedIds;
+
+                $oStorage->updateGhostsContentId($iImageId, false, $iContentId);
+                $mixedValue = $iImageId;
+                break;
+
+            default: 
+                $mixedValue = $oForm->getCleanValue($aOption['name']);
+    	}
+
+    	return $mixedValue;
+    }
+
+    protected function getProcessedValue($aOption, $mixedValue)
+    {
+        $sMethod = 'processCustomValue' . bx_gen_method_name(trim(str_replace($this->sType, '', $aOption['name']), '_'));
+    	if(method_exists($this, $sMethod))
+    	    return $this->$sMethod($aOption, $mixedValue);
+
+        if(is_array($mixedValue))
+            $mixedValue = implode(',', $mixedValue);
+
+        return $mixedValue;
+    }
+
+    protected function getEmptyValue($aOption)
+    {
+        $mixedValue = '';
+
+        switch($aOption['type']) {
+            case 'digit':
+                $mixedValue = 0;
+                break;
+
+            case 'select':
+                if (BxDolService::isSerializedService($aOption['extra']))
+                    $aValues = BxDolService::callSerialized($aOption['extra']);
+                else
+                    $aValues = explode(',', $aOption['extra']);
+                $mixedValue = $aValues[0];
+                break;
+
+            case 'text':
+            case 'checkbox':
+            case 'file':
+                $mixedValue = "";
+                break;
+        }
+
+        return $mixedValue;
+    }
+
+    protected function clearCache()
+    {
+        BxDolDb::getInstance()->cacheParams(true, true);
+
+        $oCacheUtilities = BxDolCacheUtilities::getInstance();
+        $oCacheUtilities->clear('db');
+        $oCacheUtilities->clear('css');
+    }
+
+    protected function getCustomValueCurrencySign($aItem, $mixedValue)
+    {
+        return htmlspecialchars($mixedValue);
+    }
+
+    protected function processCustomValueCurrencySign($aItem, $mixedValue)
+    {
+        return htmlspecialchars_decode($mixedValue);
+    }
+}
+
+/** @} */

+ 65 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/template/images/icons/wi-api.svg

@@ -0,0 +1,65 @@
+<svg width="320" height="320" viewBox="0 0 320 320" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_740_4577)">
+<path d="M160 0C281.326 0 320 38.6737 320 160C320 281.326 281.326 320 160 320C38.6736 320 0 281.326 0 160C0 38.6737 38.6736 0 160 0Z" fill="url(#paint0_linear_740_4577)"/>
+<path d="M160 2C220.573 2 259.85 11.6833 284.083 35.9168C308.317 60.1504 318 99.4272 318 160C318 220.573 308.317 259.85 284.083 284.083C259.85 308.317 220.573 318 160 318C99.4272 318 60.1504 308.317 35.9168 284.083C11.6833 259.85 2 220.573 2 160C2 99.4272 11.6833 60.1504 35.9168 35.9168C60.1504 11.6833 99.4272 2 160 2Z" stroke="#F8FAFC" stroke-opacity="0.2" stroke-width="4"/>
+<g clip-path="url(#clip1_740_4577)">
+<path d="M89.0546 96.1256C88.1799 95.2508 88.3046 93.8024 89.2529 93.008C90.6124 91.8693 91.8694 90.6122 93.0082 89.2528C93.8025 88.3045 95.251 88.1798 96.1257 89.0545L107.251 100.18C108.106 101.034 108.01 102.443 107.096 103.234C105.715 104.429 104.429 105.714 103.234 107.096C102.443 108.009 101.035 108.105 100.18 107.251L89.0546 96.1256Z" fill="url(#paint1_linear_740_4577)"/>
+<path d="M213.59 220.661C212.715 219.786 212.84 218.338 213.788 217.543C215.148 216.405 216.405 215.148 217.543 213.788C218.338 212.84 219.786 212.715 220.661 213.59L231.786 224.715C232.641 225.57 232.545 226.978 231.631 227.769C230.25 228.964 228.964 230.25 227.769 231.631C226.978 232.545 225.57 232.641 224.715 231.786L213.59 220.661Z" fill="url(#paint2_linear_740_4577)"/>
+<path d="M220.661 106.785C219.786 107.66 218.338 107.535 217.543 106.587C216.405 105.228 215.148 103.97 213.788 102.832C212.84 102.037 212.715 100.589 213.59 99.7142L224.715 88.5888C225.57 87.7343 226.978 87.8303 227.769 88.7442C228.964 90.1253 230.25 91.4112 231.631 92.6061C232.545 93.3968 232.641 94.8053 231.786 95.6599L220.661 106.785Z" fill="url(#paint3_linear_740_4577)"/>
+<path d="M99.7142 213.59C100.589 212.715 102.037 212.84 102.832 213.788C103.97 215.148 105.228 216.405 106.587 217.543C107.535 218.338 107.66 219.786 106.785 220.661L95.6599 231.786C94.8053 232.641 93.3968 232.545 92.6061 231.631C91.4112 230.25 90.1253 228.964 88.7442 227.769C87.8303 226.978 87.7343 225.57 88.5888 224.715L99.7142 213.59Z" fill="url(#paint4_linear_740_4577)"/>
+<path d="M160 100C205.497 100 220 114.503 220 160C220 205.497 205.497 220 160 220C114.503 220 100 205.497 100 160C100 114.503 114.503 100 160 100Z" fill="url(#paint5_linear_740_4577)"/>
+<circle cx="70" cy="70" r="20" fill="url(#paint6_linear_740_4577)"/>
+<circle cx="70" cy="250" r="20" fill="url(#paint7_linear_740_4577)"/>
+<circle cx="250" cy="70" r="20" fill="url(#paint8_linear_740_4577)"/>
+<circle cx="250" cy="250" r="20" fill="url(#paint9_linear_740_4577)"/>
+</g>
+</g>
+<defs>
+<linearGradient id="paint0_linear_740_4577" x1="160" y1="0" x2="160" y2="320" gradientUnits="userSpaceOnUse">
+<stop stop-color="#475569"/>
+<stop offset="1" stop-color="#1E293B"/>
+</linearGradient>
+<linearGradient id="paint1_linear_740_4577" x1="160.188" y1="88" x2="160.188" y2="232.375" gradientUnits="userSpaceOnUse">
+<stop stop-color="#94A3B8"/>
+<stop offset="1" stop-color="#64748B"/>
+</linearGradient>
+<linearGradient id="paint2_linear_740_4577" x1="160.188" y1="88" x2="160.188" y2="232.375" gradientUnits="userSpaceOnUse">
+<stop stop-color="#94A3B8"/>
+<stop offset="1" stop-color="#64748B"/>
+</linearGradient>
+<linearGradient id="paint3_linear_740_4577" x1="160.188" y1="88" x2="160.188" y2="232.375" gradientUnits="userSpaceOnUse">
+<stop stop-color="#94A3B8"/>
+<stop offset="1" stop-color="#64748B"/>
+</linearGradient>
+<linearGradient id="paint4_linear_740_4577" x1="160.188" y1="88" x2="160.188" y2="232.375" gradientUnits="userSpaceOnUse">
+<stop stop-color="#94A3B8"/>
+<stop offset="1" stop-color="#64748B"/>
+</linearGradient>
+<linearGradient id="paint5_linear_740_4577" x1="160" y1="100" x2="160" y2="220" gradientUnits="userSpaceOnUse">
+<stop stop-color="#2DD4BF"/>
+<stop offset="1" stop-color="#0D9488"/>
+</linearGradient>
+<linearGradient id="paint6_linear_740_4577" x1="70" y1="50" x2="70" y2="90" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FB7185"/>
+<stop offset="1" stop-color="#E11D48"/>
+</linearGradient>
+<linearGradient id="paint7_linear_740_4577" x1="70" y1="230" x2="70" y2="270" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FB923C"/>
+<stop offset="1" stop-color="#EA580C"/>
+</linearGradient>
+<linearGradient id="paint8_linear_740_4577" x1="250" y1="50" x2="250" y2="90" gradientUnits="userSpaceOnUse">
+<stop stop-color="#34D399"/>
+<stop offset="1" stop-color="#059669"/>
+</linearGradient>
+<linearGradient id="paint9_linear_740_4577" x1="250" y1="230" x2="250" y2="270" gradientUnits="userSpaceOnUse">
+<stop stop-color="#60A5FA"/>
+<stop offset="1" stop-color="#2563EB"/>
+</linearGradient>
+<clipPath id="clip0_740_4577">
+<rect width="320" height="320" fill="white"/>
+</clipPath>
+<clipPath id="clip1_740_4577">
+<rect width="240" height="240" fill="white" transform="translate(40 40)"/>
+</clipPath>
+</defs>
+</svg>

+ 93 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/template/scripts/BxBaseStudioAPI.php

@@ -0,0 +1,93 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaView UNA Studio Representation classes
+ * @ingroup     UnaStudio
+ * @{
+ */
+
+class BxBaseStudioAPI extends BxDolStudioAPI
+{
+    protected $sSubpageUrl;
+    protected $aMenuItems;
+    protected $aGridObjects;
+
+    public function __construct($sPage = '')
+    {
+        parent::__construct($sPage);
+
+        $this->aPageCss = array_merge($this->aPageCss, ['forms.css', 'paginate.css']);
+        
+        $this->sSubpageUrl = BX_DOL_URL_STUDIO . 'api.php?page=';
+
+        $this->aMenuItems = array(
+            BX_DOL_STUDIO_API_TYPE_SETTINGS => array('icon' => 'cogs'),
+            BX_DOL_STUDIO_API_TYPE_ORIGINS => array('icon' => 'globe'),
+            BX_DOL_STUDIO_API_TYPE_KEYS => array('icon' => 'key'),
+        );
+
+        $this->aGridObjects = array(
+            'keys' => 'sys_studio_api_keys',
+            'origins' => 'sys_studio_api_origins',
+        );
+    }
+
+    public function getPageJsCode($aOptions = array(), $bWrap = true)
+    {
+        $aOptions = array_merge($aOptions, array(
+            'sActionUrl' => BX_DOL_URL_STUDIO . 'api.php'
+        ));
+
+        return parent::getPageJsCode($aOptions, $bWrap);
+    }
+
+    public function getPageMenu($aMenu = array(), $aMarkers = array())
+    {
+        $sJsObject = $this->getPageJsObject();
+
+        $aMenu = array();
+        foreach($this->aMenuItems as $sMenuItem => $aItem)
+            $aMenu[] = array(
+                'name' => $sMenuItem,
+                'icon' => $aItem['icon'],
+                'link' => $this->sSubpageUrl . $sMenuItem,
+                'title' => _t('_adm_lmi_cpt_' . $sMenuItem),
+                'selected' => $sMenuItem == $this->sPage
+            );
+
+        return parent::getPageMenu($aMenu);
+    }
+
+    protected function getSettings()
+    {
+        $oOptions = new BxTemplStudioOptions(BX_DOL_STUDIO_STG_TYPE_DEFAULT, 'api');
+
+        $this->aPageCss = array_merge($this->aPageCss, $oOptions->getCss());
+        $this->aPageJs = array_merge($this->aPageJs, $oOptions->getJs());
+        
+        return $oOptions->getCode();
+    }
+
+    protected function getKeys()
+    {
+        return $this->getGrid($this->aGridObjects['keys']);
+    }
+
+    protected function getOrigins()
+    {
+        return $this->getGrid($this->aGridObjects['origins']);
+    }
+
+    protected function getGrid($sObjectName)
+    {
+        $oGrid = BxDolGrid::getObjectInstance($sObjectName);
+        if(!$oGrid)
+            return '';
+
+        return $oGrid->getCode();
+    }
+}
+
+/** @} */

+ 19 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/studio/template/scripts_templ/BxTemplStudioAPI.php

@@ -0,0 +1,19 @@
+<?php
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaStudioView UNA Studio Representation classes
+ * @ingroup     UnaStudio
+ * @{
+ */
+defined('BX_DOL') or die('hack attempt');
+
+class BxTemplStudioAPI extends BxBaseStudioAPI
+{
+    function __construct($sPage = '')
+    {
+        parent::__construct($sPage);
+    }
+}
+/** @} */

+ 32 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/calendar.html

@@ -0,0 +1,32 @@
+<div id="__id__"></div>
+<script>
+    var oDefaults = {
+        headerToolbar: {
+            initialView: 'dayGridMonth',
+            left: 'prev,next today',
+            center: 'title',
+            right: 'dayGridDay,dayGridWeek,dayGridMonth' 
+        },
+        navLinks: true, // can click day/week names to navigate views
+        eventClassNames: function(oParams) {
+            if(oParams.view.type != 'dayGridDay' || !oParams.event.extendedProps.class)
+                return;
+
+            return oParams.event.extendedProps.class;
+        },
+        eventContent: function(oParams) {
+            if(oParams.view.type != 'dayGridDay' || !oParams.event.extendedProps.card)
+                return oParams.event._def.title;
+
+            oParams.event.url = '';
+            return {html: oParams.event.extendedProps.card};
+        }
+    };
+    var oOptions = __options__;
+    
+    var oBxFullCalendar = null;
+    document.addEventListener('DOMContentLoaded', function() {
+        oBxFullCalendar = new FullCalendar.Calendar(document.getElementById('__id__'), $.extend({}, oDefaults, oOptions));
+        oBxFullCalendar.render();
+    });
+</script>

+ 12 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/comments_uploader_nfw.html

@@ -0,0 +1,12 @@
+<div id="bx-uploader-file-{storage_object}-{file_id}" class="bx-uploader-ghost cmts-uploader-file flex items-start mt-2">
+    <div class="bx-uploader-ghost-cnt">
+        <input type="hidden" name="__name__[]" value="{file_id}" />
+        <div class="cmts-uploader-file-preview rounded">
+            <img src="{file_icon}" class="bx-base-general-icon bx-base-general-icon-{file_id} rounded" />
+        </div>
+        <span class="bx-base-general-uploader-ghost">
+            <a class="bx-btn bx-btn-small mr-2" href="javascript:void(0);" title="<bx_text:_sys_txt_form_entry_input_picture_delete />" onclick="{js_instance_name}.deleteGhost('{file_id}');"><i class="sys-icon times"></i></a>
+            <a class="bx-btn bx-btn-small mr-2" href="javascript:void(0);" title="<bx_text:_sys_txt_form_entry_input_picture_copy />" onclick="bx_copy_to_clipboard('{file_url}');"><i class="sys-icon copy"></i></a>
+        </span>
+    </div>
+</div>

+ 177 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/css/uploaders.css

@@ -0,0 +1,177 @@
+
+.bx-uploader-msg-err {
+    color:#900;
+}
+.bx-uploader-ghost{
+    max-width: 290px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    grid-gap: 0.5rem;
+}
+.bx-uploader-ghost.bx-uploader-ghost-single{
+    width: auto;
+}
+.bx-uploader-ghost-cnt{
+    display: flex;
+    flex-direction: column;
+    position: relative;
+    min-width: 100px;
+    min-height: 40px;
+}
+
+.bx-form-input-files-result{
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: flex-start;
+    grid-gap: 0.5rem;
+}
+.bx-base-general-icon-wrapper{
+    position: relative;
+    display: flex;
+    justify-content: center;
+}
+.bx-base-general-uploader-ghost{
+    position: absolute;
+    bottom: 0.5rem;
+    display: flex;
+    justify-content: center;
+    width: 100%;
+    flex-wrap: wrap;
+    grid-gap: 0.5rem;
+}
+.bx-base-general-uploader-ghost .bx-btn.bx-btn-small{
+    height: auto !important;
+    line-height: normal !important;
+    min-height: 30px;
+    display: flex;
+    padding: 0 0.5rem;
+    margin: 0px;
+}
+
+.bx-uploader-ghost-preview {
+    width:101px; 
+    float:left;
+}
+
+.bx-uploader-ghost-preview img {
+    width:101px;
+    min-height:20px;
+    background-image:url(../images/loading-spinner.gif);
+    background-repeat:no-repeat;
+    background-position:center center;
+    -webkit-box-reflect: below 0px -webkit-gradient(linear, left top, left bottom, from(transparent), color-stop(.7, transparent), to(rgba(0,0,0,0.4)));
+}
+
+.bx-uploader-ghost-form-filename {
+
+}
+ 
+.bx-uploader-ghost-form-actions {
+}
+
+/* simple uploader */
+
+.bx-uploader-simple-file {
+    margin-bottom:5px;
+}
+
+/* html5 filepond uploader */
+.bx-uploader-html5-drop-area .filepond--item {
+    width: calc(33% - 0.5em);
+}
+.filepond--panel-root{
+    background: none !important;
+}
+.filepond--panel-center.filepond--panel-root{
+    visibility: visible !important;
+    transform: translate3d(0px, 8px, 0px) scale3d(1, 0.6, 1);
+}
+.filepond--panel-bottom.filepond--panel-root{
+    transform: translate3d(0px, 68px, 0px);
+}
+.bx-uploader-html5-drop-area{
+    max-height: 30em;
+}
+.filepond{
+    min-height: 76px;
+    margin-bottom: 0px !important;
+}
+.bx-uploader-html5-drop-area.filepond .dz-message {
+    margin:3.5em 0;
+}
+.bx-uploader-html5-errors-area {
+    max-height:2.75rem;
+    overflow-y:auto;
+}
+.bx-uploader-html5-paste-catcher {
+    width:0; 
+    height:0;
+    border:none;
+    margin:0;
+    padding:0;
+    float: right;
+}
+
+.border-dashed {
+    border-style: dashed;
+}
+.rounded-lg {
+    border-radius: .5rem;
+}
+
+/* crop uploader */
+
+.bx-popup-content-wrapped .bx-croppie-element {
+    padding:0;
+    min-height:340px;
+}
+
+.bx-popup-content-wrapped .bx-crop-action {
+    display: block;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    overflow: hidden;
+}
+
+
+/* record video uploader */
+
+.bx-uploader-record-video-controls {
+    text-align: center;
+    display: none;
+}
+.bx-uploader-record-video-controls .bx-btn {
+    float: none;
+    display: inline-block;
+}
+
+.bx-uploader-recording-stop {
+    display: none;
+}
+
+#bx-upoloader-recording-preview {
+    display: none;
+}
+
+.bx-record-video-preview-filesize {
+    text-align: center;
+    margin: 0.2rem 0;
+}
+.bx-record-camera-switch {
+    margin-bottom: 0.5rem;
+    text-align: right;
+    display: none;
+}
+
+@keyframes bx-uploader-recording-animation {
+    0% {opacity: 0;}
+    50% {opacity: 1;}
+    100% {opacity: 0;}
+}
+.bx-uploader-recording-blinking-circle {
+    animation-name: bx-uploader-recording-animation;
+    animation-duration: 2s;
+    animation-iteration-count: infinite;
+}

+ 1596 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseCmts.php

@@ -0,0 +1,1596 @@
+<?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
+ * @{
+ */
+
+/**
+ * @see BxDolCmts
+ */
+class BxBaseCmts extends BxDolCmts
+{
+    protected static $_sTmplContentElementBlock;
+    protected static $_sTmplContentElementInline;
+    protected static $_sTmplContentDoCommentLabel;
+    protected static $_sTmplContentCounter;
+
+    protected $_sTmplNameItem;
+    protected $_sTmplNameItemContent;
+
+    protected $_sJsObjClass;
+    protected $_sJsObjName;
+    protected $_sStylePrefix;
+
+    protected $_aHtmlIds;
+
+    protected $_aElementDefaults;
+    
+    protected $_aAclId2Name;
+
+    function __construct( $sSystem, $iId, $iInit = true, $oTemplate = false)
+    {
+        parent::__construct($sSystem, $iId, $iInit, $oTemplate);
+        if (empty($sSystem))
+            return;
+
+        $this->_sTmplNameItem = 'comment.html';
+        $this->_sTmplNameItemContent = 'comment_content.html';
+
+        $this->_sJsObjClass = 'BxDolCmts';
+        $this->_sJsObjName = 'oCmts' . bx_gen_method_name($sSystem, array('_' , '-')) . '_' .str_replace('-', 'n', $iId);
+        $this->_sStylePrefix = isset($this->_aSystem['root_style_prefix']) ? $this->_aSystem['root_style_prefix'] : 'cmt';
+
+        $sHtmlId = str_replace(array('_' , ' '), array('-', '-'), $sSystem) . '-' . $iId;
+
+        $this->_aHtmlIds = array(
+            'main' => 'bx-cmt-' . $sHtmlId,
+            'counter' => 'bx-cmt-counter-' . $sHtmlId
+        );
+
+        $this->_aElementDefaults = array(
+            'show_do_comment_as_button' => false,
+            'show_do_comment_as_button_small' => false,
+            'show_do_comment_icon' => true,
+            'show_do_comment_label' => false,
+            'show_counter' => true,
+            'show_counter_only' => true,
+            'show_counter_empty' => false,
+            'show_counter_reversed' => false,
+            'recalculate_counter' => false
+        );
+
+        $this->_aAclId2Name = array();
+        $aAclLevels = BxDolAcl::getInstance()->getMemberships(false, false, false);
+        foreach($aAclLevels as $iAclId => $sAclName)
+            $this->_aAclId2Name[$iAclId] = str_replace('_', '-', str_replace('_adm_prm_txt_level_', '', $sAclName));
+
+        if(empty(self::$_sTmplContentElementBlock))
+            self::$_sTmplContentElementBlock = $this->_oTemplate->getHtml('comment_element_block.html');
+
+        if(empty(self::$_sTmplContentElementInline))
+            self::$_sTmplContentElementInline = $this->_oTemplate->getHtml('comment_element_inline.html');
+
+        if(empty(self::$_sTmplContentDoCommentLabel))
+            self::$_sTmplContentDoCommentLabel = $this->_oTemplate->getHtml('comment_do_comment_label.html');
+
+        if(empty(self::$_sTmplContentCounter))
+            self::$_sTmplContentCounter = $this->_oTemplate->getHtml('comment_counter.html');
+
+        $this->_oTemplate->addJsTranslation('_sys_txt_cmt_loading');
+    }
+
+    /**
+     * Add comments CSS/JS
+     */
+    public function addCssJs ()
+    {
+        $oForm = BxDolForm::getObjectInstance($this->_sFormObject, $this->_sFormDisplayPost);
+        $oForm->addCssJs();
+
+        $this->_oTemplate->addCss(array(
+            BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'photo-swipe/|photoswipe.css',
+            BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'photo-swipe/default-skin/|default-skin.css',
+        ));
+
+        $this->_oTemplate->addJs(array(
+            'photo-swipe/photoswipe.min.js',
+            'photo-swipe/photoswipe-ui-default.min.js',
+        ));
+    }
+
+    public function getJsObjectName()
+    {
+        return $this->_sJsObjName;
+    }
+
+    /**
+     * Get initialization section of comments box
+     *
+     * @return string
+     */
+    public function getJsScript($aBp = array(), $aDp = array())
+    {
+        $bMinPostForm = isset($aDp['min_post_form']) ? $aDp['min_post_form'] : $this->_bMinPostForm;
+
+        $aParams = array(
+            'sObjName' => $this->_sJsObjName,
+            'sRootUrl' => BX_DOL_URL_ROOT,
+            'sSystem' => $this->getSystemName(),
+            'iAuthorId' => $this->_getAuthorId(),
+            'iObjId' => $this->getId(),
+            'sBaseUrl' => $this->getBaseUrl(),
+            'sPostFormPosition' => $this->_aSystem['post_form_position'],
+            'sBrowseType' => $this->_sBrowseType,
+            'sDisplayType' => $this->_sDisplayType,
+            'iDisplayStructure' => isset($aDp['structure']) && !empty($aDp['structure']) ? 1 : 0,
+            'iMinPostForm' => $bMinPostForm ? 1 : 0,
+            'sStylePrefix' => $this->_sStylePrefix,
+            'aHtmlIds' => $this->_aHtmlIds,
+        );
+
+        $this->addCssJs();
+        return $this->_oTemplate->_wrapInTagJsCode("if(window['" . $this->_sJsObjName . "'] == undefined) var " . $this->_sJsObjName . " = new " . $this->_sJsObjClass . "(" . json_encode($aParams) . "); " . $this->_sJsObjName . ".cmtInit();");
+    }
+
+    function getCommentsBlockAPI($aParams, $aBp = [], $aDp = ['in_designbox' => false, 'show_empty' => false])
+    {
+
+        $mixedResult = $this->isViewAllowed();
+        if($mixedResult !== CHECK_ACTION_RESULT_ALLOWED)
+            return $mixedResult; // TODO: error checking
+
+        $aBp['type'] = 'head';
+        $this->_getParams($aBp, $aDp);
+        $this->_prepareParams($aBp, $aDp);
+        
+        $aBp['order']['way'] = 'desc';
+        $aBp['order_way'] =  'desc';
+        $aBp['start'] = 0 ; 
+        $aPp = $aBp['per_view'];
+        $aBp['per_view'] =  $aBp['per_view'] + 1; 
+
+        $aCmts = [];
+
+        if (!isset($aParams['comment_id']))
+            $aCmts = $this->getCommentsArray($aBp['vparent_id'], $aBp['filter'], $aBp['order'], $aBp['start'], $aBp[($aBp['init_view'] != -1 ? 'init' : 'per') . '_view']);
+        else
+            $aCmts = [['cmt_id' => $aParams['comment_id']]];
+        
+        $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) {
+            $aCmtsRv[] = $this->getCommentStructure($aCmt['cmt_id'], $aBp, $aDp);
+        }
+        return [
+            'unit' => 'comments',
+            'start' => 0,
+            'order' => 'a',
+            'module' => $this->_sSystem, 
+            'object_id' => $this->_iId,
+            'data' => $aCmtsRv,
+        ];
+        
+    }
+
+    /**
+     * get full comments block with initializations
+     */
+    function getCommentsBlock($aBp = [], $aDp = [])
+    {
+        $mixedResult = $this->isViewAllowed();
+        if($mixedResult !== CHECK_ACTION_RESULT_ALLOWED)
+            return $mixedResult;
+
+        $this->_getParams($aBp, $aDp);
+
+        //add live update
+        $this->actionResumeLiveUpdate();
+
+        if(($oLiveUpdates = BxDolLiveUpdates::getInstance())!== false) {
+            $sServiceCall = BxDolService::getSerializedService('system', 'get_live_update', [$this->_sSystem, $this->_iId, $this->_getAuthorId(), '{count}'], 'TemplCmtsServices');
+            $oLiveUpdates->add($this->_sSystem . '_live_updates_cmts_' . $this->_iId, 1, $sServiceCall);
+        }
+        //add live update
+
+        $sComments = $this->getComments($aBp, $aDp);
+        $sCommentsPinned = $this->getCommentsPinned(array_merge($aBp, ['pinned' => 1]), $aDp);
+        $sContentBefore = $this->_getContentBefore($aBp, $aDp);
+        $sContentAfter = $this->_getContentAfter($aBp, $aDp);
+        $sPostFormTop = $this->getFormBoxPost($aBp, array_merge($aDp, ['type' => $this->_sDisplayType, 'position' => BX_CMT_PFP_TOP]));
+        $sPostFormBottom = $this->getFormBoxPost($aBp, array_merge($aDp, ['type' => $this->_sDisplayType, 'position' => BX_CMT_PFP_BOTTOM]));
+        $sJsContent = $this->getJsScript($aBp, $aDp);
+
+        $sBlockTitle = _t($this->_aT['block_comments_title'], $this->getCommentsCountAll(0, true));
+        $sBlockMenu = $this->_getControlsBox();
+
+        bx_alert('system', 'view_comments', 0, 0, [
+            'object' => $this,
+            'system' => $this->_sSystem,
+            'id' => $this->getId(),
+            'params_browse' => $aBp,
+            'params_display' => $aDp,
+            'post_form_top' => &$sPostFormTop,
+            'content_before' => &$sContentBefore,
+            'comments' => &$sComments,
+            'comments_pinned' => &$sCommentsPinned,
+            'content_after' => &$sContentAfter,
+            'post_form_bottom'  => &$sPostFormBottom,
+            'js_content' => &$sJsContent,
+            'block_title' => &$sBlockTitle,
+            'block_menu' => &$sBlockMenu,
+        ]);
+
+        $sContent = $this->_oTemplate->parseHtmlByName('comments_block.html', [
+            'system' => $this->_sSystem,
+            'list_anchor' => $this->getListAnchor(),
+            'id' => $this->getId(),
+            'content_before' => $sContentBefore,
+            'comments' => $sComments,
+            'comments_pinned' => $sCommentsPinned,
+            'bx_if:show_divider_hidden' => [
+                'condition' => empty($sCommentsPinned),
+                'content' => []
+            ],
+            'content_after' => $sContentAfter,
+            'post_form_top' => $sPostFormTop,
+            'post_form_bottom'  => $sPostFormBottom,
+            'image_preview' => $this->_oTemplate->parseHtmlByName('comments_photoswipe.html', []),
+            'script' => $sJsContent
+        ]);
+
+        return $aDp['in_designbox'] ? DesignBoxContent($sBlockTitle, $sContent, BX_DB_DEF, $sBlockMenu) : [
+            'title' => $sBlockTitle,
+            'content' => $sContent,
+            'menu' => $sBlockMenu,
+        ];
+    }
+
+    /**
+     * get comments list for specified parent comment
+     *
+     * @param array $aBp - browse params array
+     * @param array $aDp - display params array
+     *
+     */
+    function getComments($aBp = [], $aDp = [])
+    {
+        $this->_prepareParams($aBp, $aDp);
+
+        $aCmts = $this->getCommentsArray($aBp['vparent_id'], $aBp['filter'], $aBp['order'], $aBp['start'], $aBp[($aBp['init_view'] != -1 ? 'init' : 'per') . '_view']);
+        if(empty($aCmts) || !is_array($aCmts))
+            return isset($aDp['show_empty']) && $aDp['show_empty'] === true ? $this->_getEmpty($aDp) : '';
+
+        $sCmts = '';
+        foreach($aCmts as $k => $aCmt)
+            $sCmts .= $this->getComment($aCmt, $aBp, $aDp);
+        $sCmts = $this->_getMoreLink($sCmts, $aBp, $aDp);       
+
+        return $sCmts;
+    }
+
+    function getCommentsPinned($aBp = [], $aDp = [])
+    {
+        $this->_prepareParams($aBp, $aDp);
+
+        $aCmts = $this->getCommentsArray($aBp['vparent_id'], BX_CMT_FILTER_PINNED, $aBp['order'], 0, -1);
+        if(empty($aCmts) || !is_array($aCmts))
+            return '';
+
+        $sCmts = '';
+        foreach($aCmts as $k => $aCmt)
+            $sCmts .= $this->getComment($aCmt, $aBp, $aDp);
+
+        return $sCmts;
+    }
+
+    public function getCommentsByStructure($aBp = array(), $aDp = array())
+    {
+        if(empty($aDp['structure']))
+            return isset($aDp['show_empty']) && $aDp['show_empty'] === true ? $this->_getEmpty($aDp) : '';
+
+        $aBp['count'] = count($aDp['structure']);
+        $this->_prepareParams($aBp, $aDp);
+        $aDp['structure'] = array_slice($aDp['structure'], $aBp['start'], $aBp[($aBp['init_view'] != -1 ? 'init' : 'per') . '_view'], true);
+
+        $sCmts = '';
+        foreach($aDp['structure'] as $iCmtId => $aCmt)
+            $sCmts .= $this->getComment($iCmtId, $aBp, $aDp);
+
+        $sCmts = $this->_getMoreLink($sCmts, $aBp, $aDp);
+        return $sCmts;
+    }    
+
+    /**
+     * get comment view block with initializations
+     */
+    function getCommentBlock($iCmtId = 0, $aBp = array(), $aDp = array())
+    {
+        $mixedResult = $this->isViewAllowed();
+        if($mixedResult !== CHECK_ACTION_RESULT_ALLOWED)
+            return $mixedResult;
+
+        $aBp = array_merge(array('type' => $this->_sBrowseType), $aBp);
+        $aDp = array_merge(array('type' => BX_CMT_DISPLAY_THREADED), $aDp);
+
+        //--- Beg: Using pregenerated structure
+        $mixedStructure = $this->getCommentStructure((int)$iCmtId, $aBp, $aDp);
+        if($mixedStructure !== false)
+            $aDp['structure'] = $mixedStructure;
+        //--- End: Using pregenerated structure
+
+        $sComment = $this->getComment($iCmtId, $aBp, $aDp);
+        if (!$sComment)
+            return '';
+
+        BxDolTemplate::getInstance()->addInjection ('injection_footer', 'text', $this->_oTemplate->parseHtmlByName('comments_photoswipe.html', []));
+
+        return $this->_oTemplate->parseHtmlByName('comment_block.html', array(
+            'system' => $this->_sSystem,
+            'id' => $this->getId(),
+            'comment' => $sComment,
+            'script' => $this->getJsScript($aBp, $aDp)
+        ));
+    }
+    
+    /**
+     * get one just posted comment
+     *
+     * @param  int    $iCmtId - comment id
+     * @return string
+     */
+    function getComment($mixedCmt, $aBp = array(), $aDp = array())
+    {
+        $iUserId = $this->_getAuthorId();
+        $aCmt = !is_array($mixedCmt) ? $this->getCommentRow((int)$mixedCmt) : $mixedCmt;
+        if (!$aCmt)
+            return '';
+
+        list($sAuthorName, $sAuthorLink, $sAuthorIcon) = $this->_getAuthorInfo($aCmt['cmt_author_id']);
+
+        $sClass = $sClassCnt = '';
+        if(isset($aCmt['vote_rate']) && (float)$aCmt['vote_rate'] < $this->_aSystem['viewing_threshold']) {
+            $this->_oTemplate->pareseHtmlByName('comment_hidden.html', array(
+                'js_object' => $this->_sJsObjName,
+                'id' => $aCmt['cmt_id'],
+                'title' => bx_process_output(_t('_hidden_comment', $sAuthorName)),
+                'bx_if:show_replies' => array(
+                    'condition' => $aCmt['cmt_replies'] > 0,
+                    'content' => array(
+                        'replies' => _t('_Show N replies', $aCmt['cmt_replies'])
+                    )
+                )
+            ));
+
+            $sClass = ' ' . $this->_sStylePrefix . '-hidden';
+        }
+
+        if($aCmt['cmt_author_id'] == $iUserId)
+            $sClass .= ' ' . $this->_sStylePrefix . '-mine';
+
+        $aAuthorAcl = BxDolAcl::getInstance()->getMemberMembershipInfo($aCmt['cmt_author_id']);
+        if(!empty($aAuthorAcl) && isset($this->_aAclId2Name[$aAuthorAcl['id']]))
+            $sClass .= ' ' . $this->_sStylePrefix . '-aml-' . $this->_aAclId2Name[$aAuthorAcl['id']];
+
+        if(!empty($aDp['blink']) && in_array($aCmt['cmt_id'], $aDp['blink']))
+            $sClass .= ' ' . $this->_sStylePrefix . '-blink';
+
+        if(!empty($aDp['class_comment']))
+            $sClass .= ' ' . $aDp['class_comment'];
+
+        if(!empty($aDp['class_comment_content']))
+            $sClassCnt .= ' ' . $aDp['class_comment_content'];
+
+        $sActions = $this->_getActionsBox($aCmt, $aBp, $aDp);
+
+        $aTmplReplyTo = array();
+        if((int)$aCmt['cmt_parent_id'] != 0) {
+            $aParent = $this->getCommentRow($aCmt['cmt_parent_id']);
+
+            if(!empty($aParent) && is_array($aParent)) {
+                $oProfile = $this->_getAuthorObject($aParent['cmt_author_id']);
+                $sParAuthorName = $oProfile->getDisplayName();
+                $sParAuthorUnit = $oProfile->getUnit(0, array('template' => array('name' => 'unit_wo_info_links', 'size' => 'icon')));
+
+                $aTmplReplyTo = array(
+                    'style_prefix' => $this->_sStylePrefix,
+                    'par_cmt_link' => $this->getItemUrl($aCmt['cmt_parent_id']),
+                    'par_cmt_title' => bx_html_attribute(_t('_in_reply_to_x', $sParAuthorName)),
+                    'par_cmt_author' => $sParAuthorName,
+                    'par_cmt_author_unit' => $sParAuthorUnit
+                );
+            }
+        }
+
+        $sReplies = '';
+        if(!(isset($aBp['pinned']) && (int)$aBp['pinned'] != 0 && (int)$aCmt['cmt_pinned'] != 0) && !empty($aDp)) {
+            $aDp['show_empty'] = false;
+
+            if(!empty($aDp['structure'][$aCmt['cmt_id']]) && is_array($aDp['structure'][$aCmt['cmt_id']])) {
+                if(!empty($aDp['structure'][$aCmt['cmt_id']]['items'])) {
+                    $aDp['structure'] = $aDp['structure'][$aCmt['cmt_id']]['items'];
+                    $sReplies = $this->getCommentsByStructure(array('parent_id' => $aCmt['cmt_id'], 'type' => $aBp['type']), $aDp);
+                }
+            } 
+            else if((int)$aCmt['cmt_replies'] > 0 && $aDp['type'] == BX_CMT_DISPLAY_THREADED)
+                $sReplies = $this->getComments(array('parent_id' => $aCmt['cmt_id'], 'vparent_id' => $aCmt['cmt_id'], 'type' => $aBp['type']), $aDp);
+        }
+
+        $aTmplVarsMeta = array();
+        if(!empty($this->_sMenuObjMeta)) {
+            $oMenuMeta = BxDolMenu::getObjectInstance($this->_sMenuObjMeta, $this->_oTemplate);
+            if($oMenuMeta) {
+                $oMenuMeta->setCmtsData($this, $aCmt['cmt_id']);
+
+                $aTmplVarsMeta = array(
+                    'style_prefix' => $this->_sStylePrefix,
+                    'meta' => $oMenuMeta->getCode()
+                );
+            }
+        }
+
+        $oProfileAuthor = BxDolProfile::getInstance($aCmt['cmt_author_id']);
+        if($this->_isShowContent($aCmt)) {
+            $sContent = $this->_getContent($aCmt, $aBp, $aDp);
+        }
+        else {
+            $sClass .= ' cmt-author-not-active';
+            $sContent = _t('_hidden_comment', BxDolProfileUndefined::getInstance()->getDisplayName());
+        }
+
+        $aVars = array_merge(array(
+            'system' => $this->_sSystem,
+            'style_prefix' => $this->_sStylePrefix,
+            'js_object' => $this->_sJsObjName,
+            'id' => $aCmt['cmt_id'],
+            'anchor' => $this->getItemAnchor($aCmt['cmt_id']),
+            'class' => $sClass,
+            'class_cnt' => $sClassCnt,
+            'bx_if:show_reply_to' => array(
+                'condition' => !empty($aTmplReplyTo),
+                'content' => $aTmplReplyTo
+            ),
+            'bx_if:meta' => array(
+                'condition' => !empty($aTmplVarsMeta),
+                'content' => $aTmplVarsMeta
+            ),
+            'bx_if:show_pinned' => array(
+                'condition' => (int)$aCmt['cmt_pinned'] > 0,
+                'content' => array(
+                    'style_prefix' => $this->_sStylePrefix,
+                )
+            ),
+            'content' => $sContent,
+            'actions' => $sActions,
+            'replies' =>  $sReplies,
+        ), $this->_getTmplVarsAuthor($aCmt), $this->_getTmplVarsNotes($aCmt));
+        
+        $sResult = $this->_oTemplate->parseHtmlByName($this->_sTmplNameItem, $aVars);
+        
+        bx_alert('system', 'view_comment', $aCmt['cmt_id'], 0, array('comment' => $aCmt, 'system' => $this->_sSystem, 'tmpl_name' => $this->_sTmplNameItem, 'tmpl_vars' => $aVars, 'override_result' => &$sResult));
+        
+        return $sResult;
+    }
+
+    public function getCommentStructure($iCmtId, $aBp = array(), $aDp = array())
+    {
+        $aRoot = $this->getCommentRow((int)$iCmtId);
+
+        $aBps = $aBp;
+        if(!empty($aRoot))
+            $aBps['parent_id'] = $aRoot['cmt_id'];
+        $this->_prepareStructureBp($aDp['type'], $aBps);
+
+        $iLevel = 0;
+        $aStructure = array();
+        $this->_getStructure($aRoot, $aBps, $iLevel, $aStructure);
+
+        return !empty($aStructure) && is_array($aStructure) ? $aStructure : false;
+    }
+
+    function getCommentSearch($iCmtId, &$sAddon)
+    {
+        $aBp = array();
+        $aDp = array(
+            'type' => BX_CMT_DISPLAY_FLAT, 
+            'view_only' => true
+        );
+
+        if(empty($sAddon))
+            $sAddon = $this->getJsScript($aBp, $aDp);
+
+        BxDolTemplate::getInstance()->addInjection ('injection_footer', 'text', $this->_oTemplate->parseHtmlByName('comments_photoswipe.html', []));
+
+        return $this->_oTemplate->parseHtmlByName('comment_search.html', array(
+            'comment' => $this->getComment($iCmtId, $aBp, $aDp),
+        )); 
+    }
+
+    /**
+     * get one comment for "Live Search"
+     *
+     * @param  int    $iCmtId - comment id
+     * @return string
+     */
+    function getCommentLiveSearch($mixedCmt, $aParams = array())
+    {
+        $aCmt = !is_array($mixedCmt) ? $this->getCommentRow((int)$mixedCmt) : $mixedCmt;
+
+        list($sAuthorName, $sAuthorLink, $sAuthorIcon) = $this->_getAuthorInfo($aCmt['cmt_author_id']);
+        $bAuthorIcon = !empty($sAuthorIcon);
+
+        $sViewLink = $this->getViewUrl($aCmt['cmt_id']);
+        if(empty($sViewLink))
+            $sViewLink = '#';
+
+        return $this->_oTemplate->parseHtmlByName('comment_live_search.html', array(
+            'bx_if:show_icon' => array(
+                'condition' => $bAuthorIcon,
+                'content' => array(
+                    'author_icon' => $sAuthorIcon,
+        			'view_link' => $sViewLink
+                )
+            ),
+            'bx_if:show_icon_empty' => array(
+                'condition' => !$bAuthorIcon,
+                'content' => array()
+            ),
+            'view_link' => $sViewLink,
+            'text' => BxTemplFunctions::getInstance()->getStringWithLimitedLength(strip_tags($aCmt['cmt_text']), $this->_sSnippetLenthLiveSearch),
+            'sample' => isset($aParams['txt_sample_single']) ? _t($aParams['txt_sample_single']) : ''
+        ));
+    }
+
+    function getFormBoxPost($aBp = array(), $aDp = array())
+    {
+        return $this->_getFormBox(BX_CMT_ACTION_POST, $aBp, $aDp);
+    }
+
+    function getFormBoxEdit($aBp = array(), $aDp = array())
+    {
+        return $this->_getFormBox(BX_CMT_ACTION_EDIT, $aBp, $aDp);
+    }
+
+    function getFormPost($iCmtParentId = 0, $aDp = array())
+    {
+        return $this->_getFormPost($iCmtParentId, $aDp);
+    }
+
+    function getFormEdit($iCmtId, $aDp = array())
+    {
+        return $this->_getFormEdit($iCmtId, $aDp);
+    }
+
+    function getControlsBox()
+    {
+        return $this->_getControlsBox();
+    }
+    
+    function getLiveUpdate($iCountOld = 0, $iCountNew = 0)
+    {
+        $iCount = (int)$iCountNew - (int)$iCountOld;
+        if($iCount < 0)
+            return '';
+
+        $aComments = $this->_oQuery->getCommentsBy(array('type' => 'latest', 'object_id' => $this->_iId, 'author' => $this->_getAuthorId(), 'others' => 1, 'start' => '0', 'per_page' => $iCount));
+        if(empty($aComments) || !is_array($aComments))
+            return '';
+
+        $aComment = array_shift($aComments);
+        if(empty($aComment) || !is_array($aComment))
+            return '';
+
+        $sJsObject = $this->getJsObjectName();
+        return $this->_oTemplate->parseHtmlByName('comments_lu_button.html', array(
+            'style_prefix' => $this->_sStylePrefix,
+            'html_id' => $this->getNotificationId(),
+            'onclick_show' => "javascript:" . $sJsObject . ".goToBtn(this, '" . $this->getItemAnchor($aComment['cmt_id']) . "', '" . $aComment['cmt_id'] . "');",
+        ));
+    }
+
+    function getLiveUpdates($iCountOld = 0, $iCountNew = 0)
+    {
+        $bShowAll = true;
+        $bShowActions = false;
+
+        $iCount = (int)$iCountNew - (int)$iCountOld;
+        if($iCount < 0)
+            return '';
+
+        $aComments = $this->_oQuery->getCommentsBy(array('type' => 'latest', 'object_id' => $this->_iId, 'author' => $this->_getAuthorId(), 'others' => 1, 'start' => '0', 'per_page' => $iCount));
+        if(empty($aComments) || !is_array($aComments))
+            return '';
+
+        $sJsObject = $this->getJsObjectName();
+
+        $iUserId = $this->_getAuthorId();
+        $bModerator = $this->isModerator();
+
+        $aComments = array_reverse($aComments);
+        $iComments = count($aComments);
+
+        $aTmplVarsNotifs = array();
+        foreach($aComments as $iIndex => $aComment) {
+            $iCommentId = $aComment['cmt_id'];
+
+            $sShowOnClick = "javascript:" . $sJsObject . ".goTo(this, '" .  $this->getItemAnchor($iCommentId) . "', '" . $iCommentId . "');";
+            $sReplyOnClick = "javascript:" . $sJsObject . ".goToAndReply(this, '" . $this->getItemAnchor($iCommentId) . "', '" . $iCommentId . "');";
+
+            $iAuthorId = (int)$aComment['cmt_author_id'];
+            if($iAuthorId < 0) {
+                if(abs($iAuthorId) == $iUserId)
+                    continue;
+                else if($bModerator)
+                    $iAuthorId *= -1;
+            }
+
+            $oAuthor = $this->_getAuthorObject($iAuthorId);
+            $sAuthorName = $oAuthor->getDisplayName();
+
+            $aTmplVarsNotifs[] = array(
+                'bx_if:show_as_hidden' => array(
+                    'condition' => !$bShowAll && $iIndex < ($iComments - 1),
+                    'content' => array(),
+                ),
+                'item' => $this->_oTemplate->parseHtmlByName('comments_lu_notifications.html', array(
+                    'style_prefix' => $this->_sStylePrefix,
+                    'onclick_show' => $sShowOnClick,
+                    'onclick_reply' => $sReplyOnClick,
+                    'author_link' => $oAuthor->getUrl(), 
+                    'author_title' => bx_html_attribute($sAuthorName),
+                    'author_name' => $sAuthorName,
+                    'author_unit' => $oAuthor->getUnit(0, array('template' => 'unit_wo_info_links')), 
+                    'text' => _t('_cmt_txt_added_sample')
+                )),
+                'bx_if:show_previous' => array(
+                    'condition' => $bShowActions && $iIndex > 0,
+                    'content' => array(
+                        'onclick_previous' => $sJsObject . '.previousLiveUpdate(this)'
+                    )
+                ),
+                'bx_if:show_close' => array(
+                    'condition' => $bShowActions,
+                    'content' => array(
+                        'onclick_close' => $sJsObject . '.hideLiveUpdate(this)'
+                    )
+                )
+            );
+        }
+
+        return $this->_oTemplate->parseHtmlByName('popup_chain.html', array(
+            'html_id' => $this->getNotificationId(),
+            'bx_repeat:items' => $aTmplVarsNotifs
+        ));
+    }
+
+    public function getElementBlock($aParams = array())
+    {
+        $aParams['usage'] = BX_CMT_USAGE_BLOCK;
+
+        return $this->getElement($aParams);
+    }
+
+    public function getElementInline($aParams = array())
+    {
+        $aParams['usage'] = BX_CMT_USAGE_INLINE;
+
+        return $this->getElement($aParams);
+    }
+
+    public function getElement($aParams = array())
+    {
+    	$aParams = array_merge($this->_aElementDefaults, $aParams);
+
+        $bShowDoCommentAsButtonSmall = isset($aParams['show_do_comment_as_button_small']) && $aParams['show_do_comment_as_button_small'] == true;
+        $bShowDoCommentAsButton = !$bShowDoCommentAsButtonSmall && isset($aParams['show_do_comment_as_button']) && $aParams['show_do_comment_as_button'] == true;
+        $bShowCounterEmpty = isset($aParams['show_counter_empty']) && $aParams['show_counter_empty'] == true;
+        $bRecalculateCounter = isset($aParams['recalculate_counter']) && $aParams['recalculate_counter'] == true;
+
+        $iObjectId = $this->getId();
+        $iAuthorId = $this->_getAuthorId();
+        $iAuthorIp = $this->_getAuthorIp();
+
+        $iCount = $this->getCommentsCountAll(0, $bRecalculateCounter);
+        $bCount = (int)$iCount != 0;
+
+        $isAllowedComment = $this->isPostAllowed();
+
+        //--- Do Comment
+        $bTmplVarsDoComment = $this->_isShowDoComment($aParams, $isAllowedComment, $bCount);
+        $aTmplVarsDoComment = array();
+        if($bTmplVarsDoComment) {
+            $sClass = '';
+            if($bShowDoCommentAsButton)
+                $sClass = 'bx-btn';
+            else if ($bShowDoCommentAsButtonSmall)
+                $sClass = 'bx-btn bx-btn-small';
+
+            if(!$isAllowedComment)
+                $sClass .= $bShowDoCommentAsButton || $bShowDoCommentAsButtonSmall ? ' bx-btn-disabled' : 'bx-cmts-disabled';
+
+            $aTmplVarsDoComment = array(
+                'style_prefix' => $this->_sStylePrefix,
+                'do_comment' => $this->_oTemplate->parseLink($this->getListUrl(), $this->_getLabelDo($aParams), array(
+                    'class' => $this->_sStylePrefix . '-do-comment ' . $this->_sStylePrefix . '-dc ' . $sClass,
+                    'title' => _t($this->_getTitleDo())
+                )),
+            );
+        }
+
+        //--- Counter
+        $bTmplVarsCounter = $this->_isShowCounter($aParams, $isAllowedComment, $bCount);
+
+        $aTmplVarsCounter = array();
+        if($bTmplVarsCounter)
+            $aTmplVarsCounter = array(
+                'style_prefix' => $this->_sStylePrefix,
+                'bx_if:show_hidden' => array(
+                    'condition' => !$bShowCounterEmpty && !$bCount,
+                    'content' => array()
+                ),
+                'counter' => $this->getCounter(array_merge($aParams, [
+                    'show_counter_only' => false
+                ]))
+            );
+
+        if(!$bTmplVarsDoComment && !$bTmplVarsCounter)
+            return '';
+
+        $sTmplName = $this->{'_getTmplElement' . bx_gen_method_name(!empty($aParams['usage']) ? $aParams['usage'] : BX_CMT_USAGE_DEFAULT)}();
+        return $this->_oTemplate->parseHtmlByContent($sTmplName, array(
+            'style_prefix' => $this->_sStylePrefix,
+            'html_id' => $this->_aHtmlIds['main'],
+            'class' => $this->_sStylePrefix . ($bShowDoCommentAsButton ? '-button' : '') . ($bShowDoCommentAsButtonSmall ? '-button-small' : ''),
+            'count' => $iCount,
+            'bx_if:show_do_comment' => array(
+                'condition' => $bTmplVarsDoComment,
+                'content' => $aTmplVarsDoComment
+            ),
+            'bx_if:show_counter' => array(
+                'condition' => $bTmplVarsCounter,
+                'content' => $aTmplVarsCounter
+            ),
+            'script' => ''
+        ));
+    }
+
+    protected function _getCounterItems($iCmtsLimit, $iCmtsStart = 0)
+    {
+        return $this->_oQuery->getCommentsBy(array('type' => 'object_id', 'object_id' => $this->getId(), 'order_way' => 'desc', 'start' => $iCmtsStart, 'per_page' => $iCmtsLimit * 4));
+    }
+    
+    private function _isShowContent($aCmt)
+    {
+        $oProfileAuthor = BxDolProfile::getInstance($aCmt['cmt_author_id']);
+        return (int)$aCmt['cmt_author_id'] == 0 || ($oProfileAuthor && $oProfileAuthor->isActive()) || isAdmin() || BxDolAcl::getInstance()->isMemberLevelInSet([MEMBERSHIP_ID_MODERATOR, MEMBERSHIP_ID_ADMINISTRATOR]);
+    }
+    
+    public function getCounter($aParams = [])
+    {
+        $aParams = array_merge($this->_aElementDefaults, $aParams);
+
+        $bShowDoCommentAsButtonSmall = isset($aParams['show_do_comment_as_button_small']) && $aParams['show_do_comment_as_button_small'] == true;
+        $bShowDoCommentAsButton = !$bShowDoCommentAsButtonSmall && isset($aParams['show_do_comment_as_button']) && $aParams['show_do_comment_as_button'] == true;
+        $bShowEmpty = isset($aParams['show_counter_empty']) && $aParams['show_counter_empty'] == true;
+        $bShowReversed = isset($aParams['show_counter_reversed']) && $aParams['show_counter_reversed'] == true;
+        $bRecalculateCounter = isset($aParams['recalculate_counter']) && $aParams['recalculate_counter'] == true;
+
+        $sClass = 'sys-action-counter';
+        if(isset($aParams['show_counter_only']) && (bool)$aParams['show_counter_only'] === true)
+            $sClass .= ' sys-ac-only';
+
+        $iCount = (int)$this->getCommentsCountAll(0, $bRecalculateCounter);        
+        if($iCount == 0 && !$bShowEmpty)
+            $sClass .= ' sys-ac-hidden';
+
+        $sClass .= ' ' . $this->_sStylePrefix . '-counter';
+        if($bShowDoCommentAsButtonSmall)
+            $sClass .= ' bx-btn-small-height';
+        if($bShowDoCommentAsButton)
+            $sClass .= ' bx-btn-height';
+
+        $iCmtsLimit = 5;
+        $aCmts = $this->_getCounterItems($iCmtsLimit);
+        if(!$bShowReversed)
+            $aCmts = array_reverse($aCmts);
+
+        $aTmplVarsProfiles = [];
+        foreach($aCmts as $aCmt) {
+            $iAuthor = (int)$aCmt['cmt_author_id'];
+            if(array_key_exists($iAuthor, $aTmplVarsProfiles))
+                continue;
+
+            $oAuthor = BxDolProfile::getInstanceMagic($iAuthor);
+            if(!$oAuthor)
+                continue;
+
+            $aTmplVarsProfiles[$iAuthor] = [
+                'icon' => $oAuthor->getUnit(0, ['template' => ['name' => 'unit_wo_info_links', 'size' => 'icon']]) 
+            ];
+
+            if(count($aTmplVarsProfiles) >= $iCmtsLimit)
+                break;
+        }
+        $aTmplVarsProfiles = array_values($aTmplVarsProfiles);
+
+        $sHref = !empty($aParams['overwrite_counter_link_href']) ? $aParams['overwrite_counter_link_href'] : $this->getListUrl();
+        $sOnclick = '';
+        if(!empty($aParams['overwrite_counter_link_onclick']))
+            $sOnclick = $aParams['overwrite_counter_link_onclick'];
+
+        return $this->_oTemplate->parseHtmlByContent($this->_getTmplCounter(), [
+            'style_prefix' => $this->_sStylePrefix,
+            'id' => $this->_aHtmlIds['counter'],
+            'class' => $sClass,
+            'href' => $sHref,
+            'bx_if:show_onclick' => [
+                'condition' => !empty($sOnclick),
+                'content' => [
+                    'onclick' => $sOnclick
+                ]
+            ],
+            'content' => $this->_getCounterLabel($iCount, $aParams),
+            'bx_repeat:profiles' => $aTmplVarsProfiles,
+            'bx_if:show_icon' => [
+                'condition' => ($bShowEmpty || !empty($aTmplVarsProfiles)) && (!isset($aParams['show_icon']) || $aParams['show_icon']),
+                'content' => [
+                    'style_prefix' => $this->_sStylePrefix,
+                    'icon' => !empty($aParams['custom_icon']) ? $aParams['custom_icon'] : $this->_oTemplate->getImageAuto('comment|' . $this->_sStylePrefix . '-counter-icon')
+                ]
+            ]
+        ]);
+    }
+
+    protected function _getLabelDo($aParams = [])
+    {
+        return $this->_oTemplate->parseHtmlByContent($this->_getTmplLabelDo(), [
+            'style_prefix' => $this->_sStylePrefix,
+            'bx_if:show_icon' => [
+                'condition' => isset($aParams['show_do_comment_icon']) && $aParams['show_do_comment_icon'] == true,
+                'content' => [
+                    'style_prefix' => $this->_sStylePrefix,
+                    'icon' => $this->_oTemplate->getImageAuto($this->_getIconDo())
+                ]
+            ],
+            'bx_if:show_text' => [
+                'condition' => isset($aParams['show_do_comment_label']) && $aParams['show_do_comment_label'] == true,
+                'content' => [
+                    'style_prefix' => $this->_sStylePrefix,
+                    'text' => _t($this->_getTitleDo())
+                ]
+            ]
+        ]);
+    }
+
+    protected function _getCounterLabel($iCount, $aParams = [])
+    {
+        $aParams = array_merge($this->_aElementDefaults, $aParams);
+
+        $sResult = '';
+        if((int)$iCount != 0)
+            $sResult = _t(isset($aParams['caption']) ? $aParams['caption'] : '_cmt_txt_counter', $iCount);
+        else
+            $sResult = _t(isset($aParams['caption_empty']) ? $aParams['caption_empty'] : '_cmt_txt_counter_empty');
+        
+        return $sResult;
+    }
+
+    protected function _getTmplElementBlock()
+    {
+        return self::$_sTmplContentElementBlock;
+    }
+
+    protected function _getTmplElementInline()
+    {
+        return self::$_sTmplContentElementInline;
+    }
+
+    protected function _getTmplLabelDo()
+    {
+        return self::$_sTmplContentDoCommentLabel;
+    }
+
+    protected function _getTmplCounter()
+    {
+        return self::$_sTmplContentCounter;
+    }
+
+
+    /**
+     * private functions
+     */
+    protected function _getContentBefore($aBp = array(), $aDp = array())
+    {
+        return '';
+    }
+
+    protected function _getContentAfter($aBp = array(), $aDp = array())
+    {
+        return '';
+    }
+
+    protected function _getControlsBox()
+    {
+        $sDisplay = '';
+        $bDisplay = (int)$this->_aSystem['is_display_switch'] == 1;
+        if($bDisplay) {
+            $aDisplayLinks = array(
+                array('id' => $this->_sSystem . '-flat', 'name' => $this->_sSystem . '-flat', 'class' => '', 'title' => '_cmt_display_flat', 'target' => '_self', 'onclick' => 'javascript:' . $this->_sJsObjName . '.cmtChangeDisplay(this, \'flat\');'),
+                array('id' => $this->_sSystem . '-threaded', 'name' => $this->_sSystem . '-threaded', 'class' => '', 'title' => '_cmt_display_threaded', 'target' => '_self', 'onclick' => 'javascript:' . $this->_sJsObjName . '.cmtChangeDisplay(this, \'threaded\');')
+            );
+
+            $oMenu = new BxTemplMenuInteractive(array('template' => 'menu_interactive_vertical.html', 'menu_id'=> $this->_sSystem . '-display', 'menu_items' => $aDisplayLinks));
+            $oMenu->setSelected('', $this->_sSystem . '-' . $this->_sDisplayType);
+            $sDisplay = $oMenu->getCode();
+        }
+
+        $sBrowseType = '';
+        $bBrowseType = (int)$this->_aSystem['is_browse_switch'] == 1;
+        if($bBrowseType) {
+            $aBrowseLinks = array(
+                array('id' => $this->_sSystem . '-tail', 'name' => $this->_sSystem . '-tail', 'class' => '', 'title' => '_cmt_browse_tail', 'target' => '_self', 'onclick' => 'javascript:' . $this->_sJsObjName . '.cmtChangeBrowse(this, \'tail\');'),
+                array('id' => $this->_sSystem . '-head', 'name' => $this->_sSystem . '-head', 'class' => '', 'title' => '_cmt_browse_head', 'target' => '_self', 'onclick' => 'javascript:' . $this->_sJsObjName . '.cmtChangeBrowse(this, \'head\');'),
+                array('id' => $this->_sSystem . '-popular', 'name' => $this->_sSystem . '-popular', 'class' => '', 'title' => '_cmt_browse_popular', 'target' => '_self', 'onclick' => 'javascript:' . $this->_sJsObjName . '.cmtChangeBrowse(this, \'popular\');'),
+            );
+
+            $oMenu = new BxTemplMenuInteractive(array('template' => 'menu_interactive_vertical.html', 'menu_id'=> $this->_sSystem . '-browse', 'menu_items' => $aBrowseLinks));
+            $oMenu->setSelected('', $this->_sSystem . '-' . $this->_sBrowseType);
+            $sBrowseType = $oMenu->getCode();
+        }
+
+        $sBrowseFilter = '';
+        $bBrowseFilter = (int)$this->_aSystem['is_browse_filter'] == 1;
+        if($bBrowseFilter) {
+            $aFilterLinks = array(
+                array('id' => $this->_sSystem . '-all', 'name' => $this->_sSystem . '-all', 'class' => '', 'title' => '_cmt_browse_all', 'target' => '_self', 'onclick' => 'javascript:' . $this->_sJsObjName . '.cmtChangeFilter(this, \'all\');'),
+                array('id' => $this->_sSystem . '-friends', 'name' => $this->_sSystem . '-friends', 'class' => '', 'title' => '_cmt_browse_friends', 'target' => '_self', 'onclick' => 'javascript:' . $this->_sJsObjName . '.cmtChangeFilter(this, \'friends\');'),
+                array('id' => $this->_sSystem . '-subscriptions', 'name' => $this->_sSystem . '-subscriptions', 'class' => '', 'title' => '_cmt_browse_subscriptions', 'target' => '_self', 'onclick' => 'javascript:' . $this->_sJsObjName . '.cmtChangeFilter(this, \'subscriptions\');')
+            );
+
+            $oMenu = new BxTemplMenuInteractive(array('template' => 'menu_interactive_vertical.html', 'menu_id'=> $this->_sSystem . '-filter', 'menu_items' => $aFilterLinks));
+            $oMenu->setSelected('', $this->_sSystem . '-' . $this->_sBrowseFilter);
+            $sBrowseFilter = $oMenu->getCode();
+        }
+
+        return $this->_oTemplate->parseHtmlByName('comments_controls.html', array(
+            'display_switcher' => $bDisplay ? $sDisplay : '',
+            'bx_if:is_divider_1' => array(
+                'condition' => $bDisplay && $bBrowseType,
+                'content' => array(
+                    'style_prefix' => $this->_sStylePrefix,
+                )
+            ),
+            'browse_switcher' => $bBrowseType ? $sBrowseType : '',
+            'bx_if:is_divider_2' => array(
+                'condition' => $bBrowseType && $bBrowseFilter,
+                'content' => array(
+                    'style_prefix' => $this->_sStylePrefix,
+                )
+            ),
+            'filter_switcher' => $bBrowseFilter ? $sBrowseFilter : '',
+        ));
+    }
+
+    protected function _getActionsBox(&$aCmt, $aBp = [], $aDp = [])
+    {
+    	$bViewOnly = isset($aDp['view_only']) && $aDp['view_only'] === true;
+    	$bDynamicMode = isset($aDp['dynamic_mode']) && $aDp['dynamic_mode'] === true;
+
+        $sMenuActions = '';
+        if(!$bViewOnly) {
+            $oMenuActions = BxDolMenu::getObjectInstance($this->_sMenuObjActions);
+            $oMenuActions->setCmtsData($this, $aCmt['cmt_id'], $aBp, $aDp);
+            $oMenuActions->setDynamicMode($bDynamicMode);
+            $sMenuActions = $oMenuActions->getCode();
+        }
+
+        return $this->_oTemplate->parseHtmlByName('comment_actions.html', array(
+            'id' => $aCmt['cmt_id'],
+            'js_object' => $this->_sJsObjName,
+            'style_prefix' => $this->_sStylePrefix,
+            'menu_actions' => $sMenuActions,
+        ));
+    }
+
+    protected function _getFormBox($sType, $aBp, $aDp)
+    {
+        $iCmtParentId = isset($aBp['parent_id']) ? (int)$aBp['parent_id'] : 0;
+        $sPosition = isset($aDp['position']) ? $aDp['position'] : '';
+        $bQuote = isset($aDp['quote']) && (bool)$aDp['quote'];
+        $bFormMin = $iCmtParentId == 0 && isset($aDp['min_post_form']) && (bool)$aDp['min_post_form'];
+
+        $sPositionSystem = $this->_aSystem['post_form_position'];
+        if(!empty($sPosition) && $sPositionSystem != $sPosition)
+            return '';
+
+        $sClass = '';
+        if(!empty($sPosition))
+            $sClass .= ' ' . $this->_sStylePrefix . '-reply-' . $sPosition;
+        if($bQuote)
+            $sClass .= ' ' . $this->_sStylePrefix . '-reply-quote';
+        if($bFormMin)
+            $sClass .= ' ' . $this->_sStylePrefix . '-reply-min';
+        if(!empty($aDp['class']))
+            $sClass .= ' ' . $aDp['class'];
+
+        $sClassBody = '';
+        if(!empty($aDp['class_body']))
+            $sClassBody .= ' ' . $aDp['class_body'];
+
+        $aTmplVarsFormMin = array();
+        if($bFormMin) {
+            list($sAuthorName, $sAuthorLink, $sAuthorIcon, $sAuthorUnit) = $this->_getAuthorInfo($this->_getAuthorId());
+
+            $oForm = new BxTemplFormView(array());
+            $aInputPlaceholder = array(
+                'type' => 'text',
+                'name' => 'comment',
+                'caption' => '',
+                'attrs' => array(
+                    'onclick' => 'javascript:' . $this->_sJsObjName . '.cmtShowForm(this)',
+                    'placeholder' => _t($this->_aT['txt_min_form_placeholder'], $sAuthorName),
+                    'autocomplete' => 'off',
+                    'readonly' => 'readonly'
+                ),
+                'value' => '',
+            );
+
+            $sClassBodyMin = '';
+            if(!empty($aDp['class_body_min']))
+                $sClassBodyMin .= ' ' . $aDp['class_body_min'];
+
+            $aTmplVarsFormMin = array(
+                'js_object' => $this->_sJsObjName,
+                'class_body_min' => $sClassBodyMin,
+                'style_prefix' => $this->_sStylePrefix,
+                'author_unit' => $sAuthorUnit,
+                'placeholder' => $oForm->genRow($aInputPlaceholder)
+            );
+        }
+
+        $aForm = $this->{'_getForm' . ucfirst($sType)}($iCmtParentId, $aDp);
+        if(empty($aForm['form']))
+            return !empty($aForm['msg']) ? MsgBox($aForm['msg']) : '';
+
+        return $this->_oTemplate->parseHtmlByName('comment_reply_box.html', array(
+            'js_object' => $this->_sJsObjName,
+            'style_prefix' => $this->_sStylePrefix,
+            'class' => $sClass,
+            'class_body' => $sClassBody,
+            'bx_if:show_form_min' => array(
+                'condition' => $bFormMin,
+                'content' => $aTmplVarsFormMin
+            ),
+            'form' => $aForm['form'],
+            'form_id' => $aForm['form_id'],
+        ));
+    }
+
+    protected function _getFormAdd($aValues)
+    {
+        $iCmtAuthorId = isset($aValues['cmt_author_id']) ? (int)$aValues['cmt_author_id'] : $this->_getAuthorId();
+        $iCmtParentId = isset($aValues['cmt_parent_id']) ? (int)$aValues['cmt_parent_id'] : 0;
+
+        $oForm = $this->_getForm(BX_CMT_ACTION_POST, $iCmtParentId);
+        $oForm->aFormAttrs['method'] = BX_DOL_FORM_METHOD_SPECIFIC;
+        $oForm->aParams['csrf']['disable'] = true;
+        $oForm->aInputs['cmt_parent_id']['value'] = $iCmtParentId;
+        if(!empty($oForm->aParams['db']['submit_name'])) {            
+            $sSubmitName = false;
+            if(is_array($oForm->aParams['db']['submit_name']))
+                foreach($oForm->aParams['db']['submit_name'] as $sVal) {
+                    if(isset($oForm->aInputs[$sVal])) {
+                        $sSubmitName = $sVal;
+                        break;
+                    }
+                }
+            else
+                $sSubmitName = $oForm->aParams['db']['submit_name'];
+
+            if($sSubmitName && isset($oForm->aInputs[$sSubmitName]))
+                $aValues[$sSubmitName] = $oForm->aInputs[$sSubmitName]['value'];
+        }
+
+        $oForm->initChecker(array(), $aValues);
+        if(!$oForm->isSubmittedAndValid()) 
+            return array('code' => 1, 'message' => '_sys_txt_error_occured');
+
+        $iLevel = 0;
+        $iCmtVisualParentId = 0;
+        if((int)$iCmtParentId > 0) {
+            $aParent = $this->getCommentRow($iCmtParentId);
+
+            $iLevel = (int)$aParent['cmt_level'] + 1;
+            $iCmtVisualParentId = $iLevel > $this->getMaxLevel() ? $aParent['cmt_vparent_id'] : $iCmtParentId;
+        }
+
+        $iCmtId = (int)$oForm->insert(array('cmt_vparent_id' => $iCmtVisualParentId, 'cmt_object_id' => $this->_iId, 'cmt_author_id' => $iCmtAuthorId, 'cmt_level' => $iLevel, 'cmt_time' => time()));
+        if(!$iCmtId) {
+            if(!$oForm->isValid())
+                return array('code' => 1, 'message' => '_sys_txt_error_occured');
+            else
+                return array('code' => 2, 'message' => '_cmt_err_cannot_perform_action');
+        }
+
+        $iCmtUniqId = $this->_oQuery->getUniqId($this->_aSystem['system_id'], $iCmtId, [
+            'author_id' => $iCmtAuthorId, 
+            'status_admin' => $this->getStatusAdmin()
+        ]);
+
+        if($iCmtParentId) {
+            $this->_oQuery->updateRepliesCount($iCmtParentId, 1);
+
+            if(!BxDolModuleQuery::getInstance()->isEnabledByName('bx_notifications'))
+                $this->_sendNotificationEmail($iCmtId, $iCmtParentId);
+        }
+
+        $this->_triggerComment();
+
+        if($this->_sMetatagsObj && ($oMetatags = BxDolMetatags::getObjectInstance($this->_sMetatagsObj)) !== false)
+            $oMetatags->metaAdd($iCmtUniqId, $aValues['cmt_text']);
+
+        $mixedResult = $this->onPostAfter($iCmtId);
+        if($mixedResult === false)
+            return array('code' => 2, 'message' => '_cmt_err_cannot_perform_action');
+
+        return $mixedResult;
+    }
+
+    protected function _getFormPost($iCmtParentId = 0, $aDp = [])
+    {
+        $bCmtParentId = !empty($iCmtParentId);
+
+        if(!$bCmtParentId && !$this->isPostAllowed()) {
+            $sMsg = '';
+            if(!isLogged()) {
+                $oPermalink = BxDolPermalinks::getInstance();
+                $sMsg = _t('_cmt_msg_login_required', $oPermalink->permalink('page.php?i=login'), $oPermalink->permalink('page.php?i=create-account'));
+            }
+            else
+                $sMsg = $this->msgErrPostAllowed();
+
+            return bx_is_api() ? bx_api_get_msg($sMsg) : ['msg' => $sMsg];
+        }
+
+        if($bCmtParentId && !$this->isReplyAllowed($iCmtParentId))
+            return bx_is_api() ? bx_api_get_msg($sMsg) : ['msg' => $this->msgErrReplyAllowed()];
+
+        $bDynamic = isset($aDp['dynamic_mode']) && (bool)$aDp['dynamic_mode'];
+        $bQuote = isset($aDp['quote']) && (bool)$aDp['quote'];
+
+        $oForm = $this->_getForm(BX_CMT_ACTION_POST, $iCmtParentId);
+        $oForm->aInputs['cmt_parent_id']['value'] = $iCmtParentId;
+
+        if($bQuote) {
+            $aCmtParent = $this->getCommentRow((int)$iCmtParentId);
+            if(!empty($aCmtParent['cmt_text']))
+                $oForm->aInputs['cmt_text']['value'] = $this->_oTemplate->parseHtmlByName('comment_quote.html', [
+                    'content' => $aCmtParent['cmt_text']
+                ]);
+        }
+
+        $oForm->initChecker();
+        if($oForm->isSubmittedAndValid()) {
+            $iCmtAuthorId = $this->_getAuthorId();
+            $iCmtParentId = (int)$oForm->getCleanValue('cmt_parent_id');
+            
+            //--- Process Text ---//
+            $sCmtText = $oForm->getCleanValue('cmt_text');
+            $bCmtText = !empty($sCmtText);
+
+            //--- Process Media ---//
+            $aImageIds = $oForm->getCleanValue('cmt_image');
+            $bImageIds = !empty($aImageIds) && is_array($aImageIds);
+
+            if(!$bCmtText && !$bImageIds) {
+                $oForm->aInputs['cmt_text']['error'] =  _t('_Please enter characters');
+                $oForm->setValid(false);
+                return bx_is_api() ? ['form' => $oForm, 'res' => 0] : ['form' => $oForm->getCode($bDynamic), 'form_id' => $oForm->id];
+            }
+
+            $aParent = [];
+            if($iCmtParentId > 0) {
+                $aParent = $this->getCommentRow($iCmtParentId);
+                if(empty($aParent) || !is_array($aParent)) {
+                    $iCmtParentId = 0;
+                    $oForm->setSubmittedValue('cmt_parent_id', $iCmtParentId, $oForm->aFormAttrs['method']);
+                }
+            }
+
+            $iLevel = 0;
+            $iCmtVisualParentId = 0;
+            if($iCmtParentId > 0) {
+                $iLevel = (int)$aParent['cmt_level'] + 1;
+                $iCmtVisualParentId = $iLevel > $this->getMaxLevel() ? $aParent['cmt_vparent_id'] : $iCmtParentId;
+            }
+
+            $iCmtId = (int)$oForm->insert(['cmt_vparent_id' => $iCmtVisualParentId, 'cmt_object_id' => $this->_iId, 'cmt_author_id' => $iCmtAuthorId, 'cmt_level' => $iLevel, 'cmt_time' => time()]);
+            if($iCmtId != 0) {
+                $iCmtUniqId = $this->_oQuery->getUniqId($this->_aSystem['system_id'], $iCmtId, [
+                    'author_id' => $iCmtAuthorId,
+                    'status_admin' => $this->getStatusAdmin()
+                ]);
+
+                if($this->isAttachImageEnabled())
+                    $oForm->processImages($this, 'cmt_image', $iCmtUniqId, $iCmtId, $iCmtAuthorId, true);
+
+                if($iCmtParentId > 0) {
+                    $this->_oQuery->updateRepliesCount($iCmtParentId, 1);
+
+                    if(!BxDolModuleQuery::getInstance()->isEnabledByName('bx_notifications'))
+                        $this->_sendNotificationEmail($iCmtId, $iCmtParentId);
+                }
+
+                $this->_triggerComment();
+
+                if($iCmtParentId > 0)
+                    $this->isReplyAllowed($iCmtParentId, true);
+                else
+                    $this->isPostAllowed(true);
+
+                if($this->_sMetatagsObj && ($oMetatags = BxDolMetatags::getObjectInstance($this->_sMetatagsObj)) !== false)
+                    $oMetatags->metaAdd($iCmtUniqId, $sCmtText);
+
+                if(($mixedResult = $this->onPostAfter($iCmtId, $aDp)) !== false){
+                    if (bx_is_api()){
+                        $this->_unsetFormObject(BX_CMT_ACTION_POST);
+                        return ['form' => $this->_getForm(BX_CMT_ACTION_POST, $iCmtParentId), 'res' => $iCmtId];
+                    }
+                    else{
+                        return $mixedResult;
+                    }
+                }
+            }
+            return bx_is_api() ? bx_api_get_msg('_cmt_err_cannot_perform_action') : ['msg' => _t('_cmt_err_cannot_perform_action')];
+        }
+        return bx_is_api() ? ['form' => $oForm, 'res' => 0] : ['form' => $oForm->getCode($bDynamic), 'form_id' => $oForm->id];
+    }
+
+    protected function _getFormEdit($iCmtId, $aDp = [])
+    {
+        $bDynamic = isset($aDp['dynamic_mode']) && (bool)$aDp['dynamic_mode'];
+
+        $aCmt = $this->getCommentSimple($iCmtId);
+        if(!$aCmt)
+            return array('msg' => _t('_No such comment'));
+
+        $iCmtAuthorId = $this->_getAuthorId();
+        if(!$this->isEditAllowed($aCmt))
+            return array('msg' => $aCmt['cmt_author_id'] == $iCmtAuthorId ? strip_tags($this->msgErrEditAllowed()) : _t('_Access denied'));
+
+        $oForm = $this->_getForm(BX_CMT_ACTION_EDIT, $aCmt['cmt_id']);
+
+        $oForm->initChecker($aCmt);
+        if($oForm->isSubmittedAndValid()) {
+            $sCmtText = $oForm->getCleanValue('cmt_text');
+
+            if($oForm->update($iCmtId) !== false) {
+                $iCmtUniqId = $this->_oQuery->getUniqId($this->_aSystem['system_id'], $iCmtId, [
+                    'author_id' => (int)$aCmt['cmt_author_id']
+                ]);
+                
+                $sStatusAdmin = $this->getStatusAdmin();
+                if($sStatusAdmin !== BX_CMT_STATUS_ACTIVE)
+                    $this->_oQuery->updateUniqId(['status_admin' => $sStatusAdmin], ['id' => $iCmtUniqId]);
+
+                if($this->isAttachImageEnabled())
+                    $oForm->processImages($this, 'cmt_image', $iCmtUniqId, $iCmtId, $iCmtAuthorId, false);
+
+                $this->isEditAllowed($aCmt, true);
+
+                if($this->_sMetatagsObj && ($oMetatags = BxDolMetatags::getObjectInstance($this->_sMetatagsObj)) !== false)
+                    $oMetatags->metaAdd($iCmtUniqId, $sCmtText);
+
+                if(($mixedResult = $this->onEditAfter($iCmtId, $aDp)) !== false)
+                    return $mixedResult;
+            }
+
+            return array('msg' => _t('_cmt_err_cannot_perform_action'));
+        }
+
+        return array('form' => $oForm->getCode($bDynamic), 'form_id' => $oForm->id);
+    }
+
+    protected function _getForm($sAction, $iId)
+    {
+        $oForm = $this->_getFormObject($sAction);
+        $oForm->setId(sprintf($oForm->getAttributeMask('id'), $sAction, $this->_sSystem, $this->_iId, $iId));
+        $oForm->setName(sprintf($oForm->getAttributeMask('name'), $sAction, $this->_sSystem, $this->_iId, $iId));
+        $oForm->aParams['db']['table'] = $this->_aSystem['table'];
+        $oForm->aInputs['sys']['value'] = $this->_sSystem;
+        $oForm->aInputs['id']['value'] = $this->_iId;
+        $oForm->aInputs['action']['value'] = 'Submit' . ucfirst($sAction) . 'Form';
+
+        if(!$this->isAttachImageEnabled())
+            unset($oForm->aInputs['cmt_image']);
+
+        if(isset($oForm->aInputs['cmt_text'])) {
+            $oForm->aInputs['cmt_text']['html'] = $this->_aSystem['html'];
+            $oForm->aInputs['cmt_text']['db']['pass'] = $this->isHtml() ? 'XssHtml' : 'XssMultiline';
+
+            if(isset($oForm->aInputs['cmt_text']['checker']['func']) && strtolower($oForm->aInputs['cmt_text']['checker']['func']) == 'length') {
+                $iCmtTextMin = (int)$this->_aSystem['chars_post_min'];
+                $iCmtTextMax = (int)$this->_aSystem['chars_post_max'];
+                $oForm->aInputs['cmt_text']['checker']['params'] = array($iCmtTextMin, $iCmtTextMax);
+                $oForm->aInputs['cmt_text']['checker']['error'] = _t('_Please enter n1-n2 characters', $iCmtTextMin, $iCmtTextMax);
+            }
+        }
+
+        if($sAction == BX_CMT_ACTION_EDIT && isset($oForm->aInputs['cmt_controls']))
+            foreach($oForm->aInputs['cmt_controls'] as $mixedKey => $mixedValue) {
+                if(!is_numeric($mixedKey) || empty($mixedValue['name']) || $mixedValue['name'] != 'cmt_cancel')
+                    continue;
+
+                if(!isset($oForm->aInputs['cmt_controls'][$mixedKey]['attrs']))
+                    $oForm->aInputs['cmt_controls'][$mixedKey]['attrs'] = array();
+
+                $oForm->aInputs['cmt_controls'][$mixedKey]['attrs']['onclick'] = $this->_sJsObjName . '.cmtEdit(this, ' . $iId . ', false)';
+            }
+
+        return $oForm;
+    }
+
+    protected function _getContent($aCmt, $aBp = [], $aDp = [])
+    {
+        $bDynamicMode = isset($aDp['dynamic_mode']) && $aDp['dynamic_mode'] === true;
+
+        $sAttachments = $this->_getAttachments($aCmt);
+
+        $oMenuCounters = BxDolMenu::getObjectInstance($this->_sMenuObjCounters);
+        $oMenuCounters->setCmtsData($this, $aCmt['cmt_id'], $aBp, $aDp);
+        $oMenuCounters->setDynamicMode($bDynamicMode);
+
+        return $this->_oTemplate->parseHtmlByName($this->_sTmplNameItemContent, array_merge(array(
+            'style_prefix' => $this->_sStylePrefix,
+            'js_object' => $this->_sJsObjName,
+            'bx_if:show_attached' => array(
+                'condition' => !empty($sAttachments),
+                'content' => array(
+                    'style_prefix' => $this->_sStylePrefix,
+                    'attached' => $sAttachments
+                )
+            ),
+            'counters' => $oMenuCounters->getCode(),
+        ), $this->_getTmplVarsText($aCmt)));
+    }
+
+    protected function _getMoreLink($sCmts, $aBp = array(), $aDp = array())
+    {
+        $iStart = $iPerView = 0;
+        switch($aBp['type']) {
+            case BX_CMT_BROWSE_HEAD:
+            case BX_CMT_BROWSE_POPULAR:
+            case BX_CMT_BROWSE_CONNECTION:
+                $iPerView = $aBp['per_view'];
+
+                $iStart = $aBp['start'] + $iPerView;
+                if($iStart >= $aBp['count'])
+                    return $sCmts;
+
+                break;
+
+            case BX_CMT_BROWSE_TAIL:
+                $iPerView = $aBp['per_view'];
+
+                $iStart = $aBp['start'] - $iPerView;
+                if($iStart < 0) {
+                    $iPerView += $iStart;
+                    $iStart = 0;
+                }
+
+                if($iStart == 0 && $iPerView == 0)
+                    return $sCmts;
+
+                break;
+        }
+
+        $sParentKey = isset($aBp['vparent_id']) ? 'vparent_id' : 'parent_id';
+        $bRoot = (int)$aBp[$sParentKey] <= 0;
+
+        $sMore = $this->_oTemplate->parseHtmlByName('comment_more.html', array(
+            'js_object' => $this->_sJsObjName,
+            'style_prefix' => $this->_sStylePrefix,
+            'bx_if:is_root' => array(
+                'condition' => $bRoot,
+                'content' => array()
+            ),
+            'parent_id' => $aBp[$sParentKey],
+            'start' => $iStart,
+            'per_view' => $iPerView,
+            'title' => _t('_cmt_load_more_' . ($aBp[$sParentKey] == 0 ? 'comments' : 'replies') . '_' . $aBp['type'])
+        ));
+
+        switch($aBp['type']) {
+            case BX_CMT_BROWSE_HEAD:
+                case BX_CMT_BROWSE_POPULAR:
+                case BX_CMT_BROWSE_CONNECTION:
+                $sCmts .= $sMore;
+                break;
+
+            case BX_CMT_BROWSE_TAIL:
+                $sCmts = $sMore . $sCmts;
+                break;
+        }
+
+        return $sCmts;
+    }
+
+    protected function _getEmpty($aDp = array())
+    {
+        $sClass = '';
+        if(!empty($aDp['class']))
+            $sClass .= ' ' . $aDp['class'];
+
+        return $this->_oTemplate->parseHtmlByName('comment_empty.html', array(
+            'style_prefix' => $this->_sStylePrefix,
+            'class' => $sClass,
+            'content' => MsgBox(_t('_Empty'))
+        ));
+    }
+
+    protected function _getAttachments($aCmt)
+    {
+        $aTmplImages = array();
+        if(!$this->isAttachImageEnabled())
+            return ''; 
+
+        $aFiles = $this->_oQuery->getFiles($this->_aSystem['system_id'], $aCmt['cmt_id']);
+        if(!empty($aFiles) && is_array($aFiles)) {
+            $oStorage = BxDolStorage::getObjectInstance($this->getStorageObjectName());
+            $oTranscoder = BxDolTranscoderImage::getObjectInstance($this->getTranscoderPreviewName());
+
+            foreach($aFiles as $aFile) {
+                $sFile = $oStorage->getFileUrlById($aFile['image_id']);
+                $bImage = $oTranscoder && $oTranscoder->isMimeTypeSupported($aFile['mime_type']);
+
+                $sPreview = '';
+                $iWidth = $iHeight = 0;
+
+                if($bImage) {
+                    if($oTranscoder)
+                        $sPreview = $oTranscoder->getFileUrl($aFile['image_id']);
+
+                    $aImageInfo = BxDolImageResize::getInstance()->getImageSize($sFile);
+                    if(isset($aImageInfo['w']))
+                        $iWidth = (int)$aImageInfo['w'];
+                    if(isset($aImageInfo['h']))
+                        $iHeight = (int)$aImageInfo['h'];
+                }
+
+                if(!$sPreview)
+                    $sPreview = $this->_oTemplate->getIconUrl($oStorage->getIconNameByFileName($aFile['file_name']));
+
+                $aTmplVarsFile = array(
+                    'js_object' => $this->_sJsObjName,
+                    'preview' => $sPreview,
+                    'file' => $sFile,
+                    'file_id' => $aFile['id'],
+                    'file_name' => $aFile['file_name'],
+                    'file_icon' => $oStorage->getFontIconNameByFileName($aFile['file_name']),
+                    'file_size' => _t_format_size($aFile['size']),
+                    'w' => $iWidth, 
+                    'h' => $iHeight
+                );
+
+                $aTmplImages[] = array(
+                    'style_prefix' => $this->_sStylePrefix,
+                    'bx_if:show_image' => array(
+                        'condition' => $bImage,
+                        'content' => $aTmplVarsFile
+                    ),
+                    'bx_if:show_file' => array(
+                        'condition' => !$bImage,
+                        'content' => $aTmplVarsFile
+                    ),
+                );
+            }
+        }
+
+        return $this->_oTemplate->parseHtmlByName('comment_attachments.html', array(
+            'bx_repeat:attached' => $aTmplImages
+        ));
+    }
+
+    protected function _getTmplVarsNotes($aCmt)
+    {
+        $aTmplVars = array();
+        
+        //--- Pending status related notes.
+        if($aCmt['cmt_status_admin'] == BX_CMT_STATUS_PENDING)
+            $aTmplVars[] = [
+                'style_prefix' => $this->_sStylePrefix,
+                'bx_if:show_note_color' => [
+                    'condition' => true,
+                    'content' => [
+                        'item_note_color' => 'red3'
+                    ]
+                ],
+                'item_note' => _t('_cmt_txt_note_approve_pending')
+            ];
+
+        return [
+            'bx_if:show_notes' => [
+                'condition' => !empty($aTmplVars),
+                'content' => [
+                    'style_prefix' => $this->_sStylePrefix,
+                    'bx_repeat:notes' => $aTmplVars
+                ]
+            ],
+        ];
+    }
+
+    protected function _getTmplVarsAuthor($aCmt)
+    {
+    	list($sAuthorName, $sAuthorLink, $sAuthorIcon, $sAuthorUnit) = $this->_getAuthorInfo($aCmt['cmt_author_id']);
+    	$bAuthorIcon = !empty($sAuthorIcon);
+
+    	return array(
+    	    'author_unit' => $sAuthorUnit,
+            'bx_if:show_icon' => array(
+                'condition' => $bAuthorIcon,
+                'content' => array(
+                    'author_icon' => $sAuthorIcon
+                )
+            ),
+            'bx_if:show_icon_empty' => array(
+                'condition' => !$bAuthorIcon,
+                'content' => array()
+            ),
+            'bx_if:show_author_link' => array(
+                'condition' => !empty($sAuthorLink),
+                'content' => array(
+                    'author_link' => $sAuthorLink,
+            		'author_title' => bx_html_attribute($sAuthorName),
+                    'author_name' => $sAuthorName
+                )
+            ),
+            'bx_if:show_author_text' => array(
+                'condition' => empty($sAuthorLink),
+                'content' => array(
+                    'author_name' => $sAuthorName
+                )
+            ),
+    	);
+    }
+
+    protected function _getTmplVarsText($aCmt)
+    {
+    	$sText = $aCmt['cmt_text'];
+        $sTextMore = '';
+
+        if(!$this->isHtml()) {
+            $iMaxLength = (int)$this->_aSystem['chars_display_max'];
+            if(strlen($sText) > $iMaxLength) {
+                $iLength = strpos($sText, ' ', $iMaxLength);
+    
+                $sTextMore = trim(substr($sText, $iLength));
+                $sText = trim(substr($sText, 0, $iLength));
+            }
+        }
+
+        $sText = $this->_prepareTextForOutput($sText, $aCmt['cmt_id']);
+        $sTextMore = $this->_prepareTextForOutput($sTextMore, $aCmt['cmt_id']);
+
+        return array(
+			'text' => $sText,
+            'bx_if:show_more' => array(
+                'condition' => !empty($sTextMore),
+                'content' => array(
+                    'style_prefix' => $this->_sStylePrefix,
+                    'js_object' => $this->_sJsObjName,
+                    'text_more' => $sTextMore
+                )
+            ),
+        );
+    }
+}
+
+/** @} */

+ 589 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseCmtsServices.php

@@ -0,0 +1,589 @@
+<?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 serviceGetBlockView($sSystem = '', $iObjectId = 0, $iCommentId = 0)
+    {
+        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($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);
+        
+        $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
+
+        $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']) ? [['cmt_id' => $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' => $aBp['count'],
+            'order' => $aParams['order_way'],
+            'view' => $aDp['type'],
+            'module' => $oCmts->getSystemName(), 
+            'object_id' => $oCmts->getId(),
+            '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;
+    }
+}
+
+/** @} */

+ 897 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseFunctions.php

@@ -0,0 +1,897 @@
+<?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 BxBaseFunctions extends BxDolFactory implements iBxDolSingleton
+{
+    protected $_oTemplate;
+
+    protected $_sDesignBoxMenuTmplDefault;
+
+    protected $_sDesignBoxMenuIcon;
+    protected $_sDesignBoxMenuIconType;
+    protected $_sDesignBoxMenuClick;
+
+    protected function __construct($oTemplate)
+    {
+        parent::__construct();
+
+        $this->_oTemplate = $oTemplate ? $oTemplate : BxDolTemplate::getInstance();
+
+        $this->_sDesignBoxMenuTmplDefault = 'menu_block_submenu_ver.html';
+
+        $this->_sDesignBoxMenuIcon = 'ellipsis-v';
+        $this->_sDesignBoxMenuIconType = 'icon';
+        $this->_sDesignBoxMenuClick = "bx_menu_popup_inline('#{design_box_menu}', this, " . json_encode(array(
+            'moveToDocRoot' => false
+        )) . ")";
+    }
+
+    /**
+     * 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);
+    }
+
+    public static function getInstanceWithTemplate($oTemplate)
+    {
+        $sClassName = 'BxTemplFunctions';
+        if ($oTemplate){
+            $sClassName .= get_class($oTemplate);
+        }
+        if(!isset($GLOBALS['bxDolClasses'][$sClassName]))
+            $GLOBALS['bxDolClasses'][$sClassName] = new BxTemplFunctions($oTemplate);
+
+        return $GLOBALS['bxDolClasses'][$sClassName];
+    }
+    
+    public static function getInstance()
+    {
+        return self::getInstanceWithTemplate(null);
+    }
+
+    function TemplPageAddComponent($sKey)
+    {
+        $mixedResult = false; // if you have not such component, return false!
+
+        switch($sKey) {
+            case 'sys_header_width':
+                $mixedResult = 'bx-def-page-width';
+                break;
+        }
+
+        return $mixedResult;
+    }
+
+    function msgBox($sText, $iTimer = 0, $sOnClose = "")
+    {
+        $iId = time() . mt_rand(1, 1000);
+
+        if($iTimer > 0)
+            BxDolTemplate::getInstance()->addJs(array('jquery.anim.js'));
+
+        return $this->_oTemplate->parseHtmlByName('messageBox.html', array(
+            'id' => $iId,
+            'msgText' => $sText,
+            'bx_if:timer' => array(
+                'condition' => $iTimer > 0,
+                'content' => array(
+                    'id' => $iId,
+                    'time' => 1000 * $iTimer,
+                    'on_close' => $sOnClose,
+                )
+            )
+        ));
+    }
+
+    /**
+     * Get standard popup box with title.
+     *
+     * @param  string $sName    - unique name
+     * @param  string $sTitle   - translated title
+     * @param  string $sContent - content of the box
+     * @return HTML   string
+     */
+    function popupBox($sName, $sTitle, $sContent, $isHiddenByDefault = false)
+    {
+        $iId = !empty($sName) ? $sName : time();
+
+        return $this->_oTemplate->parseHtmlByName('popup_box.html', array(
+            'id' => $iId,
+            'wrapper_style' => $isHiddenByDefault ? 'display:none;' : '',
+            'title' => $sTitle,
+            'content' => $sContent
+        ));
+    }
+
+    /**
+     * Get popup box without title.
+     *
+     * @param  string $sName    - unique name
+     * @param  string $sContent - content of the box
+     * @return HTML   string
+     */
+    function transBox($sName, $sContent, $isHiddenByDefault = false, $isPlaceInCenter = false)
+    {
+    	return $this->simpleBox($sName, $sContent, $isHiddenByDefault, $isPlaceInCenter, 'popup_trans.html');
+    }
+
+    function slideBox($sName, $sContent, $isHiddenByDefault = false)
+    {
+    	return $this->simpleBox($sName, $sContent, $isHiddenByDefault, false, 'popup_slide.html');
+    }
+
+    function inlineBox($sName, $sContent, $isHiddenByDefault = false)
+    {
+        return $this->simpleBox($sName, $sContent, $isHiddenByDefault, false, 'popup_inline.html');
+    }
+
+    protected function simpleBox($sName, $sContent, $isHiddenByDefault, $isPlaceInCenter, $sTemplate) 
+    {
+    	$iId = !empty($sName) ? $sName : time();
+
+        if(!is_array($sContent))
+            $sContent = array('content' => $sContent);
+
+        $sContent = $this->_oTemplate->parseHtmlByName($sTemplate, array_merge(array(
+            'id' => $iId,
+            'wrapper_class' => '',
+            'wrapper_style' => $isHiddenByDefault ? 'display:none;' : '',
+            'content' => ''
+        ), $sContent));
+
+        if($isPlaceInCenter)
+            $sContent = '<div class="login_ajax_wrap">' . $sContent . '</div>';
+
+        return $sContent;
+    }
+
+    function simpleBoxContent($sContent, $bWithIndent = true)
+    {
+        if(!$bWithIndent)
+            return $sContent;
+
+        return $this->_oTemplate->parseHtmlByName('popup_content_indent.html', array(
+            'content' => $sContent
+        ));
+    }
+
+    function getIcon($sCode, $aAttrs = array())
+    {
+        $sIconFont = false;
+        $sIconA = false;
+        $sIconUrl = false;
+        $sIconHtml = false;
+        $sIconFontWithHtml = false;
+
+        $sClass = '';
+        if(!empty($aAttrs['class'])) {
+            $sClass = ' ' . $aAttrs['class'] .' ';
+            unset($aAttrs['class']);
+        }
+
+        $sAttrs = '';
+        foreach($aAttrs as $sKey => $sValue)
+            $sAttrs .= ' ' . $sKey . '="' . bx_html_attribute($sValue) . '"';
+
+        if (!empty($sCode)) {
+            if (is_numeric($sCode) && (int)$sCode > 0) {
+                $oStorage = BxDolStorage::getObjectInstance(BX_DOL_STORAGE_OBJ_IMAGES);
+                $sIconUrl = $oStorage ? $oStorage->getFileUrlById((int)$sCode) : false;
+            } 
+            else {
+                //--- Process Inline SVG
+                if (strpos($sCode, '&lt;svg') !== false || strpos($sCode, '<svg') !== false) {
+                    if(strpos($sCode, '&lt;svg') !== false)
+                        $sIconHtml = htmlspecialchars_decode($sCode);
+                    else
+                        $sIconHtml = $sCode;    
+
+                    $sClass .= 'sys-icon sys-icon-svg ';
+                    
+                    
+                    $sIconHtmlClear = strip_tags($sIconHtml, '<svg>');
+                    if ($sClass != '' && strpos($sIconHtmlClear, 'class="') !== false)
+                        $sIconHtml = str_replace('class="', 'class="' . $sClass, $sIconHtml);
+                    else
+                        $sIconHtml = str_replace('<svg', '<svg class="' . $sClass . '" ', $sIconHtml);
+
+                    if ($sAttrs != '')
+                        $sIconHtml = str_replace('<svg', '<svg ' . $sAttrs . ' ', $sIconHtml);
+                }
+                else {
+                    $sEmojIsRegex =
+                        '/[\x{0080}-\x{02AF}'
+                        .'\x{0300}-\x{03FF}'
+                        .'\x{0600}-\x{06FF}'
+                        .'\x{0C00}-\x{0C7F}'
+                        .'\x{1DC0}-\x{1DFF}'
+                        .'\x{1E00}-\x{1EFF}'
+                        .'\x{2000}-\x{209F}'
+                        .'\x{20D0}-\x{214F}'
+                        .'\x{2190}-\x{23FF}'
+                        .'\x{2460}-\x{25FF}'
+                        .'\x{2600}-\x{27EF}'
+                        .'\x{2900}-\x{29FF}'
+                        .'\x{2B00}-\x{2BFF}'
+                        .'\x{2C60}-\x{2C7F}'
+                        .'\x{2E00}-\x{2E7F}'
+                        .'\x{3000}-\x{303F}'
+                        .'\x{A490}-\x{A4CF}'
+                        .'\x{E000}-\x{F8FF}'
+                        .'\x{FE00}-\x{FE0F}'
+                        .'\x{FE30}-\x{FE4F}'
+                        .'\x{1F000}-\x{1F02F}'
+                        .'\x{1F0A0}-\x{1F0FF}'
+                        .'\x{1F100}-\x{1F64F}'
+                        .'\x{1F680}-\x{1F6FF}'
+                        .'\x{1F910}-\x{1F96B}'
+                        .'\x{1F980}-\x{1F9E0}]/u';
+
+                    //--- Process Emoji
+                    if(preg_match($sEmojIsRegex, $sCode, $aTmp))
+                        $sIconHtml = $this->_oTemplate->parseHtmlByName('icon_emoji.html', array(
+                            'icon' => $sCode, 
+                            'class' => $sClass, 
+                            'attrs' => $sAttrs
+                        ));
+                    else {
+                        if (strpos($sCode, '.') === false) {
+                            //--- Process animated icon
+                            if (strncmp($sCode, 'a:', 2) === 0)
+                                $sIconA = substr($sCode, 2);
+                            //--- Process font icons
+                            else {
+                                $sIconFont = $sCode;
+                                $sIconFontWithHtml = $this->getFontIconAsHtml($sIconFont, $sClass, $sAttrs);
+                            }
+                        } 
+                        else {
+                            //--- Process common image
+                            if((!preg_match('/^https?:\/\//', $sCode)))
+                                $sIconUrl = $this->_oTemplate->getIconUrl($sCode);
+                            else
+                                $sIconUrl = $sCode;
+                        }
+                    }
+                }
+            }
+        }
+
+        return array ($sIconFont, $sIconUrl, $sIconA, $sIconHtml, $sIconFontWithHtml);
+    }
+
+    function getFontIconAsHtml($sIconFont, $sClass = '', $sAttrs = '')
+    {
+        return  '<i class="sys-icon ' . $sIconFont .' ' . $sClass . '"' . $sAttrs . '></i>';
+    }
+
+    function getIconAsHtml($sCode, $aAttrs = array())
+    {
+        $aIcons = $this->getIcon($sCode, $aAttrs);
+        return $aIcons[3] . $aIcons[4]; 
+    }
+
+    function getTemplateIcon($sName)
+    {
+        $sUrl = $this->_oTemplate->getIconUrl($sName);
+        return !empty($sUrl) ? $sUrl : $this->_oTemplate->getIconUrl('spacer.gif');
+    }
+
+    function getTemplateImage($sName)
+    {
+        $sUrl = $this->_oTemplate->getImageUrl($sName);
+        return !empty($sUrl) ? $sUrl : $this->_oTemplate->getImageUrl('spacer.gif');
+    }
+
+    function sysIcon ($sIcon, $sName, $sUrl = '', $iWidth = 0)
+    {
+        return '<div class="sys_icon">' . ($sUrl ? '<a title="'.$sName.'" href="'.$sUrl.'">' : '') . '<img alt="'.$sName.'" src="'.$sIcon.'" '.($iWidth ? 'width='.$iWidth : '').' />' . ($sUrl ? '</a>' : '') . '</div>';
+    }
+
+    /**
+     * functions for limiting maximal string length
+     */
+    function getStringWithLimitedLength($sString, $iWidth = 45, $isPopupOnOverflow = false, $bReturnString = true)
+    {
+        if (empty($sString) || mb_strlen($sString, 'UTF-8') <= $iWidth)
+            return $bReturnString ? $sString : array($sString);
+
+        $sResult = '';
+        $aWords = mb_split("[\s\r\n]", $sString);
+        $iPosition = 0;
+        $iWidthReal = $iWidth - 3;
+        $iWidthMin = $iWidth - 15;
+        foreach($aWords as $sWord) {
+            $sWord = trim($sWord);
+            $iWord = mb_strlen($sWord, 'UTF-8');
+            if ($iPosition + $iWord > $iWidthReal)
+                break;
+
+            // add word and continue
+            $sResult .= ' ' . $sWord;
+            $iPosition += 1 + $iWord;
+        }
+
+        // last word is too long, cut it
+        if(!$iPosition || $iPosition < $iWidthMin)
+            $sResult .= ' ' . mb_substr($sWord, 0, $iWidthReal - $iPosition - $iWord, 'UTF-8');
+        $sResult = trim($sResult);
+
+        // add tripple dot
+        if(!$isPopupOnOverflow) {
+            $sResult .= '...';
+            return $bReturnString ? $sResult : array($sResult);
+        }
+
+        // add button width popup
+        $sId = 'bx-str-limit-' . rand(1, PHP_INT_MAX);
+        $sPopup = '<span class="bx-str-limit" onclick="$(\'#' . $sId . '\').dolPopup({pointer:{el:$(this), offset:\'10 1\'}})"/><i class="sys-icon ellipsis-h"></i></span>';
+        $sPopup .= '<div id="' . $sId . '" style="display:none;">' . BxTemplFunctions::getInstance()->transBox('', '<div class="bx-def-padding bx-def-color-bg-block">'.$sString.'</div>') . '</div>';
+
+        return $bReturnString ? $sResult . $sPopup : array($sResult, $sPopup);
+    }
+
+    /**
+     * Display design box with specified title, template, content and menu.
+     * @param $sTitle - design box title, please note that some templates don't use title.
+     * @param $sContent - design box content.
+     * @param $iTemplateNum - number of design box template, use predefined contants only, default is BX_DB_DEF.
+     * @param $mixedMenu - design box menu, it can be:
+     *      - object: instance of BxTemplMenu class
+     *      - string: menu object identifier
+     *      - array: array of menu links to create menu from
+     * @param $mixedButtons - design box menu representation, it can be:
+     *      - false: design box menu will be used as horizontal menu (tabs)
+     *      - array: array of menu links to create menu from. If empty array is used and 'design box menu' isn't empty, then 'design box menu' will be added as one of menu items automatically. If non-empty array is used and 'design box menu' isn't empty then it should be added as one of menu items. Use array('menu' => 1) to define menu item for 'design box menu'.
+     * @return string
+     *
+     * @see BX_DB_CONTENT_ONLY
+     * @see BX_DB_DEF
+     * @see BX_DB_EMPTY
+     * @see BX_DB_NO_CAPTION
+     * @see BX_DB_PADDING_CONTENT_ONLY
+     * @see BX_DB_PADDING_DEF
+     * @see BX_DB_PADDING_NO_CAPTION
+     */
+    function designBoxContent ($sTitle, $sContent, $iTemplateNum = BX_DB_DEF, $mixedMenu = false, $mixedButtons = array())
+    {
+        return $this->_oTemplate->parseHtmlByName('designbox_' . (int)$iTemplateNum . '.html', array(
+            'title' => $sTitle,
+            'designbox_content' => $sContent,
+            'caption_item' => $this->designBoxMenu($mixedMenu, $mixedButtons),
+        ));
+    }
+
+    function designBoxMenu ($mixedMenu, $mixedButtons = array())
+    {
+        $bUseTabs = is_bool($mixedButtons) && $mixedButtons === true;
+
+        $sClass = $sMenu = '';
+        if(!empty($mixedMenu)) {
+            if(is_string($mixedMenu)) {
+                if(($oMenu = BxTemplMenu::getObjectInstance($mixedMenu)) !== false) {
+                    $oMenu->setTemplateById($bUseTabs ? BX_DB_MENU_TEMPLATE_TABS : BX_DB_MENU_TEMPLATE_POPUP);
+
+                    $sMenu = $oMenu->getCode();
+                }
+                else
+                    $sMenu = $mixedMenu;
+            } 
+            else if(is_array($mixedMenu)) {
+                if(isset($mixedMenu['template']) && isset($mixedMenu['menu_items']))
+                    $aMenu = $mixedMenu;
+                else
+                    $aMenu = array('template' => $this->_sDesignBoxMenuTmplDefault, 'menu_items' => $mixedMenu);
+
+                if(($oMenu = new BxTemplMenu($aMenu, $this->_oTemplate)) !== false) {
+                    $oMenu->setTemplateById($bUseTabs ? BX_DB_MENU_TEMPLATE_TABS : BX_DB_MENU_TEMPLATE_POPUP);
+
+                    $sMenu = $oMenu->getCode();
+                }
+                else
+                    $sMenu = '';
+            }
+            else if(is_object($mixedMenu) && is_a($mixedMenu, 'BxTemplMenu')) {
+                $mixedMenu->setTemplateById($bUseTabs ? BX_DB_MENU_TEMPLATE_TABS : BX_DB_MENU_TEMPLATE_POPUP);
+                if(($mixedMenu instanceof BxBaseMenuMoreAuto) && $mixedMenu->isMoreAuto())
+                    $sClass = ' bx-db-menu-tab-more-auto';
+
+                $sMenu = $mixedMenu->getCode();
+            }
+        }
+        $bMenu = !empty($sMenu);
+
+        $sResult = '';
+        if($bUseTabs && $bMenu)
+            $sResult = $sMenu;
+        else if(is_array($mixedButtons)) {
+            $sPopup = '';
+
+            if($bMenu) {
+                $aButton = array();
+                if(empty($mixedButtons))
+                    list($aButton, $sPopup) = $this->_designBoxMenuButton($sMenu);
+                //--- For backward compatibility
+                else if(!empty($mixedButtons['menu']) && is_array($mixedButtons['menu'])) {
+                    list($aButton, $sPopup) = $this->_designBoxMenuButton($sMenu, $mixedButtons['menu']);
+                    unset($mixedButtons['menu']);
+                }
+
+                if(!empty($aButton))
+                    $mixedButtons[] = $aButton;
+            }
+
+            foreach($mixedButtons as $aButton) {
+                if($bMenu && isset($aButton['menu'])) {
+                    if(is_numeric($aButton['menu']) && (int)$aButton['menu'] == 1)
+                        list($aButton, $sPopup) = $this->_designBoxMenuButton($sMenu);
+                    else if(is_array($aButton['menu']))
+                        list($aButton, $sPopup) = $this->_designBoxMenuButton($sMenu, $aButton['menu']);
+
+                    if(isset($aButton['menu']))
+                        continue;
+                }
+
+                $aAttrs = array();
+                if(!empty($aButton['onclick']))
+                    $aAttrs['onclick'] = $aButton['onclick'];
+                
+                $aAttrs['class'] = 'bx-btn bx-btn-small';
+                if(!empty($aButton['class']))
+                    $aAttrs['class'] .= ' ' . trim($aButton['class']);
+
+                $bTmplVarsButtonIcon = !empty($aButton['icon']);
+                $aTmplVarsButtonIcon = !$bTmplVarsButtonIcon ? array() : array(
+                    'icon' => $aButton['icon']
+                );
+
+                $bTmplVarsButtonIconA = !empty($aButton['icon-a']);
+                $aTmplVarsButtonIconA = !$bTmplVarsButtonIconA ? array() : array(
+                    'icon_a' => $aButton['icon-a']
+                );
+
+                $bTmplVarsButtonTitle = !empty($aButton['title']);
+                $aTmplVarsButtonTitle = !$bTmplVarsButtonTitle ? array() : array(
+                    'title' => $aButton['title']
+                );
+
+                $sResult .= $this->_oTemplate->parseHtmlByName('designbox_menu_button.html', array(
+                    'attrs' => bx_convert_array2attrs($aAttrs),
+                    'bx_if:show_icon' => array(
+                        'condition' => $bTmplVarsButtonIcon,
+                        'content' => $aTmplVarsButtonIcon
+                    ),
+                    'bx_if:show_icon_a' => array(
+                        'condition' => $bTmplVarsButtonIconA,
+                        'content' => $aTmplVarsButtonIconA
+                    ),
+                    'bx_if:show_title' => array(
+                        'condition' => $bTmplVarsButtonTitle,
+                        'content' => $aTmplVarsButtonTitle
+                    )
+                ));
+            }
+
+            $sResult .= $sPopup;
+        }
+
+        if(!empty($sResult))
+            $sResult = $this->_oTemplate->parseHtmlByName('designbox_menu.html', array(
+                'class' => $sClass,
+                'content' => $sResult
+            ));
+
+        return $sResult;
+    }
+
+    protected function _designBoxMenuId ()
+    {
+        return 'bx-menu-db-' . time() . rand(0, PHP_INT_MAX);
+    }
+
+    protected function _designBoxMenuButton ($sMenu, $aParams = array())
+    {
+        $sId = $this->_designBoxMenuId();
+        $aButton = array($this->_sDesignBoxMenuIconType => $this->_sDesignBoxMenuIcon, 'onclick' => $this->_sDesignBoxMenuClick);
+
+        if(!empty($aParams)) {
+            if(!empty($aParams['id']))
+                $sId = $aParams['id'];
+
+            $aButton = array_merge($aButton, $aParams);
+        }
+
+        $aButton['onclick'] = bx_replace_markers($aButton['onclick'], array(
+            'design_box_menu' => $sId
+        ));
+
+        $sMenu = $this->_oTemplate->parseHtmlByName('designbox_menu_popup.html', array(
+            'content' => $sMenu
+        ));
+
+        return array($aButton, $this->transBox($sId, $sMenu, true));
+    }
+
+    /**
+     * Get logo URL.
+     * @return string
+     */
+    function getMainLogoUrl()
+    {
+        return BxDolDesigns::getInstance()->getSiteLogoUrl();
+    }
+
+    /**
+     * Get mark URL.
+     * @return string
+     */
+    function getMainMarkUrl()
+    {
+        return BxDolDesigns::getInstance()->getSiteMarkUrl();
+    }
+
+    /**
+     * Get logo HTML.
+     * @return string
+     */
+    function getMainLogo($aParams = array())
+    {
+        $oDesigns = BxDolDesigns::getInstance();
+
+        $sAlt = $oDesigns->getSiteLogoAlt();
+        if(empty($sAlt))
+            $sAlt = getParam('site_title');
+
+        $sLogo = '<span>' . $sAlt . '</span>';
+
+        $sFileUrl = $this->getMainLogoUrl();
+        if (!empty($sFileUrl)) {
+            $iLogoWidth = (int)$oDesigns->getSiteLogoWidth();
+            $sWidth = $iLogoWidth > 0 ? 'width:' . round($iLogoWidth/16, 3) . 'rem;' : '';
+
+            $iLogoHeight = (int)$oDesigns->getSiteLogoHeight();
+            $sHeight = $iLogoHeight > 0 ? 'height:' . round($iLogoHeight/16, 3) . 'rem;' : '';
+
+            $sLogo = '<img style="' . $sWidth . ' ' . $sHeight . '" src="' . $sFileUrl . '" id="bx-logo" alt="' . bx_html_attribute($sAlt, BX_ESCAPE_STR_QUOTE) . '" />';
+        }
+
+        $aAttrs = array(
+            'class' => 'bx-def-font-contrasted',
+            'href' => BX_DOL_URL_ROOT,
+            'title' => bx_html_attribute($sAlt, BX_ESCAPE_STR_QUOTE)
+        );
+        if(!empty($aParams['attrs']) && is_array($aParams['attrs']))
+            $aAttrs = array_merge($aAttrs, $aParams['attrs']);
+
+        return '<a' . bx_convert_array2attrs($aAttrs) . '>' . $sLogo . '</a>';
+    }
+
+    /**
+     * Get HTML code for manifests.
+     * @return HTML string to insert into HEAD section
+     */
+    function getManifests()
+    {
+        $aUrlRoot = parse_url(BX_DOL_URL_ROOT);
+
+        $sUrl = bx_append_url_params(BX_DOL_URL_ROOT .  'manifest.json.php', [
+            'bx_name' => $aUrlRoot['host']
+        ]);
+
+        return '<link rel="manifest" href="' . $sUrl . '" crossorigin="use-credentials" />';
+    }
+
+    /**
+     * Get HTML code for meta icons.
+     * @return HTML string to insert into HEAD section
+     */
+    function getMetaIcons()
+    {
+        // favicon icon
+        $sImageUrlFav = '';
+        if(($iId = (int)getParam('sys_site_icon')) != 0)
+            $sImageUrlFav = BxDolStorage::getObjectInstance(BX_DOL_STORAGE_OBJ_FILES)->getFileUrlById($iId);          
+
+        // svg icon
+        $sImageUrlSvg = '';
+        if(($iId = (int)getParam('sys_site_icon_svg')) != 0)
+            $sImageUrlSvg = BxDolStorage::getObjectInstance(BX_DOL_STORAGE_OBJ_IMAGES)->getFileUrlById($iId);
+
+        if(empty($sImageUrlFav) && empty($sImageUrlSvg))
+            $sImageUrlFav = $sImageUrlSvg = $this->_oTemplate->getIconUrl('favicon.svg');
+
+        // apple device icon
+        $sImageUrlApl = '';
+        if(($iId = (int)getParam('sys_site_icon_apple')) != 0)
+            $sImageUrlApl = BxDolTranscoderImage::getObjectInstance(BX_DOL_TRANSCODER_OBJ_ICON_APPLE)->getFileUrl($iId);
+        if(empty($sImageUrlApl))
+            $sImageUrlApl = $this->_oTemplate->getIconUrl('apple-touch-icon.png');
+
+/* 
+ * TODO: 
+ * 1. Remove commented code later if it won't be used.
+ * 2. Remove 'sys_icon_favicon' and 'sys_icon_facebook' transcoders and related transcoder filters.
+ * 
+        // facebook icon
+        $sImageUrlFcb = '';
+        $oTranscoder = BxDolTranscoderImage::getObjectInstance(BX_DOL_TRANSCODER_OBJ_ICON_FACEBOOK);
+        $sImageUrlFcb = $oTranscoder->getFileUrl($iId);
+        if(empty($sImageUrlFcb))
+            $sImageUrlFcb = $this->_oTemplate->getIconUrl('facebook-icon.png');
+*/
+
+        $sRet = '';
+        if($sImageUrlFav)
+            $sRet .= '<link rel="icon" href="' . $sImageUrlFav . '" sizes="any" />';
+        if($sImageUrlFav)
+            $sRet .= '<link rel="icon" href="' . $sImageUrlSvg . '" type="image/svg+xml" />';
+        $sRet .= '<link rel="apple-touch-icon" href="' . $sImageUrlApl . '" />';
+
+/*
+        $sRet .= '<link rel="image_src" sizes="100x100" href="' . $sImageUrlFcb . '" />';
+*/
+
+        return $sRet;
+    }
+
+    function getInjectionHead() 
+    {
+        return $this->getInjection('getInjHead');
+    }
+
+    function getInjectionHeader() 
+    {
+        return $this->getInjection('getInjHeader');
+    }
+
+    function getInjectionFooter() 
+    {
+        return $this->getInjection('getInjFooter');
+    }
+
+    public function getPopupAlert()
+    {
+        return $this->transBox('bx-popup-alert', $this->_oTemplate->parseHtmlByName('popup_trans_alert_cnt.html', array()), true);
+    }
+
+    public function getPopupConfirm()
+    {
+        return $this->transBox('bx-popup-confirm', $this->_oTemplate->parseHtmlByName('popup_trans_confirm_cnt.html', array()), true);
+    }
+
+    public function getPopupPrompt()
+    {
+        $sInputText = 'bx-popup-prompt-value';
+        $aInputText = array(
+            'type' => 'text',
+            'name' => $sInputText,
+            'attrs' => array(
+                'id' => $sInputText,
+            ),
+            'value' => '',
+            'caption' => ''
+        );
+
+        $oForm = new BxTemplFormView(array(), $this->_oTemplate);
+        return $this->transBox('bx-popup-prompt', $this->_oTemplate->parseHtmlByName('popup_trans_prompt_cnt.html', array(
+            'input' => $oForm->genRow($aInputText)
+        )), true);
+    }
+
+    /**
+     * Output time wrapped in <time> tag in HTML.
+     * Then time is autoformatted using JS upon page load, this is aumatically converted to user's timezone and
+     * updated in realtime in case of short periods of 'from now' time format.
+     *
+     * Short version of this function:
+     * @see bx_time_js
+     *
+     * @param $iUnixTimestamp time as unixtimestamp
+     * @param $sFormatIdentifier output format identifier
+     *     @see BX_FORMAT_DATE
+     *     @see BX_FORMAT_TIME
+     *     @see BX_FORMAT_DATE_TIME
+     * @param $bForceFormat force provided format and don't use "from now" time autoformat.
+     */
+    function timeForJs ($iUnixTimestamp, $sFormatIdentifier = BX_FORMAT_DATE, $bForceFormat = false)
+    {
+        $sDateUTC = bx_time_utc ($iUnixTimestamp);
+        return $this->timeForJsFullDate ($sDateUTC, $sFormatIdentifier, $bForceFormat);
+    }
+
+    /**
+     * Same as @see timeForJs but instead of unxtimestamp full date format is used (ex: 2005-08-15T15:52:01) as passec date param
+     */ 
+    function timeForJsFullDate ($sDateUTC, $sFormatIdentifier = BX_FORMAT_DATE, $bForceFormat = false, $bUTC = false)
+    {
+        return '<time datetime="' . $sDateUTC . '" data-bx-format="' . getParam($sFormatIdentifier) . '" data-bx-autoformat="' . ($bForceFormat ? 0 : getParam('sys_format_timeago')) . '" data-bx-utc="' . ($bUTC ? 1 : 0) . '">' . $sDateUTC . '</time>';
+    }
+    
+    function statusOnOff ($mixed, $isMsg = false)
+    {
+        if ((is_bool($mixed) && !$mixed) || (is_string($mixed) && 'fail' == $mixed))
+            return '<i class="sys-icon circle col-red2"></i> ' . ($isMsg ? _t('_sys_off') : '');
+        elseif (is_string($mixed) && 'warn' == $mixed)
+            return '<i class="sys-icon circle col-red3"></i> ' . ($isMsg ? _t('_sys_warn') : '');
+        elseif (is_string($mixed) && 'undef' == $mixed)
+            return '<i class="sys-icon circle col-gray"></i> ' . ($isMsg ? _t('_undefined') : '');
+        else
+            return '<i class="sys-icon circle col-green1"></i> ' . ($isMsg ? _t('_sys_on') : '');
+    }
+
+    /**
+     * Ouputs HTML5 video player.
+     * @param $sUrlPoster video poster image
+     * @param $sUrlMP4 .mp4 video
+     * @param $sUrlMP4Hd .mp4 video in better quality
+     * @param $aAttrs custom attributes, defaults are: controls="" preload="none" autobuffer=""
+     * @param $sStyles custom styles, defaults are: width:100%; height:auto;
+     */
+    function videoPlayer ($sUrlPoster, $sUrlMP4, $sUrlMP4Hd = '', $aAttrs = false, $sStyles = false, $bDynamicMode = false)
+    {
+        $oPlayer = BxDolPlayer::getObjectInstance();
+        if(!$oPlayer)
+            return '';
+
+        if($sStyles === false)
+            $sStyles = 'width:100%; height:auto;';
+
+        return $oPlayer->getCodeVideo (BX_PLAYER_STANDARD, array(
+            'poster' => $sUrlPoster,
+            'mp4' => array('sd' => $sUrlMP4, 'hd' => $sUrlMP4Hd),
+            'attrs' => $aAttrs,
+            'styles' => $sStyles,
+        ), $bDynamicMode);
+    }
+
+    protected function getInjection($sPrefix)
+    {
+        $sContent = '';
+
+        $aMethods = get_class_methods($this);
+        foreach($aMethods as $sMethod)
+            if(preg_match("/^(" . $sPrefix . ")[A-Z].+$/", $sMethod))
+                $sContent .= $this->$sMethod();
+
+        return $sContent;
+    }
+
+    protected function getInjHeadLiveUpdates() 
+    {
+        $sContent = '';
+
+        if(($oLiveUpdates = BxDolLiveUpdates::getInstance()) !== false)
+            $sContent .= $oLiveUpdates->init();
+
+        return $sContent;
+    }
+
+    protected function getInjHeaderPushNotifications() 
+    {
+        $iProfileId = bx_get_logged_profile_id();
+        if(empty($iProfileId))
+            return '';
+
+        $sAppId = getParam('sys_push_app_id');
+        if(empty($sAppId))
+            return '';
+
+        $aTags = BxDolPush::getTags($iProfileId);
+        if (!$aTags)
+            return '';
+
+        $sShortName = getParam('sys_push_short_name');
+        $sSafariWebId = getParam('sys_push_safari_id');
+
+        $sSubfolder = '/plugins_public/onesignal/';
+        $aUrl = parse_url(BX_DOL_URL_ROOT);
+        if(!empty($aUrl['path'])) {
+            $sPath = trim($aUrl['path'], '/');
+            if(!empty($sPath))
+                $sSubfolder = '/' . $sPath . $sSubfolder;
+        }
+
+        $this->_oTemplate->addJs(array(
+            'https://cdn.onesignal.com/sdks/OneSignalSDK.js',
+            'BxDolPush.js',
+        ));
+
+        $sJsClass = 'BxDolPush';
+        $sJsObject = 'oBxDolPush';
+
+        $sContent = "var " . $sJsObject . " = new " . $sJsClass . "(" . json_encode(array(
+            'sObjName' => $sJsObject,
+            'sSiteName' => getParam('site_title'),
+            'aTags' => $aTags,
+            'sAppId' => $sAppId,
+            'sShortName' => $sShortName,
+            'sSafariWebId' => $sSafariWebId,
+            'sSubfolder' => $sSubfolder,
+            'sNotificationUrl' => BX_DOL_URL_ROOT,
+            'sTxtNotificationRequest' => _t('_sys_push_notification_request', getParam('site_title')),
+            'sTxtNotificationRequestYes' => _t('_sys_push_notification_request_yes'),
+            'sTxtNotificationRequestNo' => _t('_sys_push_notification_request_no'),
+        )) . ");";
+
+        return $this->_oTemplate->_wrapInTagJsCode($sContent);
+    }
+
+    protected function getInjHeaderPopupLoading() 
+    {
+        return $this->transBox('bx-popup-loading', $this->_oTemplate->parsePageByName('popup_loading.html', array()), true);  
+    }
+    
+    protected function getInjFooterMenuLoading() 
+    {
+        return $this->_oTemplate->parsePageByName('menu_loading.html', array());  
+    }
+
+    protected function getInjFooterPopupMenus() 
+    {
+        $sContent = '';
+
+        $oSearch = new BxTemplSearch();
+        $oSearch->setLiveSearch(true);
+        $sContent .= $this->_oTemplate->parsePageByName('search.html', array(
+            'search_form' => $oSearch->getForm(BX_DB_CONTENT_ONLY),
+            'results' => $oSearch->getResultsContainer(),
+        ));
+
+        $sContent .= $this->_oTemplate->getMenu ('sys_site');
+        if(isLogged()) {
+            $sContent .= $this->_oTemplate->getMenu ('sys_add_content');
+            $sContent .= $this->_oTemplate->getMenu ('sys_account_popup');
+        }
+
+        return $sContent;
+    }
+    
+    protected function getInjFooterPopupApps() 
+    {
+        if($this->_oTemplate->getPageType() != BX_PAGE_TYPE_APPLICATION) 
+            return '';
+
+        $oMenu = BxDolMenu::getObjectInstance('sys_homepage');
+        if(!$oMenu) 
+            return '';
+
+        $this->_oTemplate->addJs(['popper.js']);
+        return $this->_oTemplate->parsePageByName('menu_apps.html', [
+            'name' => 'sys-menu-apps',
+            'title' => _t('_apps'),
+            'bx_repeat:menu_items' => $oMenu->getMenuItems(),
+        ]);
+    }
+
+    protected function getInjFooterPopups() 
+    {
+        $sContent = '';
+
+        $sContent .= $this->getPopupAlert();
+        $sContent .= $this->getPopupConfirm();
+        $sContent .= $this->getPopupPrompt();
+
+        return $sContent;
+    }
+
+    protected function getInjFooterEmbed() 
+    {    
+        // Load embed files
+        $oEmbed = BxDolEmbed::getObjectInstance(false);
+        if ($oEmbed) 
+            return $oEmbed->addJsCss() . $oEmbed->addProcessLinkMethod();
+
+        return '';
+    }
+}
+
+/** @} */

+ 398 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseMenu.php

@@ -0,0 +1,398 @@
+<?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");
+
+/**
+ * Menu representation.
+ * @see BxDolMenu
+ */
+class BxBaseMenu extends BxDolMenu
+{
+    protected $_oTemplate;
+    protected $_iPageType; 
+    protected $_aOptionalParams = array('target' => '', 'onclick' => '');
+    protected $_bDisplayAddons = false;
+
+    public function __construct ($aObject, $oTemplate)
+    {
+        parent::__construct ($aObject);
+
+        if ($oTemplate)
+            $this->_oTemplate = $oTemplate;
+        else
+            $this->_oTemplate = BxDolTemplate::getInstance();
+
+        $this->_iPageType = false;
+    }
+
+    public function getDisplayAddons()
+    {
+        return $this->_bDisplayAddons;
+    }   
+
+    public function setDisplayAddons($b)
+    {
+        $bRet = $this->_bDisplayAddons;
+        $this->_bDisplayAddons = $b;
+        return $bRet;
+    }
+
+    /**
+     * Get menu code.
+     * @return string
+     */
+    public function getCode ()
+    {
+        $sMenuTitle = isset($this->_aObject['title']) ? _t($this->_aObject['title']) : 'Menu-' . rand(0, PHP_INT_MAX);
+        if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->beginMenu($sMenuTitle);
+
+        if(!$this->_iPageType)
+            $this->_iPageType = BxDolTemplate::getInstance()->getPageType();
+
+        $s = '';
+        $aVars = $this->_getTemplateVars ();
+        if (!empty($aVars['bx_repeat:menu_items'])) {
+            $this->_addJsCss();
+            $s = $this->_getCode($this->_aObject['template'], $aVars);
+        }
+
+        if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->endMenu($sMenuTitle);
+
+        return $s;
+    }
+
+    /**
+     * Get menu code API.
+     * @return array
+     */
+    public function getCodeAPI ()
+    {
+        $aItems = [];
+
+        $aVars = $this->_getTemplateVars();
+        if(!empty($aVars['bx_repeat:menu_items']))
+            $aItems = $aVars['bx_repeat:menu_items'];
+
+        return [
+            'object' => $this->_sObject, 
+            'items' => $aItems
+        ];
+    }
+
+    protected function _getCode($sTmplName, $aTmplVars)
+    {
+        return $this->_oTemplate->parseHtmlByName($this->getTemplateName($sTmplName), $aTmplVars);
+    }
+
+    public function getCodeItem ($sName)
+    {
+        if(empty($sName))
+            return '';
+
+        $sCode = $this->_oTemplate->getHtml(str_replace('.html', '_item.html', $this->_aObject['template']));
+        if(empty($sCode))
+            return '';
+
+        $mixedTmplVars = $this->getMenuItem($sName);
+        if($mixedTmplVars === false)
+            return '';
+
+        return $this->_oTemplate->parseHtmlByContent($sCode, $mixedTmplVars);
+    }
+
+    /**
+     * Get template variables array
+     * @return array
+     */
+    protected function _getTemplateVars ()
+    {
+        return array (
+            'object' => $this->_sObject,
+            'bx_repeat:menu_items' => $this->getMenuItems (),
+        );
+    }
+
+    /**
+     * Get menu items array, which is ready to pass to menu template. 
+     * @return array or false
+     */
+    public function getMenuItems ()
+    {
+        if (!isset($this->_aObject['menu_items']))
+            $this->_aObject['menu_items'] = $this->getMenuItemsRaw ();
+
+        $aItems = array();
+        foreach ($this->_aObject['menu_items'] as $aItem) {
+            $aItem = $this->_getMenuItem ($aItem);
+            if($aItem !== false)
+                $aItems[] = $aItem;
+        }
+
+        return $aItems;
+    }
+    
+    /**
+     * Get menu item array, which is ready to pass to whole menu or 
+     * single menu item template. May return false if single menu item 
+     * is requested but cannot be shown by circumstances.
+     * @return array or false
+     */
+    public function getMenuItem ($sName)
+    {
+        if (!isset($this->_aObject['menu_items']))
+            $this->_aObject['menu_items'] = $this->getMenuItemsRaw ();
+
+        if(!empty($sName))
+            return $this->_getMenuItem($this->_aObject['menu_items'][$sName]);
+    }
+
+    /**
+     * Get menu items array, this is just a wrapper for DB function for make it easier to override.
+     * It is used in @see BxBaseMenu::getMenuItems
+     * @return array
+     */
+    protected function getMenuItemsRaw ()
+    {
+        if($this->_bMultilevel)
+            return $this->_oQuery->getMenuItemsHierarchy();
+        else 
+            return $this->_oQuery->getMenuItems();
+    }
+
+    protected function _getMenuItem ($a)
+    {
+        if (isset($a['active']) && !$a['active'])
+            return false;
+
+        if (!$this->_isVisible($a))
+            return false;
+
+        if ($this->_bIsApi) {
+            list ($sIcon, $sIconUrl) = $this->_getMenuIcon($a);
+
+            if(($aMarkers = $this->_getMenuMarkers($a)) && is_array($aMarkers))
+                $this->addMarkers($aMarkers);
+
+            $a = $this->_replaceMarkers($a);
+
+            return [
+                'id' => $a['id'],
+                'name' => $a['name'],
+                'title' => _t($a['title']),
+                'link' => isset($a['link']) ? $this->_oPermalinks->permalink($a['link']) : '',
+                'icon' => $sIcon ? $sIcon : '',
+                'image' => $sIconUrl ? $sIconUrl : ''
+            ];
+        }
+
+        $a['object'] = $this->_sObject;
+
+        $a['title'] = _t($a['title']);
+        $a['bx_if:title'] = array(
+            'condition' => !empty($a['title']),
+            'content' => array(
+                'title' => $a['title']
+            )
+        );
+
+        $this->removeMarker('addon');
+
+        $a = $this->_replaceMarkers($a);
+
+        if ($this->_bDisplayAddons) {
+            $mixedAddon = $this->_getMenuAddon($a);
+            if (!is_array($mixedAddon)) {
+                $this->addMarkers(array('addon' => $mixedAddon));
+                $a = $this->_replaceMarkers($a);
+            }
+        }
+
+        $aMarkers = $this->_getMenuMarkers($a);
+        if ($aMarkers && is_array($aMarkers)) {
+            $this->addMarkers($aMarkers);
+            $a = $this->_replaceMarkers($a);
+        }
+
+        list ($sIcon, $sIconUrl, $sIconA, $sIconHtml) = $this->_getMenuIcon($a);
+
+        $a['class_add'] = $this->_isSelected($a) ? 'bx-menu-tab-active' : '';
+        $a['class_add'] .= $this->_getVisibilityClass($a);
+
+        $a['link'] = isset($a['link']) ? $this->_oPermalinks->permalink($a['link']) : 'javascript:void(0);';
+        $a['title_attr'] = bx_html_attribute(strip_tags($a['title']));
+
+        $a['attrs'] = $this->_getMenuAttrs($a);
+
+        $a['bx_if:image'] = array (
+            'condition' => (bool)$sIconUrl,
+            'content' => array('icon_url' => $sIconUrl),
+        );
+        $a['bx_if:image_inline'] = array (
+            'condition' => false,
+            'content' => array('image' => ''),
+        );
+        $a['bx_if:icon'] = array (
+            'condition' => (bool)$sIcon,
+            'content' => array('icon' => $sIcon),
+        );
+        $a['bx_if:icon-html'] = array (
+            'condition' => (bool)$sIconHtml,
+            'content' => array('icon' => $sIconHtml),
+        );
+        $a['bx_if:icon-a'] = array (
+            'condition' => (bool)$sIconA,
+            'content' => array('icon-a' => $sIconA),
+        );
+        $a['bx_if:title'] = array (
+            'condition' => (bool)$a['title'],
+            'content' => array(
+                'title' => $a['title'],
+                'title_attr' => $a['title_attr']
+            ),
+        );
+
+        $aTmplVarsAddon = $this->_bDisplayAddons ? $this->_getTmplVarsAddon($mixedAddon, $a) : array('addon' => '', 'addonf' => '');
+        $a['bx_if:addon'] = array (
+            'condition' => $this->_bDisplayAddons && !empty($aTmplVarsAddon['addon']),
+            'content' => $aTmplVarsAddon
+        );
+
+        $aTmplVarsSubitems = array('subitems' => '');
+        $bTmplVarsSubitems = $this->_bMultilevel && !empty($a['subitems']);
+        if($bTmplVarsSubitems) {
+            $sClassCollpsed = 'bx-mi-collapsed';
+            if(($iCollapsed = $this->getUserChoiceCollapsedSubmenu($a)) !== false)
+                $a['class_add'] .= $iCollapsed ? ' ' . $sClassCollpsed : '';
+            else if(isset($a['collapsed']) && $a['collapsed'])
+                $a['class_add'] .= ' ' . $sClassCollpsed;
+
+            $aSubitems = array();
+            foreach($a['subitems'] as $aSubitem) {
+                $aSubitem = $this->_getMenuItem($aSubitem);
+                if($aSubitem !== false)
+                    $aSubitems[] = $aSubitem;
+            }
+
+            $aTmplVarsSubitems['subitems'] = $this->_oTemplate->parseHtmlByName(str_replace('.html', '_subitems.html', $this->getTemplateName()), array(
+                'bx_repeat:menu_items' => $aSubitems,
+            ));
+        }
+
+        $a['bx_if:show_subitems'] = array (
+            'condition' => $bTmplVarsSubitems,
+            'content' => $aTmplVarsSubitems
+        );
+
+        unset($a['subitems']);
+
+        foreach ($this->_aOptionalParams as $sName => $sDefaultValue)
+            if (!isset($a[$sName]))
+                $a[$sName] = $sDefaultValue;
+
+        return $a;
+    }
+    
+    protected function _getMenuIcon ($a)
+    {
+        return BxTemplFunctions::getInstanceWithTemplate($this->_oTemplate)->getIcon(!empty($a['icon']) ? $a['icon'] : '');
+    }
+    
+    public function getMenuIconHtml($sIcon)
+    {
+        list ($sIcon, $sIconUrl, $sIconA, $sIconHtml) = BxTemplFunctions::getInstanceWithTemplate($this->_oTemplate)->getIcon($sIcon);
+
+        $a['bx_if:image'] = array (
+            'condition' => (bool)$sIconUrl,
+            'content' => array('icon_url' => $sIconUrl),
+        );
+        $a['bx_if:icon'] = array (
+            'condition' => (bool)$sIcon,
+            'content' => array('icon' => $sIcon),
+        );
+        $a['bx_if:icon-a'] = array (
+            'condition' => (bool)$sIconA,
+            'content' => array('icon-a' => $sIconA),
+        );
+        $a['bx_if:icon-html'] = array (
+            'condition' => (bool)$sIconHtml,
+            'content' => array('icon' => $sIconHtml),
+        );
+
+        return $this->_oTemplate->parseHtmlByName('menu_icon.html', $a);
+    }
+
+    protected function _getMenuAddon ($aMenuItem)
+    {
+        if (empty($aMenuItem['addon']))
+            return '';
+
+        if (isset($aMenuItem['addon_cache']) && $aMenuItem['addon_cache']) {
+            $oCache = BxDolDb::getInstance()->getDbCacheObject();
+            $sKey = 'menu_' . $this->_sObject . '_' . $aMenuItem['name'] . '_' . bx_get_logged_profile_id() . '_' . bx_site_hash() . '.php';
+            $s = $oCache->getData($sKey);
+            if ($s !== null) {
+                return $s;
+            }
+            else {
+                $s = BxDolService::callSerialized($aMenuItem['addon'], $this->_aMarkers);
+                $oCache->setData($sKey, $s);
+            }
+
+            return $s;
+        }
+        else {
+            return BxDolService::callSerialized($aMenuItem['addon'], $this->_aMarkers);
+        }
+    }
+
+    protected function _getMenuMarkers ($aMenuItem)
+    {
+        if (empty($aMenuItem['markers']))
+            return '';
+
+        return BxDolService::callSerialized($aMenuItem['markers'], $this->_aMarkers);
+    }
+
+    protected function _getMenuAttrs ($aMenuItem)
+    {
+        $sAttrs = '';
+        if(!empty($aMenuItem['target']))
+            $sAttrs .= ' target="' . $aMenuItem['target'] . '"';
+
+        if($this->_bAddNoFollow && !empty($aMenuItem['link']) && preg_match('@^https?://@', $aMenuItem['link']) && strncmp($aMenuItem['link'], BX_DOL_URL_ROOT, strlen(BX_DOL_URL_ROOT)) !== 0)
+            $sAttrs .= ' rel="noreferrer"';
+
+        return $sAttrs;
+    }
+
+    /**
+     * Add css/js files which are needed for menu display and functionality.
+     */
+    protected function _addJsCss()
+    {
+        $this->_oTemplate->addCss('menu.css');
+    }
+
+    protected function _getTmplVarsAddon($mixedAddon, $aMenuItem) 
+    {
+        $sAddon = '';
+        if(!is_array($mixedAddon))
+            $sAddon = $mixedAddon;
+        else if(!empty($mixedAddon['addon']))
+            $sAddon = $mixedAddon['addon'];
+
+        return array(
+            'addon' => $sAddon,
+            'addonf' => ''
+        );
+    }
+}
+
+/** @} */

+ 155 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseMenuCustom.php

@@ -0,0 +1,155 @@
+<?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 BxBaseMenuCustom extends BxTemplMenuMoreAuto
+{
+    protected $_sTmplContent;
+    protected $_sTmplContentItem;
+
+    public function __construct($aObject, $oTemplate = false)
+    {
+        parent::__construct($aObject, $oTemplate);
+
+        $this->_sTmplContent = $this->_oTemplate->getHtml($this->getTemplateName());
+        $this->_sTmplContentItem = $this->_oTemplate->getHtml($this->getTemplateNameItem());
+
+        $this->_sTmplNameItemMore = 'menu_custom_item_more.html';
+    }
+
+    /**
+     * Get template name with checking for custom template related to exactly this menu object.
+     * @return string with template name.
+     */
+    public function getTemplateNameItem($sName = '')
+    {
+        if(empty($sName))
+            $sName = 'menu_custom_item.html';
+
+        $sNameCustom = str_replace('.html', '_' . $this->_sObject . '.html', $sName);
+        return $this->_oTemplate->isHtml($sNameCustom) ? $sNameCustom : $sName;
+    }
+
+    public function setTemplateById ($iTemplateId)
+    {
+        $sTemplate = $this->_aObject['template'];
+
+        switch($iTemplateId) {
+            case BX_DB_MENU_TEMPLATE_TABS:
+                $iTemplateId = BX_MENU_TEMPLATE_CUSTOM_HOR;
+                break;
+
+            case BX_DB_MENU_TEMPLATE_POPUP:
+                $iTemplateId = BX_MENU_TEMPLATE_CUSTOM_VER;
+                break;
+        }
+
+        parent::setTemplateById ($iTemplateId);
+
+        if($sTemplate != $this->_aObject['template'])
+            $this->_sTmplContent = $this->_oTemplate->getHtml($this->getTemplateName());
+    }
+
+    protected function _getCode($sTmplName, $aTmplVars)
+    {
+        if($sTmplName != $this->_aObject['template'])
+            return parent::_getCode($sTmplName, $aTmplVars);
+
+        return $this->_oTemplate->parseHtmlByContent($this->_sTmplContent, $aTmplVars);
+    }
+
+    protected function _getMenuItem ($aItem)
+    {
+    	if (isset($aItem['active']) && !$aItem['active'])
+            return false;
+
+        if (!$this->_isVisible($aItem))
+            return false;
+
+    	$sMethod = '_getMenuItem' . str_replace(' ', '', ucwords(str_replace('-', ' ', $aItem['name'])));
+        $bMethod = method_exists($this, $sMethod);
+
+        $mixedItem = false;
+        if($bMethod)
+            $mixedItem = $this->$sMethod($aItem);
+
+    	if($mixedItem === true || !$bMethod) {
+            $aItem = parent::_getMenuItem($aItem);
+            if($aItem === false)
+                return false;
+
+            if($this->_bIsApi)
+                return $aItem;
+
+            $mixedItem = $this->_getMenuItemDefault($aItem);
+    	}
+
+    	if(empty($mixedItem))
+            return false;
+
+        if($this->_bIsApi)
+            return $mixedItem;
+
+        $sItem = $sClass = '';
+        if(is_array($mixedItem)) 
+            list($sItem, $sClass) = $mixedItem;
+        else
+            $sItem = $mixedItem;
+
+        if(!empty($sClass))
+            $sClass = ' ' . $sClass;
+        if($this->_isSelected($aItem))
+            $sClass .= ' bx-menu-tab-active';
+
+        $sClass .= $this->_getVisibilityClass($aItem);
+
+        return [
+            'name' => $aItem['name'],
+            'class' => $sClass,
+            'item' => $sItem
+        ];
+    }
+
+    protected function _getMenuItemMoreAuto ($aItem)
+    {
+        $aItem['onclick'] = $this->_sJsObjectMoreAuto . '.more(this);';
+
+        $aItem = parent::_getMenuItem($aItem);
+        if($aItem === false)
+            return false;
+
+        if($this->_bIsApi)
+            return $aItem;
+
+        return $this->_oTemplate->parseHtmlByContent($this->_getTmplContentItemMore(), array(
+            'item' => $this->_getMenuItemDefault($aItem),
+            'popup' => BxTemplFunctions::getInstance()->transBox($this->_aHtmlIds['more_auto_popup'], $this->_oTemplate->parseHtmlByContent($this->_getTmplContentItemMorePopup(), array(
+                'content' => ''
+            )), true)
+        ));
+    }
+
+    protected function _getMenuItemDefault ($aItem)
+    {
+        if(!isset($aItem['class_wrp']))
+            $aItem['class_wrp'] = '';
+
+        if(!isset($aItem['class_link']))
+            $aItem['class_link'] = '';
+
+        return $this->_oTemplate->parseHtmlByContent($this->_getTmplContentItem(), $aItem);
+    }
+
+    protected function _getTmplContentItem()
+    {
+        return $this->_sTmplContentItem;
+    }
+}
+
+/** @} */

+ 126 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseMenuProfileStats.php

@@ -0,0 +1,126 @@
+<?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
+ * @{
+ */
+
+/**
+ * Menu representation.
+ * @see BxDolMenu
+ */
+class BxBaseMenuProfileStats extends BxTemplMenuAccountNotifications
+{
+    protected $_iMenuItemsMin;
+
+    public function __construct ($aObject, $oTemplate)
+    {
+        parent::__construct ($aObject, $oTemplate);
+
+        $this->_bDisplayAddons = true;
+        $this->_iMenuItemsMin = 10;
+    }
+
+    public function getMenuItems ()
+    {
+        $aItems = parent::getMenuItems();
+        if(empty($aItems) || !is_array($aItems) || $this->_bIsApi)
+            return $aItems;
+
+        $iMaxNum = count($aItems);
+        if($iMaxNum <= $this->_iMenuItemsMin)
+            return $aItems;
+
+        $mixedCollpsed = $this->getUserChoiceCollapsed();
+        $bCollpsed = $mixedCollpsed === false || $mixedCollpsed == 1;
+
+        for($i = $this->_iMenuItemsMin; $i < $iMaxNum; $i++)
+            $aItems[$i]['class_add'] .= ' bx-mi-aux' . ($bCollpsed ? ' bx-mi-hidden' : '');
+
+        $aShowMoreLinks = [
+            'more' => ['title' => '_sys_show_more', 'icon' => 'chevron-down', 'class' => $bCollpsed ? '' : 'bx-mi-hidden'],
+            'less' => ['title' => '_sys_show_less', 'icon' => 'chevron-up', 'class' => !$bCollpsed ? '' : 'bx-mi-hidden']
+        ];
+
+        foreach($aShowMoreLinks as $sLink => $aLink)
+            $aItems[] = array(
+                'class_add' => 'bx-psmi-show-' . $sLink . ' ' . $aLink['class'],
+                'name' => 'show-' . $sLink,
+                'title' => _t($aLink['title']),
+                'link' => 'javascript:void(0)',
+                'onclick' => 'bx_menu_show_more_less(this, \'' . $this->_sObject . '\', \'.bx-menu-object-' . $this->_sObject . '\')',
+                'attrs' => '',
+                'bx_if:image' => [
+                    'condition' => false,
+                    'content' => ['icon_url' => ''],
+                ],
+                'bx_if:image_inline' => [
+                    'condition' => false,
+                    'content' => ['image' => ''],
+                ],
+                'bx_if:icon' => [
+                    'condition' => true,
+                    'content' => ['icon' => $aLink['icon']],
+                ],
+                'bx_if:icon-html' => [
+                    'condition' => false,
+                    'content' => ['icon-a' => ''],
+                ],
+                'bx_if:icon-a' => [
+                    'condition' => false,
+                    'content' => ['icon-a' => ''],
+                ],
+                'bx_if:addon' => [
+                    'condition' => false,
+                    'content' => []
+                ]
+            );
+
+        return $aItems;
+    }
+
+    protected function getMenuItemsRaw ()
+    {
+        $aItems = $this->_oQuery->getMenuItemsBy(array(
+            'type' => 'set_name', 
+            'set_name' => $this->_aObject['set_name']
+        ));
+
+        $aDuplicates = $this->_oQuery->getMenuItemsBy(array(
+            'type' => 'set_name_duplicates', 
+            'set_name' => $this->_aObject['set_name']
+        ));
+
+        $oProfile = BxDolProfile::getInstance();
+        if(!$oProfile)
+            return array();
+
+        $sModule = $oProfile->getModule();
+
+        $aResult = array();
+        foreach($aItems as $aItem) {
+            if(in_array($aItem['name'], $aDuplicates) && $aItem['module'] != $sModule)
+                continue;
+            
+            $aResult[$aItem['name']] = $aItem;
+        }
+
+        return $aResult;
+    }
+    
+    protected function _getMenuItem($a)
+    {
+        $mixedResult = parent::_getMenuItem($a);
+
+        if($mixedResult !== false && !empty($mixedResult['link']) && strpos($mixedResult['link'], 'javascript:') === false)
+            $mixedResult['link'] = bx_append_url_params($mixedResult['link'], [
+                'owner' => 1
+            ]);
+
+        return $mixedResult;
+    }
+}
+
+/** @} */

+ 221 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseMenuSubmenu.php

@@ -0,0 +1,221 @@
+<?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
+ * @{
+ */
+
+/**
+ * Menu representation.
+ * @see BxDolMenu
+ */
+class BxBaseMenuSubmenu extends BxTemplMenu
+{
+    protected $_aSocialSharingService = false;
+    protected $_sObjectActionsMenu = false;
+    protected $_sObjectSubmenu = false;
+    protected $_oObjectSubmenu = null;
+    protected $_mixedMainMenuItemSelected = false;
+
+    protected $_sJsObject;
+
+    protected $_iContentId;
+
+    public function __construct ($aObject, $oTemplate)
+    {
+        parent::__construct ($aObject, $oTemplate);
+
+        $this->_sJsObject = 'o' . bx_gen_method_name($this->_sObject);
+
+        $this->_iContentId = 0;
+    }
+
+    public function isVisible()
+    {
+        if((int)$this->_aObject['active'] == 0)
+            return false;
+
+        $aMenuItemSelected = $this->getSelectedMenuItem();
+        if(isset($aMenuItemSelected['set_name']) && $aMenuItemSelected['set_name'] == 'sys_site' && $aMenuItemSelected['name'] == 'home')
+            return false;
+
+        if(!$this->_oObjectSubmenu || !$this->_oObjectSubmenu->isVisible())
+            return false;
+        
+        return true;
+    }
+
+    public function setContentId($iContentId)
+    {
+        $this->_iContentId = $iContentId;
+    }
+
+    /**
+     * Get current menu in submenu
+     * @return menu object name
+     */
+    public function getObjectSubmenu()
+    {        
+        return $this->_sObjectSubmenu;
+    }
+    
+    /**
+     * Set current menu object in submenu
+     * @param $sMenuObject menu object name
+     * @param $sForceMainMenuSelection force main menu item selection by menu item name
+     */
+    public function setObjectSubmenu ($sMenuObject, $sForceMainMenuSelection = false)
+    {
+        $this->_sObjectSubmenu = $sMenuObject;
+        $this->_oObjectSubmenu = BxDolMenu::getObjectInstance($this->_sObjectSubmenu);
+
+        $this->_mixedMainMenuItemSelected = $sForceMainMenuSelection;
+    }
+
+    /**
+     * Set current actions menu in submenu
+     * @param $sActionsMenuObject menu object name
+     */
+    public function setObjectActionsMenu ($sActionsMenuObject)
+    {
+        $this->_sObjectActionsMenu = $sActionsMenuObject;
+    }
+
+    /**
+     * Get current actions menu in submenu
+     * @return menu object name
+     */
+    public function getObjectActionsMenu()
+    {        
+        return $this->_sObjectActionsMenu;
+    }
+
+    /**
+     * Set social sharing menu in submenu
+     * @param $a menu service call array (module, method, etc)
+     */
+    public function setServiceSocialSharing ($a)
+    {
+        $this->_aSocialSharingService = $a;
+    }
+
+    /**
+     * Get social sharing menu in submenu
+     * @return menu service call array (module, method, etc)
+     */
+    public function getServiceSocialSharing ()
+    {
+        return $this->_aSocialSharingService;
+    }
+
+    /**
+     * Get class
+     * @return class
+     */
+    public function getClass ()
+    {
+        return $this->isVisible() && $this->_oObjectSubmenu->getTemplateId() == BX_MENU_TEMPLATE_SUBMENU_MORE_AUTO ? 'bx-menu-more-auto' : '';
+    }
+
+    /**
+     * Get menu code.
+     * @return string
+     */
+    public function getCode ()
+    {
+        if(!$this->isVisible())
+            return '';
+
+        if(!empty($this->_iContentId) && method_exists($this->_oObjectSubmenu, 'setContentId'))
+            $this->_oObjectSubmenu->setContentId($this->_iContentId);
+
+        $this->_addJsCss();
+        return $this->_oObjectSubmenu->getCode();
+    }
+    
+    public function getCodeAPI ()
+    {
+        if(!$this->isVisible())
+            return [];
+
+        if(!empty($this->_iContentId) && method_exists($this->_oObjectSubmenu, 'setContentId'))
+            $this->_oObjectSubmenu->setContentId($this->_iContentId);
+
+        return $this->_oObjectSubmenu->getCodeAPI();
+    }
+
+    public function getPageCoverParams ()
+    {
+        $aMenuItemSelected = $this->getSelectedMenuItem ();
+        if (!$aMenuItemSelected)
+            return '';
+
+        $oMenuActions = BxDolMenu::getObjectInstance($this->_sObjectActionsMenu);
+
+        $bImageInline = strpos($aMenuItemSelected['icon'], '&lt;svg') !== false || strpos($aMenuItemSelected['icon'], '<svg') !== false;
+
+        $bVarsImage = !$bImageInline && strpos($aMenuItemSelected['icon'], '.') !== false;
+        $aVarsImage = [];
+        if($bVarsImage)
+            $aVarsImage = [
+                'icon_url' => $this->_oTemplate->getIconUrl($aMenuItemSelected['icon'])
+            ];
+
+        $aVars = array (
+            'object' => $this->_sObject,
+            'title' => bx_process_output($aMenuItemSelected['title']),
+            'link' => BxDolPermalinks::getInstance()->permalink($aMenuItemSelected['link']),
+            'actions' => $oMenuActions ? $oMenuActions->getCode() : '',
+            'bx_if:image' => array (
+                'condition' => $bVarsImage,
+                'content' => $aVarsImage,
+            ),
+            'bx_if:icon' => array (
+                'condition' => !$bVarsImage && !empty($aMenuItemSelected['icon']),
+                'content' => array(
+                    'icon' => $aMenuItemSelected['icon']
+                ),
+            ),
+        );
+
+        return $aVars;
+    }
+
+    public function getSelectedMenuItem ()
+    {
+        if (is_array($this->_mixedMainMenuItemSelected))
+            return $this->_mixedMainMenuItemSelected;
+
+        if (!isset($this->_aObject['menu_items']))
+            $this->_aObject['menu_items'] = $this->_oQuery->getMenuItems();
+
+        foreach ($this->_aObject['menu_items'] as $a) {
+
+            if (isset($a['active']) && !$a['active'])
+                continue;
+
+            if (isset($a['visible_for_levels']) && !$this->_isVisible($a))
+                continue;
+
+            $isSelected = false;
+
+            if ($this->_mixedMainMenuItemSelected)
+                $isSelected = $this->_mixedMainMenuItemSelected == $a['name'];
+            else
+                $isSelected = $this->_isSelected($a) || ($this->_sObjectSubmenu && $this->_sObjectSubmenu == $a['submenu_object']);
+
+            if (!$isSelected)
+                continue;
+
+            $a = $this->_replaceMarkers($a);
+            $a['title'] = _t($a['title']);
+            return $a;
+        }
+
+        return false;
+    }
+}
+
+/** @} */

+ 167 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseMenuTagsCloud.php

@@ -0,0 +1,167 @@
+<?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
+ * @{
+ */
+
+/**
+ * Menu representation.
+ * @see BxDolMenu
+ */
+class BxBaseMenuTagsCloud extends BxTemplMenu
+{
+    protected $_iMenuItemsMin;
+    protected $_aItems;
+
+    public function __construct ($aObject, $oTemplate)
+    {
+        parent::__construct ($aObject, $oTemplate);
+        
+        $this->_bDisplayAddons = true;
+        $this->_iMenuItemsMin = 10;
+    }
+    
+    public function setKeywords($aKeywords, $oMetaObject, $mixedSection)
+    {
+        foreach ($aKeywords as $sKeyword => $iCount) {    
+            $aItems[] =  [
+                'class_add' => 'bx-psmi-show-' . $sKeyword . ' ',
+                'name' => 'show-' . $sKeyword,
+                'title' => htmlspecialchars_adv($sKeyword),
+                'link' => $oMetaObject->keywordsGetHashTagUrl($sKeyword, 0, $mixedSection),
+                'onclick' => 'javascript:',
+                'attrs' => '',
+                'bx_if:image' => array (
+                    'condition' => false,
+                    'content' => [],
+                ),
+                'bx_if:image_inline' => array (
+                    'condition' => false,
+                    'content' => [],
+                ),
+                'bx_if:icon' => array (
+                    'condition' => true,
+                    'content' => array('icon' => 'hashtag'),
+                ),
+                'bx_if:icon-a' => array (
+                    'condition' => false,
+                    'content' => [],
+                ),
+                'bx_if:icon-html' => array (
+                    'condition' => false,
+                    'content' => [],
+                ),
+                'bx_if:addon' => [
+                    'condition' => true,
+                    'content' => ['addon' => $iCount]
+                ]
+            ];
+        }
+        
+        $this->_aItems = $aItems;
+    }
+    
+    public function getMenuItems ()
+    {
+        $aItems = $this->_aItems;
+        if(empty($aItems) || !is_array($aItems))
+            return $aItems;
+
+        $iMaxNum = count($aItems);
+        if($iMaxNum <= $this->_iMenuItemsMin)
+            return $aItems;
+
+        $mixedCollpsed = $this->getUserChoiceCollapsed();
+        $bCollpsed = $mixedCollpsed === false || $mixedCollpsed == 1;
+
+        for($i = $this->_iMenuItemsMin; $i < $iMaxNum; $i++)
+            $aItems[$i]['class_add'] .= ' bx-mi-aux' . ($bCollpsed ? ' bx-mi-hidden' : '');
+
+        $aShowMoreLinks = [
+            'more' => ['title' => '_sys_show_more', 'icon' => 'chevron-down', 'class' => $bCollpsed ? '' : 'bx-mi-hidden'],
+            'less' => ['title' => '_sys_show_less', 'icon' => 'chevron-up', 'class' => !$bCollpsed ? '' : 'bx-mi-hidden']
+        ];
+
+        foreach($aShowMoreLinks as $sLink => $aLink)
+            $aItems[] = array(
+                'class_add' => 'bx-psmi-show-' . $sLink . ' ' . $aLink['class'],
+                'name' => 'show-' . $sLink,
+                'title' => _t($aLink['title']),
+                'link' => 'javascript:void(0)',
+                'onclick' => 'bx_menu_show_more_less(this, \'' . $this->_sObject . '\', \'.bx-menu-object-' . $this->_sObject . '\')',
+                'attrs' => '',
+                'bx_if:image' => [
+                    'condition' => false,
+                    'content' => ['icon_url' => ''],
+                ],
+                'bx_if:image_inline' => [
+                    'condition' => false,
+                    'content' => ['image' => ''],
+                ],
+                'bx_if:icon' => [
+                    'condition' => true,
+                    'content' => ['icon' => $aLink['icon']],
+                ],
+                'bx_if:icon-html' => [
+                    'condition' => false,
+                    'content' => ['icon-a' => ''],
+                ],
+                'bx_if:icon-a' => [
+                    'condition' => false,
+                    'content' => ['icon-a' => ''],
+                ],
+                'bx_if:addon' => [
+                    'condition' => false,
+                    'content' => []
+                ]
+            );
+
+        return $aItems;
+    }
+
+    protected function getMenuItemsRaw ()
+    {
+        $aItems = $this->_oQuery->getMenuItemsBy(array(
+            'type' => 'set_name', 
+            'set_name' => $this->_aObject['set_name']
+        ));
+
+        $aDuplicates = $this->_oQuery->getMenuItemsBy(array(
+            'type' => 'set_name_duplicates', 
+            'set_name' => $this->_aObject['set_name']
+        ));
+
+        $oProfile = BxDolProfile::getInstance();
+        if(!$oProfile)
+            return array();
+
+        $sModule = $oProfile->getModule();
+
+        $aResult = array();
+        foreach($aItems as $aItem) {
+            if(in_array($aItem['name'], $aDuplicates) && $aItem['module'] != $sModule)
+                continue;
+            
+            $aResult[$aItem['name']] = $aItem;
+        }
+
+        return $aResult;
+    }
+    
+    protected function _getMenuItem($a)
+    {
+        $mixedResult = parent::_getMenuItem($a);
+
+        if($mixedResult !== false && !empty($mixedResult['link']) && strpos($mixedResult['link'], 'javascript:') === false)
+            $mixedResult['link'] = bx_append_url_params($mixedResult['link'], [
+                'owner' => 1
+            ]);
+
+        return $mixedResult;
+    }
+}
+
+/** @} */

+ 1059 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBasePage.php

@@ -0,0 +1,1059 @@
+<?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 $_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-',
+        );
+    }
+
+    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 (!$this->_isVisiblePage($this->_aObject))
+            return $this->_getPageAccessDeniedMsg ();
+
+        $this->_addJsCss();
+
+        if (!$this->_bSubPage) {
+            $this->_addSysTemplateVars();
+
+            $this->_selectMenu();
+
+            $this->_setSubmenu(array());
+        }
+
+        if (!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()
+    {
+        $oMenuSubmenu = BxDolMenu::getObjectInstance('sys_site_submenu');
+        if($oMenuSubmenu) {
+            $aParams = $oMenuSubmenu->getPageCoverParams();
+            if(!empty($aParams) && is_array($aParams))
+                return $aParams;
+        }
+
+    	return array (
+            'title' => $this->_getPageTitle(),
+            'actions' => '',
+            'bx_if:image' => array (
+                'condition' => false,
+                'content' => array(),
+            ),
+            'bx_if:icon' => array (
+                'condition' => false,
+                'content' => array(),
+            ),
+        );
+    }
+
+    /**
+     * 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 ()
+    {
+        $a = [
+            'id' => $this->_aObject['id'],
+            'title' => $this->_getPageTitle(),
+            'uri' => $this->_aObject['uri'],
+            'author' => $this->_aObject['author'],
+            'added' => $this->_aObject['added'],
+            'module' => $this->getModule(),
+            'type' => $this->getType (),
+            'layout' => $this->_aObject['layout_id'],
+            'cover_block' => '',
+            'menu_top' => '',
+            'menu' => '',
+            'menu_bottom' => '',
+            'elements' => $this->getPageBlocksAPI(),
+        ];
+        
+        
+        if (BxDolCover::getInstance($this)->isCover() && isset($this->_aProfileInfo)){
+            
+            $oModule = BxDolModule::getInstance($this->getModule());
+            
+            $a['cover_block'] = [
+                'profile' => BxDolProfile::getInstance()->getData($this->_aProfileInfo['id']),
+                'actions_menu' => '',
+                'meta_menu' => '',
+                'cover' => $oModule->serviceGetCover($this->_aProfileInfo['content_id'])
+            ];
+            
+            $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();
+            }
+            
+            if(!empty($CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY'])){
+                $oActionMenu = BxTemplMenu::getObjectInstance($CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY']);
+                $a['cover_block']['actions_menu'] = $oActionMenu->getCodeAPI();
+            }  
+        }
+        
+        if (($oMenuTop = BxDolMenu::getObjectInstance('sys_site')) !== 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()) {
+            $o = BxDolProfile::getInstance();
+            $a['user'] = [
+                'id' => $o->id(),
+                'display_name' => $o->getDisplayName(),
+                'url' => $o->getUrl(),
+                'avatar' => $o->getAvatar(),
+                'info' => $o->getInfo(),
+                'active' => $o->isActive(),
+                'status' => $o->getStatus(),
+            ];
+        }
+
+        return $a;
+    }
+
+    public function getPageBlocksAPI()
+    {
+        $aFieldsUnset = ['cell_id', 'active', 'copyable', 'deletable', 'object', 'text', 'text_updated', 'title_system', 'visible_for_levels'];
+
+        $aCells = $this->_oQuery->getPageBlocks();
+        foreach($aCells as $sKey => &$aCell) {
+            foreach($aCell as $i => $aBlock) {     
+                if(!$this->_isVisibleBlock($aBlock) || ($aCells[$sKey][$i]['hidden_on'] > 0 && ((int)$aCells[$sKey][$i]['hidden_on'] & 8) > 0)) {
+                    unset($aCells[$sKey][$i]);
+                    continue;
+                }
+
+                $this->processPageBlock($aCells[$sKey][$i], true);
+                $aBlock = $aCells[$sKey][$i];
+
+                $sFunc = '_getBlock' . ucfirst($aBlock['type']);
+                $mBlock = method_exists($this, $sFunc) ? $this->$sFunc($aBlock) : $aBlock['content'];
+
+                $aCells[$sKey][$i]['title'] = isset($mBlock['title']) ? $mBlock['title'] : $this->getBlockTitle($aBlock);
+                $aCells[$sKey][$i]['content'] = isset($mBlock['content']) ? $mBlock['content'] : $mBlock;
+                $aCells[$sKey][$i]['menu'] = isset($mBlock['menu']) ? $mBlock['menu'] : '';
+
+                $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 (!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);
+
+            $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);
+
+        $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)
+    {
+        $s = '<div class="bx-page-raw-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 'custom' block type.
+     * @return string
+     */
+    protected function _getBlockCustom ($aBlock)
+    {
+        $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)
+    {
+        $s = '<div class="bx-page-html-container">' . $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)
+    {
+        $s = '<div class="bx-page-lang-container">' . _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)
+    {
+        return BxDolService::callSerialized($aBlock['content'], $this->_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 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 access denied message HTML.
+     * @return string
+     */
+    protected function _getPageAccessDeniedMsg ()
+    {
+        return MsgBox(_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;
+    }
+}
+
+/** @} */

+ 349 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseSearchExtended.php

@@ -0,0 +1,349 @@
+<?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
+ * @{
+ */
+
+/**
+ * Extended Search objects representation.
+ * 
+ * @see BxDolSearchExtended
+ */
+class BxBaseSearchExtended extends BxDolSearchExtended
+{
+    protected $_sFormClassName;
+    protected $_sFormClassPath;
+
+    protected $_oForm;
+    protected $_oTemplate;
+
+    protected $_bJsMode;
+
+    protected $_iAgeMin;
+    protected $_iAgeMax;
+
+    public function __construct ($aObject, $oTemplate = null)
+    {
+        parent::__construct ($aObject);
+
+        if ($oTemplate)
+            $this->_oTemplate = $oTemplate;
+        else if(!empty($this->_aObject['module']))
+            $this->_oTemplate = BxDolModule::getInstance($this->_aObject['module'])->_oTemplate;
+        else 
+            $this->_oTemplate = BxDolTemplate::getInstance();
+
+        $this->_sFormClassName = '';
+        $this->_sFormClassPath = '';
+
+        $this->_oForm = null;
+
+        $this->_bJsMode = false;
+
+        $this->_iAgeMin = 1;
+        $this->_iAgeMax = 75;
+    }
+
+    public function getForm($aParams = array())
+    {
+        if(!$this->isEnabled())
+            return '';
+
+        $oForm = $this->prepareForm($aParams);
+        return $oForm->getCode();
+    }
+
+    /**
+     * Get search results from search form or from custom condition
+     *
+     * @param $aParams['cond'] custom condition to pass instead of form submission, 
+     *        conditions are key&value pair, where 'key' is form input name and 
+     *        'value' is the term to search for
+     * @param $aParams['start'] position of first record to display 
+     * @param $aParams['per_page'] number of items per page
+     * @param $aParams['template'] custom unit templates to use
+     * @return HTML string with search results
+     */ 
+    public function getResults($aParams = array())
+    {
+        if(!$this->isEnabled())
+            return '';
+
+        $bJsMode = isset($aParams['js_mode']) ? (bool)$aParams['js_mode'] : $this->_bJsMode;
+        $bCondition = !empty($aParams['cond']) && is_array($aParams['cond']);
+
+        $iStart = !empty($aParams['start']) ? $aParams['start'] : 0;
+        $iPerPage = !empty($aParams['per_page']) ? $aParams['per_page'] : 0;
+        unset($aParams['start'], $aParams['per_page']);
+
+        $sUnitTemplate = !empty($aParams['template']) ? $aParams['template'] : '';
+
+        $oForm = $this->prepareForm($aParams);
+        if($bCondition) {
+            $mixedSubmitName = $oForm->aParams['db']['submit_name'];
+            if(is_array($mixedSubmitName))
+                $mixedSubmitName = array_pop($mixedSubmitName);
+            $aParams['cond'][$mixedSubmitName] = 1;
+
+            $oForm->aFormAttrs['method'] = BX_DOL_FORM_METHOD_SPECIFIC;
+            $oForm->aParams['csrf']['disable'] = true;
+
+            $oForm->initChecker(array(), $aParams['cond']);
+        }
+
+        if(!$oForm->isSubmittedAndValid() && !$this->_bFilterMode)
+            return '';
+
+        $oContentInfo = BxDolContentInfo::getObjectInstance($this->_aObject['object_content_info']);
+        
+        if(!$oContentInfo)
+            return '';
+
+        $aParamsSearch = array();
+        foreach($this->_aObject['fields'] as $aField) {
+            $mixedValue = $oForm->getCleanValue($aField['name']);
+            if(empty($mixedValue) || (is_array($mixedValue) && bx_is_empty_array($mixedValue)))
+                continue;
+
+            $aParamsSearch[$aField['name']] = array(
+                'type' => $aField['search_type'],
+                'value' => $mixedValue,
+                'operator' => $aField['search_operator']
+            );
+
+            if(!$bCondition) {
+                if(!isset($aParams['cond']))
+                    $aParams['cond'] = array();
+
+                switch($oForm->aInputs[$aField['name']]['type']) {
+                    case 'location':
+                    case 'location_radius':
+                        $aParams['cond'][$aField['name']] = $mixedValue['string'];
+
+                        $aLocationComponents = BxDolMetatags::locationsParseComponents($mixedValue['array'], $aField['name']);
+                        if($oForm->aInputs[$aField['name']]['type'] == 'location_radius' && count($mixedValue['array']) > count($aLocationComponents))
+                            $aLocationComponents[$aField['name'] . '_rad'] = array_pop($mixedValue['array']);
+
+                        $aParams['cond'] = array_merge($aParams['cond'], $aLocationComponents);
+                        break;
+
+                    default:
+                        $aParams['cond'][$aField['name']] = $mixedValue;
+                }
+            }
+        }
+
+        if((empty($aParamsSearch) || !is_array($aParamsSearch)) && !$this->_bFilterMode)
+            return '';
+
+        if (!$iPerPage) {
+            bx_import('BxDolSearch');
+            $iPerPage = BX_DOL_SEARCH_RESULTS_PER_PAGE_DEFAULT;
+        }
+
+        $aResults = false;
+        
+        if (bx_get('sort')){
+            $aTmp = explode(':', bx_get('sort'));
+            $aParamsSearch['order'] = [['field' => $aTmp[0], 'direction' => $aTmp[1]]];
+        }
+        
+        bx_alert('search', 'get_data', 0, false, array('object' => $this->_aObject, 'search_params' => &$aParamsSearch, 'search_results' => &$aResults));
+
+    	if($aResults === false)
+    	    $aResults = $oContentInfo->getSearchResultExtended($aParamsSearch, $iStart, $iPerPage + 1, $this->_bFilterMode);
+
+    	if(empty($aResults) || !is_array($aResults))
+    	    return '';
+
+        if(!empty($aParams['cond']) && is_array($aParams['cond']))
+            $aParams['cond'] = self::encodeConditions($aParams['cond']);
+
+        $aPaginate = array('start' => $iStart, 'per_page' => $iPerPage);
+        if(!$bJsMode) {
+            $aParams['start'] = '{start}';
+            $aParams['per_page'] = '{per_page}';
+            list($sPageLink, $aPageParams) = bx_get_base_url_inline($aParams);
+
+            $aPaginate['page_url'] = BxDolPermalinks::getInstance()->permalink(bx_append_url_params($sPageLink, $aPageParams, true, ['{start}', '{per_page}']));
+        }
+        else
+            $aPaginate['on_change_page'] = "return !loadDynamicBlockAutoPaginate(this, '{start}', '{per_page}', " . bx_js_string(json_encode($aParams)) . ");";            
+
+        $oPaginate = new BxTemplPaginate($aPaginate);
+        $oPaginate->setNumFromDataArray($aResults);
+
+        $bTmplVarsPaginate = $iStart || $oPaginate->getNum() > $iPerPage;
+        $aTmplVarsPaginate = $bTmplVarsPaginate ? array('paginate' => $oPaginate->getSimplePaginate()) : array();
+
+    	$sResults = '';
+    	foreach($aResults as $iId)
+    	    $sResults .= $oContentInfo->getContentSearchResultUnit($iId, $sUnitTemplate); 
+
+        $aTxtDirection = [
+            'asc' => _t('_order_asc'),
+            'desc' => _t('_order_desc')
+        ];
+
+        $sSort = '';
+        if ($sResults != ''){
+
+            $aData = $this->_aObject['sortable_fields'];
+            $aValues = [];
+            foreach($aData as $aField) {
+                if ($aField['active'] == 0)
+                    continue;
+
+                $sLangKey = $aField['caption'] . '_' . $aField['direction'];
+                $sLangVal = _t($sLangKey);
+
+                $aValues[$aField['name'] . ':' . $aField['direction']] = strcmp($sLangKey, $sLangVal) != 0 ? $sLangVal : _t($aField['caption']) . ' ' . $aTxtDirection[$aField['direction']];
+            }
+            
+            if (!empty($aValues)){
+                $aValues = array_merge(['' => _t('_sys_txt_search_sort_by_default')], $aValues);
+            }
+            $oForm = new BxTemplFormView(array());
+            
+            $sOnChange = '';
+            if(!$bJsMode) {
+                unset($aParams['start']);
+                unset($aParams['per_page']);
+                list($sPageLink, $aPageParams) = bx_get_base_url_inline($aParams);
+                $sOnChange = "bx_search_extnded_sort(this,'" . BxDolPermalinks::getInstance()->permalink(bx_append_url_params($sPageLink, $aPageParams)) . "')";
+            }
+            else{
+                $sOnChange = "return !loadDynamicBlockAutoSort(this, $(this).val()," . bx_js_string(json_encode($aParams)) . ");";
+            }
+            
+            $sSort = '';
+            if (!empty($aValues)){
+                $aInputSort = array(
+                    'type' => 'select',
+                    'name' => 'sort',
+                    'value' =>  bx_get('sort') ? bx_get('sort') : '',
+                    'values' => $aValues,
+                    'caption' => _t('_sys_txt_search_sort_by'),
+                    'attrs' => array(
+                        'onChange' => $sOnChange,
+                    ),
+                    'tr_attrs' => array(
+                        'class' => 'sort'
+                    )
+                );
+                $sSort = $oForm->genRow($aInputSort);
+            }
+            
+        }
+
+        return $this->_oTemplate->parseHtmlByName('search_extended_results.html', array(
+            'sort' => $sSort,
+            'code' => $sResults,
+            'bx_if:show_paginate' => array(
+                'condition' => $bTmplVarsPaginate,
+                'content' => $aTmplVarsPaginate
+            )
+        ));
+    }
+
+    protected function &prepareForm($aParams = array())
+    {
+        if(!empty($this->_oForm) && $this->_oForm instanceof BxDolForm)
+            return $this->_oForm;
+
+        $sForm = 'sys_search_extended_' . $this->_sObject;
+        $sFormSubmit = 'search' . $this->_sObject;
+
+        list($sPageLink, $aPageParams) = bx_get_base_url_inline();
+
+        $aForm = array(
+            'form_attrs' => array(
+                'id' => $sForm,
+                'name' => $sForm,
+                'action' => BxDolPermalinks::getInstance()->permalink(bx_append_url_params($sPageLink, array('i' => $aPageParams['i']))),
+                'method' => 'post'
+            ),
+            'params' => array(
+                'db' => array(
+                    'table' => '',
+                    'key' => '',
+                    'uri' => '',
+                    'uri_title' => '',
+                    'submit_name' => $sFormSubmit
+                ),
+            ),
+            'inputs' => array()
+        );
+
+        foreach ($this->_aObject['fields'] as $aField) {
+            if((int)$aField['active'] == 0)
+                continue;
+
+            if(in_array($aField['search_type'], array('checkbox_set', 'select_multiple'))) {
+                if(isset($aField['values']['']))
+                    unset($aField['values']['']);
+
+                if(isset($aField['values'][0]) && !is_array($aField['values'][0]))
+                    unset($aField['values'][0]);
+            }
+
+            $aAttrs = array();
+
+            if(in_array($aField['search_type'], array('datepicker_range_age')) && !empty($aField['search_value'])) {
+                $aFieldParams = BxDolService::callSerialized($aField['search_value']);
+                $iMin = isset($aFieldParams['min']) && is_numeric($aFieldParams['min']) ? $aFieldParams['min'] : $this->_iAgeMin;
+                $iMax = isset($aFieldParams['max']) && is_numeric($aFieldParams['max']) ? $aFieldParams['max'] : $this->_iAgeMax;
+
+                $aField['search_value'] = $iMin . '-' . $iMax;
+                $aAttrs = array('min' => $iMin, 'max' => $iMax, 'step' => 1);
+            }
+
+            if(in_array($aField['search_type'], array('datepicker_range')) && !empty($aField['search_value'])) {
+                $aField['search_value'] = '';
+            }
+
+            $aForm['inputs'][$aField['name']] = array(
+                'type' => $aField['search_type'],
+                'name' => $aField['name'],
+                'caption' => _t($aField['caption']),
+            	'info' => _t($aField['info']),
+                'values' => $aField['values'],
+                'value' => $aField['search_value'],
+                'attrs' => $aAttrs,
+                'db' => array(
+                    'pass' => !empty($aField['pass']) ? $aField['pass'] : 'Xss'
+                )
+            );
+
+            if(in_array($aField['search_type'], array('location', 'location_radius')))
+                $aForm['inputs'][$aField['name']]['manual_input'] = true;
+        }
+
+        $aForm['inputs']['search'] = array(
+            'type' => 'submit',
+            'name' => $sFormSubmit,
+            'value' => _t($this->_bFilterMode ? '_Apply' : '_Search')
+        );
+
+        $sClass = 'BxTemplSearchExtendedForm';
+        if(!empty($this->_sFormClassName)) {
+            $sClass = $this->_sFormClassName;
+            if(!empty($this->_sFormClassPath))
+                require_once(BX_DIRECTORY_PATH_ROOT . $this->_sFormClassPath);
+        }
+
+        $bJsMode = isset($aParams['js_mode']) ? (bool)$aParams['js_mode'] : $this->_bJsMode;
+        $bCondition = !empty($aParams['cond']) && is_array($aParams['cond']);
+        $aValues = !$bJsMode && $bCondition ? $aParams['cond'] : array();
+
+        $this->_oForm = new $sClass($aForm, $this->_oTemplate);
+        $this->_oForm->initChecker($aValues, $aValues);
+
+        return $this->_oForm;
+    }
+}
+
+/** @} */

+ 457 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseServiceConnections.php

@@ -0,0 +1,457 @@
+<?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];
+
+        $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)
+            return '';
+
+        $aResult = [];
+        switch($sObject) {
+            case 'sys_profiles_friends':
+                if($oConnection->isConnectedNotMutual($iInitiatorId, $iContentId))
+                    $aResult = [
+                        'add' => '',
+                        'remove' => '_sys_menu_item_title_sm_unfriend_cancel',
+                    ];
+                else if($oConnection->isConnectedNotMutual($iContentId, $iInitiatorId))
+                    $aResult = [
+                        'add' => '_sys_menu_item_title_sm_befriend_confirm',
+                        'remove' => '_sys_menu_item_title_sm_unfriend_reject',
+                    ];
+                else if($oConnection->isConnected($iInitiatorId, $iContentId, true))
+                    $aResult = [
+                        'add' => '',
+                        'remove' => '_sys_menu_item_title_sm_unfriend',
+                    ];
+                else
+                    $aResult = [
+                        'add' => '_sys_menu_item_title_sm_befriend',
+                        'remove' => '',
+                    ];
+                break;
+
+            case 'sys_profiles_subscriptions':
+                if($oConnection->isConnected($iInitiatorId, $iContentId))
+                    $aResult = [
+                        'add' => '',
+                        'remove' => '_sys_menu_item_title_sm_unsubscribe',
+                    ];
+                else
+                    $aResult = [
+                        'add' => '_sys_menu_item_title_sm_subscribe',
+                        'remove' => '',
+                    ];
+                break;
+        }
+
+        $aFlip = ['add' => 'remove', 'remove' => 'add'];
+        if($bFlip)
+            $sAction = $aFlip[$sAction];
+
+        return !empty($aResult[$sAction]) ? _t($aResult[$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;
+
+        $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);
+
+        $aProfile = BxDolProfile::getInstance($iProfileId)->getInfo();
+        if(empty($aProfile) || !is_array($aProfile))
+            return false;
+
+        $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);
+
+        $aProfile = BxDolProfile::getInstance($iProfileId)->getInfo();
+        if(empty($aProfile) || !is_array($aProfile))
+            return false;
+
+        $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']);
+    }
+}
+
+/** @} */

+ 345 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseServiceLogin.php

@@ -0,0 +1,345 @@
+<?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 login form functionality.
+ */
+class BxBaseServiceLogin extends BxDol
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-login Login
+     * @subsubsection bx_system_general-test test
+     * 
+     * @code bx_srv('system', 'test', [3], 'TemplServiceLogin'); @endcode
+     * @code {{~system:test:TemplServiceLogin[3]~}} @endcode
+     * 
+     * Test method which returns provided number multiplied by 2
+     * @param $n number 
+     * 
+     * @see BxBaseServiceLogin::serviceTest
+     */
+    /** 
+     * @ref bx_system_general-test "test"
+     * @api @ref bx_system_general-test "test"
+     */
+    public function serviceTest ($n = 1)
+    {
+        return $n*2;
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-login Login
+     * @subsubsection bx_system_general-member_auth_code member_auth_code
+     * 
+     * @code bx_srv('system', 'member_auth_code', [], 'TemplServiceLogin'); @endcode
+     * @code {{~system:member_auth_code:TemplServiceLogin~}} @endcode
+     * 
+     * Display external login buttons such as Facebook, Twitter, etc
+     * @param $aAuthTypes optional list of external login buttons
+     * 
+     * @see BxBaseServiceLogin::serviceMemberAuthCode
+     */
+    /** 
+     * @ref bx_system_general-member_auth_code "member_auth_code"
+     */
+    public function serviceMemberAuthCode($aAuthTypes = array())
+    {
+        if(empty($aAuthTypes) || !is_array($aAuthTypes))
+            $aAuthTypes = BxDolDb::getInstance()->fromCache('sys_objects_auths', 'getAll', 'SELECT * FROM `sys_objects_auths`');
+
+        $bCompact = getParam('site_login_social_compact') == 'on';
+
+        $aTmplButtons = array();
+        foreach($aAuthTypes as $iKey => $aItems) {
+            $sTitle = _t($aItems['Title']);
+
+            $aTmplButtons[] = array( 
+            	'class' => ($bCompact ? 'sys-auth-compact ' : '') . $aItems['Name'],
+                'href' => !empty($aItems['Link']) ? BX_DOL_URL_ROOT . $aItems['Link'] : 'javascript:void(0)',
+                'title_alt' => bx_html_attribute($sTitle),
+                'bx_if:show_onclick' => array(
+                    'condition' => !empty($aItems['OnClick']),
+                    'content' => array(
+                        'onclick' => 'javascript:' . $aItems['OnClick']
+                    )
+                ),
+                'bx_if:show_icon' => array(
+                    'condition' => !empty($aItems['Icon']),
+                    'content' => array(
+                        'icon' => $aItems['Icon']
+                    )
+                ),
+                'bx_if:show_title' => array(
+                    'condition' => !$bCompact || empty($aItems['Icon']),
+                    'content' => array(
+                		'title' => $sTitle
+                    )
+                )
+            );
+            if ($aItems['Style'] != ""){
+                $aStyles = unserialize($aItems['Style']);
+                foreach($aStyles as $sKey => $aValues) {
+                    BxDolTemplate::getInstance()->addCssStyle('.'. $aItems['Name'] .' ' . $sKey, $aValues);
+                }
+            }
+        }
+
+        if(empty($aTmplButtons) || !is_array($aTmplButtons))
+            return '';
+
+        $oTemplate = BxDolTemplate::getInstance();
+        $oTemplate->addCss(array('auth.css'));
+        return $oTemplate->parseHtmlByName('auth.html', array(
+            'bx_repeat:buttons' => $aTmplButtons,
+            'class_container' => ($bCompact ? 'sys-auth-compact-container' : '')
+        ));
+    }
+
+    /**
+     * Display login form without join text
+     * @see ::serviceLoginForm
+     */
+    public function serviceLoginFormOnly ($sParams = '', $sForceRelocate = '')
+    {
+    	if(strpos($sParams, 'no_join_text') === false)
+    		$sParams = ($sParams != '' ? ' ' : '') . 'no_join_text';
+
+    	return $this->serviceLoginForm($sParams, $sForceRelocate);
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-login Login
+     * @subsubsection bx_system_general-login_form login_form
+     * 
+     * @code bx_srv('system', 'login_form', ["no_auth_buttons"], 'TemplServiceLogin'); @endcode
+     * @code {{~system:login_form:TemplServiceLogin["no_auth_buttons"]~}} @endcode
+     * 
+     * Display login form
+     * @param $sParams optional string with additional params:
+     *          - no_join_text
+     *          - no_auth_buttons
+     *          - ajax_form
+     * @param $sForceRelocate optional URL to redirect to after login
+     * 
+     * @see BxBaseServiceLogin::serviceLoginForm
+     */
+    /** 
+     * @ref bx_system_general-login_form "login_form"
+     * @api @ref bx_system_general-login_form "login_form"
+     */
+    public function serviceLoginForm ($sParams = '', $sForceRelocate = '')
+    {
+        if (isLogged() && 'login' == bx_get('i')) {
+            if (!bx_is_api()) {
+                header('Location: ' . BX_DOL_URL_ROOT);
+                exit;
+            } 
+            else {
+                return [
+                    bx_api_get_msg('You are already logged in 🐵! Redirecting...'),
+                    ['id' => 2, 'type' => 'redirect', 'data' => ['uri' => '/posts-home', 'timeout' => 3000]],
+                ];
+            }
+        }
+
+        $oPemalink = BxDolPermalinks::getInstance();
+        $oForm = BxDolForm::getObjectInstance('sys_login', 'sys_login');
+
+        $sCustomHtmlBefore = '';
+        $sCustomHtmlAfter = '';
+        bx_alert('profile', 'show_login_form', 0, 0, array('oForm' => &$oForm, 'sParams' => &$sParams, 'sCustomHtmlBefore' => &$sCustomHtmlBefore, 'sCustomHtmlAfter' => &$sCustomHtmlAfter, 'aAuthTypes' => &$aAuthTypes));
+
+        if (isset($oForm->aInputs['relocate'])) {
+            if ($sForceRelocate && 0 === mb_stripos($sForceRelocate, BX_DOL_URL_ROOT))
+                $oForm->aInputs['relocate']['value'] = $sForceRelocate;
+            elseif ('homepage' == $sForceRelocate)
+                $oForm->aInputs['relocate']['value'] = BX_DOL_URL_ROOT;
+        }
+
+        if (bx_is_api()) {   
+            
+            if ($oForm->isSubmittedAndValid()) {
+                $oAccount = BxDolAccount::getInstance(trim($oForm->getCleanValue('ID')));
+                bx_login($oAccount->id(), ($oForm->getCleanValue('rememberMe') ? true : false));
+                return [
+                    bx_api_get_block('redirect', ['uri' => '/posts-home', 'timeout' => 1000]),
+                    bx_api_get_block('login', ['session' => BxDolSession::getInstance()->getId()], ['id' => 2]),
+                    bx_api_get_msg('Login Success 👍! Redirecting...', ['id' => 3]),
+                ];
+            } 
+            else {
+                return getParam('sys_account_disable_login_form') ? null : [
+                     bx_api_get_block('form', $oForm->getCodeAPI(), ['ext' => ['request' => ['url' => '/api.php?r=system/login_form/TemplServiceLogin', 'immutable' => true]]]),
+                ];
+            }
+        }
+
+        $aTmplVarsForm = [];
+        if(!(bool)getParam('sys_account_disable_login_form'))
+            $aTmplVarsForm['content'] = $oForm->getCode();
+
+        $aTmplVarsAuth = [];
+        if(strpos($sParams, 'no_auth_buttons') === false)
+            $aTmplVarsAuth['content'] = $this->serviceMemberAuthCode();
+
+        $aTmplVarsJoin = [];
+        if(strpos($sParams, 'no_join_text') === false)
+            $aTmplVarsJoin['url'] = bx_absolute_url($oPemalink->permalink('page.php?i=create-account'));
+
+        $oTemplate = BxDolTemplate::getInstance();
+        $oTemplate->addJs(array('jquery.form.min.js'));        
+        return $oTemplate->parseHtmlByName('block_login.html', [
+            'custom_html_before' => $sCustomHtmlBefore,
+            'bx_if:show_auth' => [
+                'condition' => !empty($aTmplVarsAuth),
+                'content' => $aTmplVarsAuth
+            ],
+            'bx_if:show_form' => [
+                'condition' => !empty($aTmplVarsForm),
+                'content' => $aTmplVarsForm,
+            ],
+            'custom_html_after' => $sCustomHtmlAfter,
+            'bx_if:show_join' => [
+                'condition' => !empty($aTmplVarsJoin),
+                'content' => $aTmplVarsJoin
+            ],
+            'agreement' => _t('_sys_form_login_input_agreement_value', bx_absolute_url($oPemalink->permalink('page.php?i=terms')), bx_absolute_url($oPemalink->permalink('page.php?i=privacy'))),
+            'bx_if:show_js_code' => [
+                'condition' => strpos($sParams, 'ajax_form') !== false,
+                'content' => []
+            ]
+        ]);
+
+    }
+    
+    public function serviceLoginFormStep2 ()
+    {
+        if(isLogged()){
+            header('Location: ' . BX_DOL_URL_ROOT);
+            exit;
+        }
+
+        $oSession = BxDolSession::getInstance();
+        $iAccountId = $oSession->getValue(BX_ACCOUNT_SESSION_KEY_FOR_2FA_LOGIN_ACCOUNT_ID);
+        if ($iAccountId == '')
+            return false;
+
+        $oForm = BxDolForm::getObjectInstance('sys_login', 'sys_login_step2');
+        $oForm->aFormAttrs['action'] = '';
+        
+        $oAccount = BxDolAccount::getInstance($iAccountId);
+        $a = $oAccount->getInfo($iAccountId);
+        $sPhoneNumber = trim($a['phone']);
+            
+        $oForm->initChecker(array('phone' => $sPhoneNumber));
+        if ($oForm->isSubmittedAndValid()) {
+            $sNewPhoneNumber = trim($oForm->getCleanValue('phone'));
+            if ($sPhoneNumber != $sNewPhoneNumber)
+                $oAccount->updatePhone($sNewPhoneNumber);
+            $sActivationCode = rand(1000, 9999);
+            $sActivationText =_t('_sys_txt_login_2fa_sms_text', $sActivationCode);
+            $ret = null;
+            bx_alert('account', 'before_2fa_send_sms', $oAccount->id(), false, array('phone_number' => $sPhoneNumber, 'sms_text' => $sActivationText, 'override_result' => &$ret));
+            if ($ret === null) 
+            {
+                $oTwilio = BxDolTwilio::getInstance();
+                if(!$oTwilio->sendSms($sNewPhoneNumber, $sActivationText)){
+                    return MsgBox(_t('_sys_txt_login_2fa_sms_error_occured'));
+                }
+            }
+            $oSession = BxDolSession::getInstance();
+            $oSession->setValue(BX_ACCOUNT_SESSION_KEY_FOR_PHONE_ACTIVATEION_CODE, $sActivationCode);
+            header('Location: ' . bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=login-step3')));
+        }
+        return '<div class="bx-def-padding-sec-bottom">' . _t("_sys_txt_login_2fa_description") . '</div>' .$oForm->getCode();
+    }
+    
+    public function serviceLoginFormStep3 ()
+    {
+        if(isLogged()){
+            header('Location: ' . BX_DOL_URL_ROOT);
+            exit;
+        }
+        
+        $oForm = BxDolForm::getObjectInstance('sys_login', 'sys_login_step3');
+        $oForm->aInputs['back']['caption'] = _t( $oForm->aInputs['back']['caption_src'], bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=login-step2')));
+        $oForm->aFormAttrs['action'] = '';
+        $oForm->initChecker();
+        
+        if ($oForm->isSubmittedAndValid()) {
+            $oSession = BxDolSession::getInstance();
+            $oSession->unsetValue(BX_ACCOUNT_SESSION_KEY_FOR_PHONE_ACTIVATEION_CODE);
+            $iAccountId = $oSession->getValue(BX_ACCOUNT_SESSION_KEY_FOR_2FA_LOGIN_ACCOUNT_ID);
+            $oAccount = BxDolAccount::getInstance($iAccountId);
+            $aAccount = bx_login($oAccount->id(), $oSession->getValue(BX_ACCOUNT_SESSION_KEY_FOR_2FA_LOGIN_IS_REMEMBER));
+           
+            $sUrlRelocate = $oForm->getCleanValue('relocate');
+            if (!$sUrlRelocate || 0 !== strncmp($sUrlRelocate, BX_DOL_URL_ROOT, strlen(BX_DOL_URL_ROOT)))
+                $sUrlRelocate = BX_DOL_ROLE_ADMIN == $oForm->getRole() ? BX_DOL_URL_STUDIO . 'launcher.php' : BX_DOL_URL_ROOT . 'member.php';
+
+            bx_alert('account', 'login_after', $oAccount->id(),  false, array(
+                'account' => $aAccount,
+                'url_relocate' => &$sUrlRelocate               
+            ));
+
+            BxDolTemplate::getInstance()->setPageNameIndex (BX_PAGE_TRANSITION);
+            BxDolTemplate::getInstance()->setPageHeader (_t('_Please Wait'));
+            BxDolTemplate::getInstance()->setPageContent ('page_main_code', MsgBox(_t('_Please Wait')));
+            BxDolTemplate::getInstance()->setPageContent ('url_relocate', bx_html_attribute($sUrlRelocate, BX_ESCAPE_STR_QUOTE));
+            
+            header('Location: ' . $sUrlRelocate);
+        } 
+        return $oForm->getCode();
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-login Login
+     * @subsubsection bx_system_general-logout logout
+     * 
+     * @code bx_srv('system', 'logout', [], 'TemplServiceLogin'); @endcode
+     * 
+     * Logout
+     * 
+     * @see BxBaseServiceLogin::serviceLogout
+     */
+    /** 
+     * @ref bx_system_general-logout "logout"
+     * @api @ref bx_system_general-logout "logout"
+     */
+    public function serviceLogout ()
+    {
+        bx_logout();
+
+        if (bx_is_api()) {
+            return [
+                ['id' => 2, 'type' => 'redirect', 'data' => ['uri' => '/posts-home']],
+            ];
+        }
+        else {
+            header("Location: " . BX_DOL_URL_ROOT);
+        }
+        
+        return true;
+    }
+}
+
+/** @} */

+ 84 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseServicePages.php

@@ -0,0 +1,84 @@
+<?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 pages functionality.
+ */
+class BxBaseServicePages extends BxDol
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_pages System Services 
+     * @subsection bx_system_pages-pages Pages
+     * @subsubsection bx_system_pages-get_page_by_request Get page by page URI
+     * 
+     * @code bx_srv('system', 'get_page_by_request', ['about'], 'TemplServicePages'); @endcode
+     * @code {{~system:get_page_by_request:TemplServicePages['about']~}} @endcode
+     * 
+     * Test method which page data
+     * @param $sURI page URI 
+     * 
+     * @see BxBaseServicePages::serviceGetPage
+     */
+    /** 
+     * @ref bx_system_general-get_page_by_request "get_page_by_request"
+     */
+    public function serviceGetPageByRequest ($sRequest)
+    {
+        $mixed = BxDolPage::getPageBySeoLink($sRequest);
+
+        if (($sUrl = $mixed) && is_string($sUrl)) {
+            $aRes = ['redirect' => $sUrl];
+        }
+        elseif (($oPage = $mixed) && is_object($oPage)) {
+            return $oPage->getPageAPI();
+        }
+        else {
+            $aRes = ['code' => 404, 'error' => _t("_sys_request_page_not_found_cpt")];
+        }
+        
+        return $aRes;
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_pages System Services 
+     * @subsection bx_system_pages-pages Pages
+     * @subsubsection bx_system_pages-get_page_by_uri Get page by page URI
+     * 
+     * @code bx_srv('system', 'get_page_by_uri', ['about'], 'TemplServicePages'); @endcode
+     * @code {{~system:get_page_by_uri:TemplServicePages['about']~}} @endcode
+     * 
+     * Test method which page data
+     * @param $sURI page URI 
+     * 
+     * @see BxBaseServicePages::serviceGetPage
+     */
+    /** 
+     * @ref bx_system_general-get_page_by_uri "get_page_by_uri"
+     */
+    public function serviceGetPageByUri ($sURI)
+    {
+        $oPage = BxDolPage::getObjectInstanceByURI($sURI, false, true);
+        if (!$oPage) {
+            $aRes = ['error' => _t("_sys_request_page_not_found_cpt")];
+        } 
+        else {
+            $aRes = $oPage->getPage();
+        }
+        return $aRes;
+    }
+}
+
+/** @} */

+ 666 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseServiceProfiles.php

@@ -0,0 +1,666 @@
+<?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
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    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 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);
+    }
+
+    /**
+     * @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 = 20;
+        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 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 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;
+
+            $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))),
+            );
+        }
+
+        $oTemplate->addCss('account.css');
+        return array(
+            'content' => $oTemplate->parseHtmlByName('profile_switch_row.html', $aVars),
+        );
+    }
+
+    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();
+    }
+
+    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;
+    }
+
+}
+
+/** @} */

+ 1228 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseServices.php

@@ -0,0 +1,1228 @@
+<?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',
+            'Cmts' => 'BxBaseServices',
+
+            'CreateAccountForm' => 'BxBaseServiceAccount',
+            'ForgotPassword' => 'BxBaseServiceAccount',
+            'SwitchProfile' => 'BxBaseServiceAccount',
+
+            'CategoriesList' => 'BxBaseServiceCategory',
+
+            'Test' => 'BxBaseServiceLogin',
+            'MemberAuthCode' => 'BxBaseServiceLogin',
+            'LoginForm' => 'BxBaseServiceLogin',
+            'LoginFormOnly' => 'BxBaseServiceLogin',
+            'Logout' => 'BxBaseServiceLogin',
+        
+            'KeywordsCloud' => 'BxBaseServiceMetatags',
+
+            'ProfileMembership' => 'BxBaseServiceProfiles',
+            'ProfileNotifications' => 'BxBaseServiceProfiles',
+            'GetCountOnlineProfiles' => 'BxBaseServiceProfiles',
+
+            'GetChartGrowth' => 'BxBaseChartServices',
+            'GetChartStats' => 'BxBaseChartServices',
+
+            'GetCartItemsCount' => 'BxBasePaymentsServices',
+            'GetOrdersCount' => 'BxBasePaymentsServices',
+
+            'Do' => 'BxBaseVoteServices',
+            'GetPerformedBy' => 'BxBaseVoteServices',
+
+            'Perform' => 'BxBaseServiceConnections',
+            'GetDataApi' => 'BxBaseCmtsServices'
+        );
+    }
+
+    public function serviceGetPreloaderContent($sName) {
+        $sResult = '';
+
+        switch($sName) {
+            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)
+    {
+        if(is_string($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)
+    {
+        if(is_string($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)
+    {
+        if(is_string($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)
+    {
+        $aResults = array();
+
+        $aModules = BxDolModuleQuery::getInstance()->getModulesBy(array('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;
+
+            $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 ()
+    {
+        $oSearch = $this->_getSearchObject();
+        
+        $sCode = '';
+        if (bx_get('keyword') !== false) {
+            $sCode = $oSearch->response();
+            if (!$sCode)
+                $sCode = $oSearch->getEmptyResult();
+        }
+
+        return $sCode;
+    }
+    
+    /**
+     * @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 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 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 serviceRedirect($sUrl = false)
+    {
+        if (false === $sUrl)
+            $sUrl = bx_get('url');
+
+        if (!$sUrl || !preg_match('@^https?://@', $sUrl)) {
+            return MsgBox(_t('_error occured'));
+        }
+        else {
+
+            $bSpam = null;
+            bx_alert('system', 'check_spam_url', 0, getLoggedId(), array('is_spam' => &$bSpam, 'content' => &$sUrl, 'where' => 'redirect'));
+
+            if (false === $bSpam) {
+                header('Location: ' . $sUrl);
+            } 
+            elseif (true === $bSpam || null === $bSpam) {
+                return BxDolTemplate::getInstance()->parseHtmlByName('redirect.html', [
+                    'text' => _t('_sys_redirect_confirmation', 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,
+            )
+    	);
+    }
+
+    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;
+    }
+}
+
+/** @} */

+ 368 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseVote.php

@@ -0,0 +1,368 @@
+<?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
+ * @{
+ */
+
+/**
+ * @see BxDolVote
+ */
+class BxBaseVote extends BxDolVote
+{
+    protected $_bCssJsAdded;
+
+    protected $_sJsClsName;
+    protected $_sJsObjName;
+    protected $_sStylePrefix;
+
+    protected $_aHtmlIds;
+
+    protected $_sTmplNameLegend;
+    protected $_sTmplNameByList;
+
+    public function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
+    {
+        parent::__construct($sSystem, $iId, $iInit, $oTemplate);
+
+        $this->_aElementDefaults = [];
+        $this->_aElementDefaultsApi = [];
+
+        $this->_bCssJsAdded = false;
+        $this->_sStylePrefix = 'bx-vote';
+
+        $this->_sJsClsName = 'BxDolVote';
+        $this->_sJsObjName = 'oVote' . bx_gen_method_name($sSystem, array('_' , '-')) . $iId;
+
+        $sHtmlId = str_replace(array('_' , ' '), array('-', '-'), $sSystem) . '-' . $iId;
+        $this->_aHtmlIds = array(
+            'counter' => 'bx-vote-counter-' . $sHtmlId,
+            'by_popup' => 'bx-vote-by-popup-' . $sHtmlId
+        );
+
+        $this->_sTmplNameLegend = 'vote_legend.html';
+        $this->_sTmplNameByList = 'vote_by_list.html';
+
+        $this->_sTmplContentElementBlock = $this->_oTemplate->getHtml('vote_element_block.html');
+        $this->_sTmplContentElementInline = $this->_oTemplate->getHtml('vote_element_inline.html');
+        $this->_sTmplContentCounter = $this->_oTemplate->getHtml('vote_counter.html');
+    }
+
+    public function getJsClassName()
+    {
+        return $this->_sJsClsName;
+    }
+
+    public function getJsObjectName()
+    {
+        return $this->_sJsObjName;
+    }
+
+    public function getJsScript($aParams = array())
+    {
+        $sJsObjName = $this->getJsObjectName();
+        $sJsObjClass = $this->getJsClassName();
+
+        $bDynamicMode = isset($aParams['dynamic_mode']) && (bool)$aParams['dynamic_mode'] === true;
+
+        $sCode = "if(window['" . $sJsObjName . "'] == undefined) var " . $sJsObjName . " = new " . $sJsObjClass . "(" . json_encode($this->_prepareParamsData([
+            'aRequestParams' => $this->_prepareRequestParamsData($aParams)
+        ])) . ");";
+
+        return $this->_oTemplate->_wrapInTagJsCode($sCode);
+    }
+
+    public function getJsClick($iValue = 0)
+    {
+        return $this->getJsObjectName() . '.vote(this, ' . $iValue . ')';
+    }
+
+    public function getJsClickCounter($aParams = array())
+    {
+        return $this->getJsObjectName() . '.toggleByPopup(this)';
+    }
+
+    public function getCounter($aParams = [])
+    {
+        $aParams = array_merge($this->_aElementDefaults, $aParams);
+
+        $bDynamicMode = isset($aParams['dynamic_mode']) && (bool)$aParams['dynamic_mode'] === true;
+        $bShowEmpty = isset($aParams['show_counter_empty']) && (bool)$aParams['show_counter_empty'] === true;
+        $bShowActive = $this->isAllowedVoteViewVoters() && (!isset($aParams['show_counter_active']) || (bool)$aParams['show_counter_active'] === true);
+        $bShowScript = !isset($aParams['show_script']) || (bool)$aParams['show_script'] === true;
+
+        $sClass = 'sys-action-counter';
+        if(isset($aParams['show_counter_only']) && (bool)$aParams['show_counter_only'] === true)
+            $sClass .= ' sys-ac-only';
+
+        $sClass .= ' ' .$this->_sStylePrefix . '-counter ' . $this->_sStylePrefix . '-counter-' . $this->_sType;
+        if(!empty($aParams['class_counter']))
+            $sClass .= $aParams['class_counter'];
+
+        $aTmplVarsAttrs = array();
+
+        if($bShowActive)
+            $aTmplVarsAttrs = array_merge($aTmplVarsAttrs, array(
+                array('key' => 'href', 'value' => 'javascript:void(0)'),
+                array('key' => 'title', 'value' => bx_html_attribute($this->_getTitleDoBy($aParams))),
+                array('key' => 'onclick', 'value' => 'javascript:' . $this->getJsClickCounter($aParams))
+            ));
+
+        $sHtmlId = isset($aParams['id_counter']) ? $aParams['id_counter'] : $this->_aHtmlIds['counter'];
+        if(!empty($sHtmlId))
+            $aTmplVarsAttrs[] = array('key' => 'id', 'value' => $sHtmlId);
+
+        $aVote = !empty($aParams['vote']) && is_array($aParams['vote']) ? $aParams['vote'] : $this->_getVote();
+        $sContent = $bShowEmpty || (int)$aVote['count'] > 0 ? $this->_getCounterLabel($aVote['count'], $aParams) : '';
+
+        return $this->_oTemplate->parseHtmlByContent($this->_getTmplContentCounter(), array(
+            'bx_if:show_text' => array(
+                'condition' => !$bShowActive,
+                'content' => array(
+                    'class' => $sClass,
+                    'bx_repeat:attrs' => $aTmplVarsAttrs,
+                    'content' => $sContent
+                )
+            ),
+            'bx_if:show_link' => array(
+                'condition' => $bShowActive,
+                'content' => array(
+                    'class' => $sClass,
+                    'bx_repeat:attrs' => $aTmplVarsAttrs,
+                    'content' => $sContent
+                )
+            ),
+            'class' => $sClass,
+            'bx_repeat:attrs' => $aTmplVarsAttrs,
+            'content' => $sContent,
+            'script' => $bShowScript ? $this->getJsScript($aParams) : ''
+        ));
+    }
+
+    public function getLegend($aParams = array())
+    {
+        return '';
+    }
+
+    public function getElementBlock($aParams = array())
+    {
+        $aParams['usage'] = BX_DOL_VOTE_USAGE_BLOCK;
+
+        return $this->getElement($aParams);
+    }
+
+    public function getElementInline($aParams = array())
+    {
+        $aParams['usage'] = BX_DOL_VOTE_USAGE_INLINE;
+
+        return $this->getElement($aParams);
+    }
+
+    public function getElement($aParams = array())
+    {
+        $sMethod = '_getTmplContentElement' . bx_gen_method_name(!empty($aParams['usage']) ? $aParams['usage'] : BX_DOL_VOTE_USAGE_DEFAULT);
+        if(!method_exists($this, $sMethod))
+            return '';
+
+        $aTmplVars = $this->_getTmplVarsElement($aParams);
+        if(empty($aTmplVars) || !is_array($aTmplVars))
+            return '';
+
+        return $this->_oTemplate->parseHtmlByContent($this->$sMethod(), $aTmplVars);
+    }
+    
+    public function getElementAPI($aParams = [])
+    {
+        if(!($this->_bApi = bx_is_api()))
+            return;
+
+        if(!$this->isEnabled())
+            return bx_api_get_msg('_vote_err_not_enabled');
+
+        $aParams = array_merge($this->_aElementDefaultsApi, $aParams);
+
+        $iObjectId = $this->getId();
+        $iAuthorId = $this->_getAuthorId();
+
+        $bCount = $this->_isCount();
+        $isAllowedVote = $this->isAllowedVote();
+        $isAllowedVoteView = $this->isAllowedVoteView();
+        $aParams['is_voted'] = $this->isPerformed($iObjectId, $iAuthorId);
+        $aParams['track'] = $aParams['is_voted'] ? $this->_getTrack($iObjectId, $iAuthorId) : array();
+
+        //--- Do Vote
+        $bDoVote = $this->_isShowDoVote($aParams, $isAllowedVote, $bCount);
+        $aDoVote = $bDoVote ? $this->_getDoVote($aParams, $isAllowedVote) : [];
+
+        //--- Counter
+        $bCounter = $this->_isShowCounter($aParams, $isAllowedVote, $isAllowedVoteView, $bCount);
+        $aCounter = $bCounter ? $this->getCounterAPI(array_merge($aParams, [
+            'show_counter_only' => false, 
+            'show_script' => false
+        ])) : [];
+
+        //--- Legend
+        $bLegend = $this->_isShowLegend($aParams, $isAllowedVote, $isAllowedVoteView, $bCount);
+        $aLegend = $bLegend ? $this->getLegend($aParams) : [];
+
+        if(!$bDoVote && !$bCounter && !$bLegend)
+            return bx_api_get_msg('');
+
+        return [
+            'type' => $this->_sType,
+            'system' => $this->_sSystem,
+            'object_id' => $this->_iId,
+            'params' => $aParams,
+            'action' => $aDoVote,
+            'counter' => $aCounter,
+            'legend' => $aLegend,
+        ];
+    }
+
+    /**
+     * Internal methods.
+     */
+    protected function _prepareParamsData($aParams)
+    {
+        return parent::_prepareParamsData(array_merge([
+            'sObjName' => $this->getJsObjectName(),
+            'sStylePrefix' => $this->_sStylePrefix,
+            'aHtmlIds' => $this->_aHtmlIds,
+        ], $aParams));
+    }
+
+    /*
+     * This method should be overwritten by subclass.
+     */
+    protected function _getTmplVarsElement($aParams = array())
+    {
+    	$aParams = array_merge($this->_aElementDefaults, $aParams);
+
+    	$bDynamicMode = isset($aParams['dynamic_mode']) && (bool)$aParams['dynamic_mode'] === true;
+        $bShowCounterEmpty = isset($aParams['show_counter_empty']) && (bool)$aParams['show_counter_empty'] === true;
+
+        $iObjectId = $this->getId();
+        $iAuthorId = $this->_getAuthorId();
+
+        $bCount = $this->_isCount();
+        $isAllowedVote = $this->isAllowedVote();
+        $isAllowedVoteView = $this->isAllowedVoteView();
+        $aParams['is_voted'] = $this->isPerformed($iObjectId, $iAuthorId);
+        $aParams['track'] = $aParams['is_voted'] ? $this->_getTrack($iObjectId, $iAuthorId) : array();
+
+        //--- Do Vote
+        $bTmplVarsDoVote = $this->_isShowDoVote($aParams, $isAllowedVote, $bCount);
+        $aTmplVarsDoVote = array();
+        if($bTmplVarsDoVote)
+            $aTmplVarsDoVote = array(
+                'style_prefix' => $this->_sStylePrefix,
+                'do_vote' => $this->_getDoVote($aParams, $isAllowedVote),
+            );
+
+        //--- Counter
+        $bTmplVarsCounter = $this->_isShowCounter($aParams, $isAllowedVote, $isAllowedVoteView, $bCount);
+        $aTmplVarsCounter = array();
+        if($bTmplVarsCounter)
+            $aTmplVarsCounter = array(
+                'style_prefix' => $this->_sStylePrefix,
+                'bx_if:show_hidden' => array(
+                    'condition' => !$bCount && !$bShowCounterEmpty,
+                    'content' => array()
+                ),
+                'counter' => $this->getCounter(array_merge($aParams, [
+                    'show_counter_only' => false, 
+                    'show_script' => false
+                ]))
+            );
+
+        //--- Legend
+        $bTmplVarsLegend = $this->_isShowLegend($aParams, $isAllowedVote, $isAllowedVoteView, $bCount);
+        $aTmplVarsLegend = array();
+        if($bTmplVarsLegend)
+            $aTmplVarsLegend = array(
+                'legend' => $this->getLegend($aParams)
+            );
+
+        if(!$bTmplVarsDoVote && !$bTmplVarsCounter && !$bTmplVarsLegend)
+            return array();
+
+        $sClass = $this->_sStylePrefix . '-' . $this->_sType;
+        if(!empty($aParams['class_element']))
+            $sClass .= $aParams['class_element'];
+
+        return array(
+            'style_prefix' => $this->_sStylePrefix,
+            'html_id' => $this->_aHtmlIds['main'],
+            'class' => $sClass . ' ' . $this->_aHtmlIds['main'],
+            'bx_if:show_vote_data' => array(
+                'condition' => false,
+                'content' => array()
+            ),
+            'bx_if:show_do_vote' => array(
+                'condition' => $bTmplVarsDoVote,
+                'content' => $aTmplVarsDoVote
+            ),
+            'bx_if:show_counter' => array(
+                'condition' => $bTmplVarsCounter,
+                'content' => $aTmplVarsCounter
+            ),
+            'bx_if:show_legend' => array(
+            	'condition' => $bTmplVarsLegend,
+            	'content' => $aTmplVarsLegend
+            ),
+            'script' => $this->getJsScript($aParams)
+        );
+    }
+
+    protected function _getDoVote($aParams = array(), $isAllowedVote = true)
+    {
+        return $this->_bApi ? [] : '';
+    }
+
+    protected function _getCounterLabel($iCount, $aParams = array())
+    {
+        return _t(isset($aParams['caption']) ? $aParams['caption'] : '_vote_counter', $iCount);
+    }
+
+    protected function _getVotedBy($aParams = array())
+    {
+        $aTmplUsers = array();
+
+        $aUserIds = $this->_oQuery->getPerformedBy($this->getId());
+        foreach($aUserIds as $iUserId) {
+            list($sUserName, $sUserUrl, $sUserIcon, $sUserUnit) = $this->_getAuthorInfo($iUserId);
+            $aTmplUsers[] = array(
+                'style_prefix' => $this->_sStylePrefix,
+                'user_unit' => $sUserUnit
+            );
+        }
+
+        if(empty($aTmplUsers))
+            $aTmplUsers = MsgBox(_t('_Empty'));
+
+        return $this->_oTemplate->parseHtmlByName($this->_sTmplNameByList, array(
+            'style_prefix' => $this->_sStylePrefix,
+            'class' => '',
+            'bx_repeat:list' => $aTmplUsers
+        ));
+    }
+
+    protected function _isShowDoVote($aParams, $isAllowedVote, $bCount)
+    {
+        return !isset($aParams['show_do_vote']) || (bool)$aParams['show_do_vote'] === true;
+    }
+
+    protected function _isShowCounter($aParams, $isAllowedVote, $isAllowedVoteView, $bCount)
+    {
+        return isset($aParams['show_counter']) && (bool)$aParams['show_counter'] === true && $isAllowedVoteView && ($isAllowedVote || $bCount);
+    }
+
+    protected function _isShowLegend($aParams, $isAllowedVote, $isAllowedVoteView, $bCount)
+    {
+        return false;
+    }
+}
+
+/** @} */

+ 168 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files/template/scripts/BxBaseVoteServices.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    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * System services related to Votes.
+ */
+class BxBaseVoteServices extends BxDol
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-vote Vote
+     * @subsubsection bx_system_general-do do
+     * 
+     * @code bx_srv('system', 'do', [[...]], 'TemplVoteServices'); @endcode
+     * @code {{~system:do:TemplVoteServices[[...]]~}} @endcode
+     * 
+     * Performs Do (Vote) action
+     * @param $aParams an array with necessary parameters 
+     * 
+     * @see BxBaseVoteServices::serviceDo
+     */
+    /** 
+     * @ref bx_system_general-do "do"
+     * @api @ref bx_system_general-do "do"
+     */
+    public function serviceDo($aParams)
+    {
+        if(is_string($aParams))
+            $aParams = json_decode($aParams, true);
+
+        if(!$aParams['s'] || !$aParams['o'])
+            return ['code' => BX_DOL_OBJECT_ERR_NOT_AVAILABLE];
+
+        $oVote = BxDolVote::getObjectInstance($aParams['s'], $aParams['o']);
+        if(!$oVote)
+            return ['code' => BX_DOL_OBJECT_ERR_NOT_AVAILABLE];
+
+        $sMethod = '_serviceDo' . bx_gen_method_name($oVote->getType());
+        if(!method_exists($this, $sMethod))
+            return ['code' => BX_DOL_OBJECT_ERR_NOT_AVAILABLE];
+
+        return $this->$sMethod($aParams, $oVote);
+    }
+
+    protected function _serviceDoLikes($aParams, &$oVote)
+    {
+        $aResult = $oVote->vote($aParams);
+        if((int)$aResult['code'] != 0)
+            return $aResult;
+
+        return [
+            'is_voted' => $aResult['voted'],
+            'is_disabled' => $aResult['disabled'],
+            'icon' => $aResult['label_emoji'],
+            'title' => $aResult['label_title'],
+            'counter' => $oVote->getVote()
+        ];
+    }
+
+    protected function _serviceDoReactions($aParams, &$oVote)
+    {
+        $aResult = $oVote->vote($aParams);
+        if((int)$aResult['code'] != 0)
+            return $aResult;
+
+        $aDefault = $oVote->getReaction($oVote->getDefault());
+        $aDefaultInfo = $oVote->getReaction($aDefault['name']);
+
+        return [
+            'is_voted' => $aResult['voted'],
+            'is_disabled' => $aResult['disabled'],
+            'reaction' => $aResult['reaction'],
+            'icon' => !empty($aResult['label_emoji']) ? $aResult['label_emoji'] : $aDefaultInfo['emoji'],
+            'title' => !empty($aResult['label_title']) ? $aResult['label_title'] : '',
+            'counter' => $oVote->getVote()
+        ];
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-vote Vote
+     * @subsubsection bx_system_general-get_performed_by get_performed_by
+     * 
+     * @code bx_srv('system', 'get_performed_by', [[...]], 'TemplVoteServices'); @endcode
+     * @code {{~system:get_performed_by:TemplVoteServices[[...]]~}} @endcode
+     * 
+     * Gets a list of PerformedBy users.
+     * @param $aParams an array with necessary parameters 
+     * 
+     * @see BxBaseVoteServices::serviceGetPerformedBy
+     */
+    /** 
+     * @ref bx_system_general-get_performed_by "get_performed_by"
+     * @api @ref bx_system_general-get_performed_by "get_performed_by"
+     */
+    public function serviceGetPerformedBy($aParams)
+    {
+        if(is_string($aParams))
+            $aParams = json_decode($aParams, true);
+
+        if(!$aParams['s'] || !$aParams['o'])
+            return ['code' => BX_DOL_OBJECT_ERR_NOT_AVAILABLE];
+
+        $oVote = BxDolVote::getObjectInstance($aParams['s'], $aParams['o']);
+        if(!$oVote)
+            return ['code' => BX_DOL_OBJECT_ERR_NOT_AVAILABLE];
+
+        $sMethod = '_serviceGetPerformedBy' . bx_gen_method_name($oVote->getType());
+        if(!method_exists($this, $sMethod))
+            return ['code' => BX_DOL_OBJECT_ERR_NOT_AVAILABLE];
+
+        return $this->$sMethod($aParams, $oVote);
+    }
+
+    protected function _serviceGetPerformedByLikes($aParams, &$oVote)
+    {
+        $aValues = $oVote->getQueryObject()->getPerformedBy($oVote->getId());
+
+        $aTmplUsers = [];
+        foreach($aValues as $mValue) {
+            $mValue = is_array($mValue) ? $mValue : ['author_id' => (int)$mValue, 'reaction' => ''];
+
+            $aTmplUsers[] = BxDolProfile::getData($mValue['author_id']);
+        }
+
+        return [
+            'performed_by' => $aTmplUsers
+        ];
+    }
+
+    protected function _serviceGetPerformedByReactions($aParams, &$oVote)
+    {
+        $aReactions = !empty($aParams['reaction']) ? [$aParams['reaction']] : $oVote->getReactions();
+
+        $aResult = [];
+        foreach($aReactions as $sReaction) {
+            $aValues = $oVote->getQueryObject()->getPerformed(['type' => 'by', 'object_id' => $oVote->getId(), 'reaction'=> $sReaction]);
+
+            $aTmplUsers = [];
+            foreach($aValues as $mValue) {
+                $mValue = is_array($mValue) ? $mValue : ['author_id' => (int)$mValue, 'reaction' => ''];
+
+                $aTmplUsers[] = BxDolProfile::getData($mValue['author_id']);
+            }
+
+            $aResult[$sReaction] = $aTmplUsers;
+        }
+
+        return [
+            'performed_by' => $aResult
+        ];
+    }
+}
+
+/** @} */

+ 12 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/files_delete.php

@@ -0,0 +1,12 @@
+<?php
+
+$aFilesDelete = array(
+
+//	'plugins/phpmailer/Exception.php',
+//	'plugins/phpmailer/LICENSE',
+//	'plugins/phpmailer/PHPMailer.php',
+//	'plugins/phpmailer/README.md',
+//	'plugins/phpmailer/SMTP.php',
+//	'plugins/phpmailer/VERSION',
+);
+

+ 4 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/script.php

@@ -0,0 +1,4 @@
+<?php
+
+
+    return true;

+ 113 - 0
upgrade/files/13.0.0.RC3-13.0.0.RC4/sql.sql

@@ -0,0 +1,113 @@
+
+SET @sStorageEngine = (SELECT `value` FROM `sys_options` WHERE `name` = 'sys_storage_default');
+
+-- Settings
+
+SET @iCategoryIdHidden = (SELECT `id` FROM `sys_options_categories` WHERE `name` = 'hidden');
+
+INSERT IGNORE INTO `sys_options`(`category_id`, `name`, `caption`, `value`, `type`, `extra`, `check`, `check_params`, `check_error`, `order`) VALUES
+(@iCategoryIdHidden, 'sys_session_auth', '_adm_stg_cpt_option_sys_session_auth', '', 'checkbox', '', '', '', '', 112);
+
+
+SET @iTypeId = (SELECT `id` FROM `sys_options_types` WHERE `name` = 'system');
+
+DELETE FROM `sys_options_categories` WHERE `name` = 'api';
+INSERT INTO `sys_options_categories`(`type_id`, `name`, `caption`, `hidden`, `order`) VALUES (@iTypeId, 'api', '_adm_stg_cpt_category_api', 1, 2);
+
+SET @iCategoryIdApi = (SELECT `id` FROM `sys_options_categories` WHERE `name` = 'api');
+
+DELETE FROM `sys_options` WHERE `name` IN('sys_api_enable', 'sys_api_access_by_origin', 'sys_api_access_by_key', 'sys_api_access_unsafe_services', 'sys_api_cookie_path', 'sys_api_cookie_secure', 'sys_api_cookie_samesite', 'sys_api_comments_flat', '');
+INSERT IGNORE INTO `sys_options`(`category_id`, `name`, `caption`, `value`, `type`, `extra`, `check`, `check_error`, `order`) VALUES
+(@iCategoryIdApi, 'sys_api_enable', '_adm_stg_cpt_option_sys_api_enable', '', 'checkbox', '', '', '', 1),
+(@iCategoryIdApi, 'sys_api_access_by_origin', '_adm_stg_cpt_option_sys_api_access_by_origin', '', 'checkbox', '', '', '', 10),
+(@iCategoryIdApi, 'sys_api_access_by_key', '_adm_stg_cpt_option_sys_api_access_by_key', '', 'checkbox', '', '', '', 20),
+(@iCategoryIdApi, 'sys_api_access_unsafe_services', '_adm_stg_cpt_option_sys_api_access_unsafe_services', '', 'checkbox', '', '', '', 30),
+(@iCategoryIdApi, 'sys_api_cookie_path', '_adm_stg_cpt_option_sys_api_cookie_path', '/', 'digit', '', '', '', 40),
+(@iCategoryIdApi, 'sys_api_cookie_secure', '_adm_stg_cpt_option_sys_api_cookie_secure', '', 'checkbox', '', '', '', 42),
+(@iCategoryIdApi, 'sys_api_cookie_samesite', '_adm_stg_cpt_option_sys_api_cookie_samesite', 'Lax', 'select', 'None,Lax,Strict', '', '', 44),
+(@iCategoryIdApi, 'sys_api_comments_flat', '_adm_stg_cpt_option_sys_api_comments_flat', '', 'checkbox', '', '', '', 100);
+
+
+-- MENU
+
+UPDATE `sys_menu_items` SET `link` = '' WHERE `link` = 'index.php' AND `name` = 'home' AND `set_name` IN('sys_site', 'sys_application', 'sys_homepage_submenu');
+
+-- GRID: API Origins
+
+INSERT IGNORE INTO `sys_objects_grid` (`object`, `source_type`, `source`, `table`, `field_id`, `field_order`, `field_active`, `paginate_url`, `paginate_per_page`, `paginate_simple`, `paginate_get_start`, `paginate_get_per_page`, `filter_fields`, `filter_fields_translatable`, `filter_mode`, `sorting_fields`, `sorting_fields_translatable`, `override_class_name`, `override_class_file`) VALUES
+('sys_studio_api_origins', 'Sql', 'SELECT * FROM `sys_api_origins` WHERE 1 ', 'sys_api_origins', 'id', 'order', '', '', 20, NULL, 'start', '', 'url', '', 'like', '', '', 'BxDolStudioApiOrigins', '');
+
+INSERT IGNORE INTO `sys_grid_fields` (`object`, `name`, `title`, `width`, `translatable`, `chars_limit`, `params`, `hidden_on`, `order`) VALUES
+('sys_studio_api_origins', 'order', '', '1%', 0, 0, '', '', 1),
+('sys_studio_api_origins', 'url', '_sys_txt_url', '80%', 0, 0, '', '', 2),
+('sys_studio_api_origins', 'actions', '', '19%', 0, 0, '', '', 3);
+
+INSERT IGNORE INTO `sys_grid_actions` (`object`, `type`, `name`, `title`, `icon`, `icon_only`, `confirm`, `order`) VALUES
+('sys_studio_api_origins', 'single', 'delete', '_Delete', 'remove', 1, 1, 1),
+('sys_studio_api_origins', 'independent', 'add', '_adm_form_btn_field_add', '', 0, 0, 1);
+
+-- GRID: API Keys
+
+INSERT IGNORE INTO `sys_objects_grid` (`object`, `source_type`, `source`, `table`, `field_id`, `field_order`, `field_active`, `paginate_url`, `paginate_per_page`, `paginate_simple`, `paginate_get_start`, `paginate_get_per_page`, `filter_fields`, `filter_fields_translatable`, `filter_mode`, `sorting_fields`, `sorting_fields_translatable`, `override_class_name`, `override_class_file`) VALUES
+('sys_studio_api_keys', 'Sql', 'SELECT * FROM `sys_api_keys` WHERE 1 ', 'sys_api_keys', 'id', 'order', '', '', 20, NULL, 'start', '', 'key,title', '', 'like', '', '', 'BxDolStudioApiKeys', '');
+
+INSERT IGNORE INTO `sys_grid_fields` (`object`, `name`, `title`, `width`, `translatable`, `chars_limit`, `params`, `hidden_on`, `order`) VALUES
+('sys_studio_api_keys', 'order', '', '1%', 0, 0, '', '', 1),
+('sys_studio_api_keys', 'title', '_Name', '40%', 0, 0, '', '', 2),
+('sys_studio_api_keys', 'key', '_sys_txt_api_key', '40%', 0, 0, '', '', 3),
+('sys_studio_api_keys', 'actions', '', '19%', 0, 0, '', '', 4);
+
+INSERT IGNORE INTO `sys_grid_actions` (`object`, `type`, `name`, `title`, `icon`, `icon_only`, `confirm`, `order`) VALUES
+('sys_studio_api_keys', 'single', 'delete', '_Delete', 'remove', 1, 1, 1),
+('sys_studio_api_keys', 'independent', 'add', '_adm_form_btn_field_add', '', 0, 0, 1);
+
+
+-- PAGES
+
+INSERT IGNORE INTO `sys_objects_page` (`object`, `uri`, `title_system`, `title`, `module`, `cover`, `layout_id`, `submenu`, `visible_for_levels`, `visible_for_levels_editable`, `url`, `meta_description`, `meta_keywords`, `meta_robots`, `cache_lifetime`, `cache_editable`, `deletable`, `override_class_name`, `override_class_file`, `sticky_columns`) VALUES
+('sys_logout', 'logout', '', '_sys_page_title_logout', 'system', 0, 18, '', 2147483647, 0, 'page.php?i=logout', '', '', '', 0, 0, 0, '', '', 0);
+
+DELETE FROM `sys_pages_blocks` WHERE `object` = 'sys_logout';
+INSERT INTO `sys_pages_blocks` (`object`, `cell_id`, `module`, `title_system`, `title`, `designbox_id`, `tabs`, `async`, `visible_for_levels`, `type`, `content`, `deletable`, `copyable`, `active`, `order`) VALUES
+('sys_logout', 1, 'system', '', '_sys_page_block_title_logout', 11, 0, 0, 2147483647, 'service', 'a:3:{s:6:\"module\";s:6:\"system\";s:6:\"method\";s:6:\"logout\";s:5:\"class\";s:17:\"TemplServiceLogin\";}', 0, 1, 1, 1);
+
+-- NEW TABLES
+
+CREATE TABLE IF NOT EXISTS `sys_api_keys` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `title` varchar(255) NOT NULL,
+  `key` varchar(48) NOT NULL,
+  `order` int(11) NOT NULL,
+  PRIMARY KEY (`id`)
+);
+
+CREATE TABLE IF NOT EXISTS `sys_api_origins` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `url` varchar(255) NOT NULL,
+  `order` int(11) NOT NULL,
+  PRIMARY KEY (`id`)
+);
+
+-- STUDIO
+
+DELETE FROM `tp`, `tw`, `twb`, `tpw` 
+USING `sys_std_pages` AS `tp` LEFT JOIN `sys_std_widgets` AS `tw` ON `tp`.`id` = `tw`.`page_id` LEFT JOIN `sys_std_widgets_bookmarks` AS `twb` ON `tw`.`id` = `twb`.`widget_id` LEFT JOIN `sys_std_pages_widgets` AS `tpw` ON `tw`.`id` = `tpw`.`widget_id`
+WHERE  `tp`.`name` = 'api';
+
+SET @iIdHome = (SELECT `id` FROM `sys_std_pages` WHERE `name` = 'home');
+
+INSERT INTO `sys_std_pages`(`index`, `name`, `header`, `caption`, `icon`) VALUES
+(3, 'api', '_adm_page_cpt_api', '_adm_page_cpt_api', 'wi-api.svg');
+SET @iIdAPI = LAST_INSERT_ID();
+
+INSERT INTO `sys_std_widgets`(`page_id`, `module`, `type`, `url`, `click`, `icon`, `caption`, `cnt_notices`, `cnt_actions`) VALUES
+(@iIdAPI, 'system', 'configuration', '{url_studio}api.php', '', 'wi-api.svg', '_adm_wgt_cpt_api', '', 'a:4:{s:6:"module";s:6:"system";s:6:"method";s:11:"get_actions";s:6:"params";a:0:{}s:5:"class";s:18:"TemplStudioModules";}');
+
+INSERT INTO `sys_std_pages_widgets`(`page_id`, `widget_id`, `order`) VALUES(@iIdHome, LAST_INSERT_ID(), 14);
+
+
+
+-- Last step is to update current version
+
+UPDATE `sys_modules` SET `version` = '13.0.0-RC4' WHERE (`version` = '13.0.0.RC3' OR `version` = '13.0.0-RC3') AND `name` = 'system';
+

Неке датотеке нису приказане због велике количине промена