瀏覽代碼

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

AntonLV 1 年之前
父節點
當前提交
568cee1270
共有 100 個文件被更改,包括 26234 次插入 和 6 次删除
  1. 5 0
      inc/classes/BxDolService.php
  2. 4 4
      inc/classes/BxDolSessionQuery.php
  3. 1 1
      inc/version.inc.php
  4. 1 1
      install/index.php
  5. 14 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/check.php
  6. 64 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/README.md
  7. 1135 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolConnection.php
  8. 446 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolConnectionQuery.php
  9. 1353 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolDb.php
  10. 739 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolGrid.php
  11. 214 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolGridConnections.php
  12. 140 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolLogsStorageFolder.php
  13. 527 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolProfileQuery.php
  14. 69 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolQueuePush.php
  15. 229 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolSearchExtended.php
  16. 179 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolService.php
  17. 125 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolSessionQuery.php
  18. 3766 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolTemplate.php
  19. 12 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/version.inc.php
  20. 875 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/modules/base/general/classes/BxBaseModGeneralFormsEntryHelper.php
  21. 0 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/modules/base/general/template/css/manage_tools.css
  22. 2532 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/modules/base/groups/classes/BxBaseModGroupsModule.php
  23. 24 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/modules/base/groups/template/css/main.css
  24. 94 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/modules/base/profile/classes/BxBaseModProfileUploaderCoverCrop.php
  25. 92 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/modules/base/profile/classes/BxBaseModProfileUploaderPictureCrop.php
  26. 23 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/modules/base/profile/template/unit.html
  27. 268 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/modules/base/text/template/css/main.css
  28. 7 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/plugins/autoload.php
  29. 75 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/plugins/composer/autoload_real.php
  30. 226 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/plugins/composer/autoload_static.php
  31. 366 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/plugins/composer/installed.php
  32. 315 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/plugins_public/highlight/highlight.min.js
  33. 1 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/plugins_public/history.adapter.jquery.js
  34. 0 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/plugins_public/tailwind/css/tailwind.max.css
  35. 0 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/plugins_public/tailwind/css/tailwind.min.css
  36. 271 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/classes/BxDolStudioAgentsAsstChatsCmts.php
  37. 181 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/classes/BxDolStudioFormsField.php
  38. 186 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/classes/BxDolStudioTemplate.php
  39. 72 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/js/forms.js
  40. 38 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/_header.html
  41. 247 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/css/builder_page.css
  42. 164 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/css/common.less
  43. 56 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/css/forms.css
  44. 235 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/css/general.css
  45. 16 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/css/grid.css
  46. 446 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/css/menu_top.css
  47. 22 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/dbd_htools.html
  48. 27 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/dbd_versions.html
  49. 22 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/menu_top_toolbar.html
  50. 37 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/page_breadcrumb.html
  51. 2064 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/scripts/BxBaseStudioBuilderPage.php
  52. 2287 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/scripts/BxBaseStudioFormsField.php
  53. 144 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/scripts_templ/BxTemplStudioFormsField.php
  54. 36 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/_page_toolbar.html
  55. 12 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/_page_toolbar_search.html
  56. 12 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/block_async_create_post.html
  57. 24 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/block_async_profile_units.html
  58. 32 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/block_async_text_units_gallery.html
  59. 32 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/block_async_text_units_list.html
  60. 27 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/comment.html
  61. 1 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/comment_do_comment_label.html
  62. 13 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/comment_reply_box.html
  63. 17 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/connection_counter_label.html
  64. 27 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/create_post_form.html
  65. 104 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/css/cmts.css
  66. 969 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/css/common.css
  67. 56 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/css/editor.quill.css
  68. 686 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/css/forms.css
  69. 176 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/css/grid.css
  70. 154 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/css/label.css
  71. 23 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/css/manage_tools.css
  72. 1546 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/css/tailwind.css
  73. 277 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/css/vote.css
  74. 1 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/favorite_do_favorite_label.html
  75. 1 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/feature_do_feature_label.html
  76. 18 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/form_field_section_title.html
  77. 17 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_2_columns.html
  78. 24 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_3_columns.html
  79. 24 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_bar_content_bar.html
  80. 17 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_bar_left.html
  81. 17 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_bar_right.html
  82. 19 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_bottom_area_2_columns.html
  83. 18 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_top_area_2_columns.html
  84. 25 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_top_area_3_columns.html
  85. 25 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_top_area_bar_content_bar.html
  86. 18 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_top_area_bar_left.html
  87. 18 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_top_area_bar_right.html
  88. 26 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_topbottom_area_2_columns.html
  89. 26 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_topbottom_area_bar_left.html
  90. 26 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_topbottom_area_bar_right.html
  91. 35 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_topbottom_area_col1_col3_col2.html
  92. 26 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_topbottom_area_col1_col5.html
  93. 33 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_topbottom_area_col2_col5_col3.html
  94. 10 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/menu_buttons_hor.html
  95. 1 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/menu_switch_language_title.html
  96. 1 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/report_do_report_label.html
  97. 16 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/score_by_list.html
  98. 1 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/score_do_vote_label.html
  99. 388 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/scripts/BxBaseAccountForms.php
  100. 743 0
      upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/scripts/BxBaseCmtsServices.php

+ 5 - 0
inc/classes/BxDolService.php

@@ -169,6 +169,11 @@ class BxDolService extends BxDol
             $aModule = $oDb->getModuleById($mixed);
         return $aModule;
     }
+
+    public static function cleanMemoryCache()
+    {
+        self::$_aMemoryCache = [];
+    }
 }
 
 /** @} */

+ 4 - 4
inc/classes/BxDolSessionQuery.php

@@ -70,13 +70,13 @@ class BxDolSessionQuery extends BxDolDb
     function deleteExpired()
     {
         $iTime = time() - BX_DOL_SESSION_LIFETIME;
-        $sSql = $this->prepare("SELECT `user_id`, `date` FROM `" . $this->sTable . "` WHERE `date` < ?", $iTime);
+        $sSql = $this->prepare("SELECT `user_id`, `date` FROM `" . $this->sTable . "` WHERE `date` < ? AND `user_id` != 0 ORDER BY `date` DESC LIMIT 50000", $iTime);
         $aRows = $this->getAll($sSql);
-        
+
         foreach ($aRows as $aRow) {
             $this->updateLastActivityAccount($aRow['user_id'], $aRow['date']);
         }
-        
+
         $sSql = $this->prepare("DELETE FROM `" . $this->sTable . "` WHERE `date` < ?", $iTime);
         $iRet = (int)$this->query($sSql);
         if ($iRet)
@@ -104,7 +104,7 @@ class BxDolSessionQuery extends BxDolDb
     {
         if ($iDate > 0) {
             $a = BxDolAccountQuery::getInstance()->getInfoById($iAccountId);
-            if ($iDate > $a['active'])
+            if ($a && $iDate > $a['active'])
                 BxDolAccountQuery::getInstance()->_updateField($iAccountId, 'active', $iDate);
         }
     }

+ 1 - 1
inc/version.inc.php

@@ -7,6 +7,6 @@
  * @{
  */
 
-define('BX_DOL_VERSION', '14.0.0-DEV9'); ///< system files version, for DB version @see bx_get_ver, DB and files version must match
+define('BX_DOL_VERSION', '14.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', '14.0.0-DEV9');
+define ('BX_DOL_VER', '14.0.0-RC4');
 
 define ('BX_INSTALL_DEFAULT_LANGUAGE', 'en');
 

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

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

+ 64 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/README.md

@@ -0,0 +1,64 @@
+<a href="https://unacms.com"><img width="300" alt="UNA Platform" src="https://github.com/user-attachments/assets/ef5c2250-e172-4b12-8948-f1084643bc55"></a>
+
+## Unified Networking Applications
+
+[![Official Website](https://img.shields.io/badge/website-una.io-blue.svg?style=for-the-badge)](https://unacms.com)
+[![GitHub license](https://img.shields.io/github/license/unaio/una?style=for-the-badge)](https://github.com/unacms/una/blob/master/license.txt)
+[![Releases](https://img.shields.io/github/downloads/unaio/una/total.svg?style=for-the-badge)](https://github.com/unacms/una)
+[![GitHub forks](https://img.shields.io/github/forks/unaio/una?style=for-the-badge)](https://github.com/unacms/una/network)
+
+UNA is a **Community** Management System. The core platform is a full-stack framework for independent community websites and apps. With UNA content management, collaboration, e-commerce, learning management, and social networking, you can build any online project you have in mind.
+
+![screenshot](https://user-images.githubusercontent.com/22210428/186073113-8f82f8f2-fd5a-4dbb-8328-e0ca847809b9.png)
+
+## Table of Contents
+
+- [Key Features](#key-features)
+- [Documentation](#documentation)
+- [Download](#download)
+- [Support & Development](#support--development)
+- [License](#license)
+
+## Key Features
+
+- **Unlimited Scalability**: Hundreds, thousands, or millions - get as many users as your hosting can handle. UNA won't limit you.
+- **Absolute Control**: You own the data, you set the rules, you lead the community. We give you the tools and support.
+- **Permissive License**: MIT - the license allowing unrestricted commercial and private use, distribution, and modification.
+- **Continuous Improvement**: Regular security, performance, and feature updates for the core platform, apps, and integrations.
+- **Collective Innovation**: Improvements and revisions initiated and co-funded by the platform users community.
+- **Integrations-Friendly**: Integrate 3rd-party services as UNA apps or use UNA REST API to talk to your external apps.
+- **Secure-By-Design**: Full-site SSL encryption, SPD development, regular service updates, and vulnerability audits.
+- **Compliance Readiness**: We offer HIPAA, GDPR, CCPA (and any other regulations) compliance preparation service.
+
+## Documentation
+
+To install and run the UNA platform, you'll need a web-hosting server or you can use [UNA Cloud hosting](https://una.io/start) for an instant launch.
+
+- [UNACMS.COM - Official Website](https://unacms.com)
+- [Documentation](https://unacms.com/wiki/Introduction)
+- [Discussions](https://unacms.com/page/discussions-home)
+- [Contact Team](https://unacms.com/page/contact)
+- [Installation](https://unacms.com/wiki/Installation)
+
+## Download
+
+You can [download](https://github.com/unaio/una/archive/refs/heads/master.zip) the latest installable version of UNA Core and install it on your server. You will need to [create an UNACMS.COM account](https://unacms.com) to access all features.
+
+## Support & Development
+
+UNA is managed by [Yasko.Studio](https://yasko.studio) team and developed with the help of [UNA Community](https://una.io).
+
+- Free community support - [Discussions](https://unacms.com/page/discussions-home)
+- Direct support and professional services - [UNA Pro](https://unacms.com/start)
+
+## License
+
+MIT
+
+---
+
+> Twitter [@unaplatform](https://twitter.com/unaplatform)
+
+---
+
+This project is tested with BrowserStack.

+ 1135 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolConnection.php

@@ -0,0 +1,1135 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Default limit for connections list in Counter.
+ */
+define('BX_CONNECTIONS_LIST_COUNTER', 5);
+
+/**
+ * Default limit for connections lists
+ */
+define('BX_CONNECTIONS_LIST_LIMIT', 1000);
+
+/**
+ * No limit for connections lists. 
+ * Is needed for Total Number calculation.
+ */
+define('BX_CONNECTIONS_LIST_NO_LIMIT', -1);
+
+/**
+ * Connections order: no order
+ */
+define('BX_CONNECTIONS_ORDER_NONE', 0);
+
+/**
+ * Connections order: by addded time, asceding
+ */
+define('BX_CONNECTIONS_ORDER_ADDED_ASC', 1);
+
+/**
+ * Connections order: by addded time, desceding
+ */
+define('BX_CONNECTIONS_ORDER_ADDED_DESC', 2);
+
+/**
+ * Connection type: one-way
+ */
+define('BX_CONNECTIONS_TYPE_ONE_WAY', 'one-way');
+
+/**
+ * Connection type: mutual
+ */
+define('BX_CONNECTIONS_TYPE_MUTUAL', 'mutual');
+
+/**
+ * Connections content type: content
+ */
+define('BX_CONNECTIONS_CONTENT_TYPE_CONTENT', 'content');
+
+/**
+ * Connections content type: initiators
+ */
+define('BX_CONNECTIONS_CONTENT_TYPE_INITIATORS', 'initiators');
+
+/**
+ * Connections content type: common
+ */
+define('BX_CONNECTIONS_CONTENT_TYPE_COMMON', 'common');
+
+/**
+ * Connection is usefull when you need to organize some sorts of connections between different content,
+ * for example: friends, contacts, favorites, block lists, subscriptions, etc.
+ *
+ * Two types of connections are supported one way connections (block list, favourites) and mutual (friends).
+ *
+ * For automatic handling of connections (like, add/remove connection in frontend) refer to JS function: @see bx_conn_action()
+ *
+ * @section connection_create Creating the Connection object:
+ *
+ * Step 1:
+ * Add record to 'sys_objects_connection' table:
+ * - object: name of the connection object, in the format: vendor prefix, underscore, module prefix, underscore, internal identifier or nothing;
+ *           for example: bx_blogs_favorites - favorite blogs for users in blogs module.
+ * - table: table name with connections, see step 2
+ * - type: 'one-way' or 'mutual'
+ * - override_class_name: user defined class name which is derived from one of base classes.
+ * - override_class_file: the location of the user defined class, leave it empty if class is located in system folders.
+ *
+ * Step 2:
+ * Create table for connections:
+ * @code
+ * CREATE TABLE `my_sample_connections` (
+ *   `id` int(11) NOT NULL AUTO_INCREMENT,
+ *   `initiator` int(11) NOT NULL,
+ *   `content` int(11) NOT NULL,
+ *   `mutual` tinyint(4) NOT NULL, -- can re removed for one-way connections
+ *   `added` int(10) unsigned NOT NULL,
+ *   PRIMARY KEY (`id`),
+ *   UNIQUE KEY `initiator` (`initiator`,`content`),
+ *   KEY `content` (`content`)
+ * )
+ * @endcode
+ *
+ *
+ * @section example Example of usage
+ *
+ * Check if two profiles are friends:
+ * @code
+ *   $oConnectionFriends = BxDolConnection::getObjectInstance('bx_profiles_friends'); // get friends connections object
+ *   if ($oConnectionFriends) // check if connections is available for using
+ *      echo $oConnectionFriends->isConnected (100, 200, true) ? "100 and 200 are friends" : "100 and 200 aren't friends"; // check if profiles with IDs 100 and 200 have mutual connections
+ * @endcode
+ *
+ * Get mutual content IDs (friends IDs)
+ * @code
+ *   $oConnectionFriends = BxDolConnection::getObjectInstance('bx_profiles_friends'); // get friends connections object
+ *   if ($oConnectionFriends) // check if connections is available for using
+ *       print_r($oConnection->getConnectedContent(100, 1)); // print array of friends IDs of 100's profile
+ * @endcode
+ *
+ */
+class BxDolConnection extends BxDolFactory implements iBxDolFactoryObject
+{
+    protected $_sObject;
+    protected $_aObject;
+    protected $_oQuery;
+    protected $_sType;
+
+    /**
+     * Constructor
+     * @param $aObject array of connection options
+     */
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+
+        $this->_sObject = $aObject['object'];
+        $this->_aObject = $aObject;
+        $this->_aObject['per_page_default'] = 20;
+
+        $this->_sType = $aObject['type'];
+
+        $this->_oQuery = new BxDolConnectionQuery($aObject);
+    }
+
+    /**
+     * Get connection object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    static public function getObjectInstance($sObject)
+    {
+        if (!$sObject)
+            return false;
+
+        if (isset($GLOBALS['bxDolClasses']['BxTemplConnection!'.$sObject]))
+            return $GLOBALS['bxDolClasses']['BxTemplConnection!'.$sObject];
+
+        $aObject = BxDolConnectionQuery::getConnectionObject($sObject);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        $sClass = empty($aObject['override_class_name']) ? 'BxTemplConnection' : $aObject['override_class_name'];
+        if (!empty($aObject['override_class_file']))
+            require_once(BX_DIRECTORY_PATH_ROOT . $aObject['override_class_file']);
+
+        $o = new $sClass($aObject);
+        $o->init();
+
+        return ($GLOBALS['bxDolClasses']['BxTemplConnection!'.$sObject] = $o);
+    }
+
+    /**
+     * Init something here if it's needed.
+     */
+    public function init()
+    {
+    }
+
+    /**
+     * Get connection type.
+     * return BX_CONNECTIONS_TYPE_ONE_WAY or BX_CONNECTIONS_TYPE_MUTUAL
+     */ 
+    public function getType()
+    {
+        return $this->_sType;
+    }
+
+    /**
+     * Get connection table.
+     * return string with table name.
+     */ 
+    public function getTable()
+    {
+        return $this->_aObject['table'];
+    }
+
+    /**
+     * Checks whether connection's Initiator is profile or not.
+     * return boolean.
+     */
+    public function isProfileInitiator()
+    {
+        return (int)$this->_aObject['profile_initiator'] != 0;
+    }
+
+    /**
+     * Checks whether connection's Content is profile or not.
+     * return boolean.
+     */
+    public function isProfileContent()
+    {
+        return (int)$this->_aObject['profile_content'] != 0;
+    }
+
+    /**
+     * Check whether connection between Initiator and Content can be established.
+     */
+    public function checkAllowedConnect ($iInitiator, $iContent, $isPerformAction = false, $isMutual = false, $isInvertResult = false, $isSwap = false, $isCheckExists = true)
+    {
+        $aResult = $this->_checkAllowedConnect($iInitiator, $iContent, $isPerformAction, $isMutual, $isInvertResult, $isSwap, $isCheckExists);
+
+        return $aResult['code'] == 0 ? CHECK_ACTION_RESULT_ALLOWED : $aResult['message'];
+    }
+
+    public function checkAllowedAddConnection ($iInitiator, $iContent, $isPerformAction = false, $isMutual = false, $isInvertResult = false, $isSwap = false, $isCheckExists = true)
+    {
+        $aResult = $this->_checkAllowedConnect($iInitiator, $iContent, $isPerformAction, $isMutual, $isInvertResult, $isSwap, $isCheckExists);
+
+        return $aResult['code'] == 0 || ($aResult['code'] == 4  && $this->_sObject == 'sys_profiles_friends') ? CHECK_ACTION_RESULT_ALLOWED : $aResult['message'];
+    }
+
+    public function checkAllowedRemoveConnection ($iInitiator, $iContent, $isPerformAction = false, $isMutual = false, $isInvertResult = false, $isSwap = false, $isCheckExists = true)
+    {
+        $aResult = $this->_checkAllowedConnect($iInitiator, $iContent, $isPerformAction, $isMutual, $isInvertResult, $isSwap, $isCheckExists);
+
+        return $aResult['code'] == 0 || ($aResult['code'] == 4  && $this->_sObject == 'sys_profiles_friends') ? CHECK_ACTION_RESULT_ALLOWED : $aResult['message'];
+    }
+
+    /**
+     * Add new connection.
+     * @param $iContent content to make connection to, in most cases some content id, or other profile id in case of friends
+     * @return array
+     */
+    public function actionAdd ($iContent = 0, $iInitiator = false)
+    {
+        if (!$iContent)
+            $iContent = bx_process_input($_POST['id'], BX_DATA_INT);
+
+        return $this->_action ($iInitiator ? $iInitiator : bx_get_logged_profile_id(), $iContent, 'addConnection', '_sys_conn_err_connection_already_exists', true);
+    }
+
+    /**
+     * Remove connection. This method is wrapper for @see removeConnection to be called from @see conn.php upon AJAX request to this file.
+     * @param $iContent content to make connection to, in most cases some content id, or other profile id in case of friends
+     * @return array
+     */
+    public function actionRemove ($iContent = 0, $iInitiator = false)
+    {
+        if (!$iContent)
+            $iContent = bx_process_input($_POST['id'], BX_DATA_INT);
+
+        if ($iContent != bx_get_logged_profile_id() && BX_CONNECTIONS_TYPE_MUTUAL == $this->_aObject['type']) {
+            $a = $this->actionReject($iContent, $iInitiator);
+            if (false == $a['err'])
+                return $a;
+        }
+
+        return $this->_action ($iInitiator ? $iInitiator : bx_get_logged_profile_id(), $iContent, 'removeConnection', '_sys_conn_err_connection_does_not_exists', false, true);
+    }
+
+    /**
+     * Reject connection request. This method is wrapper for @see removeConnection to be called from @see conn.php upon AJAX request to this file.
+     * @param $iContent content to make connection to, in most cases some content id, or other profile id in case of friends
+     * @return array
+     */
+    public function actionReject ($iContent = 0, $iInitiator = false)
+    {
+        if (!$iContent)
+            $iContent = bx_process_input($_POST['id'], BX_DATA_INT);
+
+        return $this->_action($iContent, $iInitiator ? $iInitiator : bx_get_logged_profile_id(), 'removeConnection', '_sys_conn_err_connection_does_not_exists', false, true);
+    }
+
+    protected function _action ($iInitiator, $iContent, $sMethod, $sErrorKey, $isMutual = false, $isInvert = false)
+    {
+        bx_import('BxDolLanguages');
+
+        if(!$iContent || !$iInitiator)
+            return ['err' => true, 'msg' => _t('_sys_conn_err_input_data_is_not_defined')];
+
+        $sMethodCheck = 'checkAllowed' . bx_gen_method_name($sMethod);
+        if(($mixedResult = $this->{method_exists($this, $sMethodCheck) ? $sMethodCheck : 'checkAllowedConnect'}($iInitiator, $iContent, false, false, $isInvert)) !== CHECK_ACTION_RESULT_ALLOWED)
+            return ['err' => true, 'msg' => $mixedResult];
+
+        if (!$this->$sMethod((int)$iInitiator, (int)$iContent)) {
+            if ($isMutual && BX_CONNECTIONS_TYPE_MUTUAL == $this->_sType && $this->isConnected((int)$iInitiator, (int)$iContent, false) && !$this->isConnected((int)$iInitiator, (int)$iContent, true))
+                return ['err' => true, 'msg' => _t('_sys_conn_err_connection_is_awaiting_confirmation')];
+
+            return ['err' => true, 'msg' => _t($sErrorKey)];
+        }
+
+        return ['err' => false, 'msg' => _t('_sys_conn_msg_success')];
+    }
+
+    public function outputActionResult ($mixed, $sFormat = 'json')
+    {
+        switch ($sFormat) {
+            case 'html':
+                echo $mixed;
+                break;
+                
+            case 'json':
+            default:
+                header('Content-Type: application/json; charset=utf-8');
+                echo json_encode($mixed);     
+        }
+        exit;
+    }
+
+    /**
+     * Add new connection.
+     * @param $iInitiator initiator of the connection, in most cases some profile id
+     * @param $iContent content to make connection to, in most cases some content id, or other profile id in case of friends
+     * @return true - if connection was added, false - if connection already exists or error occured
+     */
+    public function addConnection ($iInitiator, $iContent, $aParams = array())
+    {
+        $iMutual = 0;
+        $iInitiator = (int)$iInitiator;
+        $iContent = (int)$iContent;
+
+        $aAlertExtras = [
+            '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']);
+        
+        /**
+         * @hooks
+         * @hookdef hook-bx_dol_connection-connection_before_add '{object_name}', 'connection_before_add' - hook before connection was added. Connection params can be overridden
+         * - $unit_name - connection object name
+         * - $action - equals `connection_before_add`
+         * - $object_id - not used
+         * - $sender_id - logged in profile id
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `initiator` - [int] by ref, profile id who created the connection, can be overridden in hook processing
+         *      - `content` - [int] by ref, profile id with whom the connection was created, can be overridden in hook processing
+         *      - `mutual` - [int] by ref, if the relation is mutual or not, can be overridden in hook processing
+         *      - `object` - [object] an instance of relation, @see BxDolConnection
+         * @hook @ref hook-bx_dol_connection-connection_before_add
+         */
+        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;
+
+        /**
+         * @hooks
+         * @hookdef hook-bx_dol_connection-connection_added '{object_name}', 'connection_added' - hook after connection was added. Connection params can be overridden
+         * It's equivalent to @ref hook-bx_dol_connection-connection_before_add
+         * @hook @ref hook-bx_dol_connection-connection_added
+         */
+        bx_alert($this->_sObject, 'connection_added', 0, bx_get_logged_profile_id(), $aAlertExtras);
+
+        $this->onAdded($iInitiator, $iContent, $iMutual);
+
+        return true;
+    }
+
+    public function onAdded($iInitiator, $iContent, $iMutual)
+    {
+        $this->checkAllowedConnect ($iInitiator, $iContent, true, $iMutual, false);
+
+        $bMutual = false;
+        
+        /**
+         * Call socket.
+         */
+        if(($oSockets = BxDolSockets::getInstance()) && $oSockets->isEnabled()){
+            $aMessageInitiator = $aMessageContent = [
+                'object' => $this->_sObject, 
+                'action' => 'added',
+            ];
+
+            if(bx_is_api()) {
+                $aMessageInitiator = array_merge($aMessageInitiator, [
+                    'user' => BxDolProfile::getDataForPage($iInitiator)
+                ]);
+
+                $aMessageContent = array_merge($aMessageContent, [
+                    'user' => BxDolProfile::getDataForPage($iContent)
+                ]);
+            }
+
+            $oSockets->sendEvent('sys_connections', $iInitiator , 'changed', json_encode($aMessageInitiator));
+            $oSockets->sendEvent('sys_connections', $iContent , 'changed', json_encode($aMessageContent));
+        }
+        
+        if($this->_aObject['type'] == BX_CONNECTIONS_TYPE_ONE_WAY || ($bMutual = ($this->_aObject['type'] == BX_CONNECTIONS_TYPE_MUTUAL && $iMutual))) {
+            $oProfileQuery = BxDolProfileQuery::getInstance();
+
+            /**
+             * Update recommendations.
+             */
+            if($this->_aObject['profile_initiator']) {
+                $aInitiator = $oProfileQuery->getInfoById($iInitiator);
+                if(bx_srv($aInitiator['type'], 'act_as_profile'))
+                    BxDolRecommendation::updateData($iInitiator);
+            }
+
+            if($bMutual && $this->_aObject['profile_content']) {
+                $aContent = $oProfileQuery->getInfoById($iContent);
+                if(bx_srv($aContent['type'], 'act_as_profile'))
+                    BxDolRecommendation::updateData($iContent);
+            }
+
+            
+        }
+    }
+
+    /**
+     * Remove connection.
+     * @param $iInitiator initiator of the connection
+     * @param $iContent connected content or other profile id in case of friends
+     * @return true - if connection was removed, false - if connection isn't exist or error occured
+     */
+    public function removeConnection ($iInitiator, $iContent)
+    {
+        if (!($aConnection = $this->_oQuery->getConnection((int)$iInitiator, (int)$iContent))) // connection doesn't exist
+            return false;
+
+        if (!$this->_oQuery->removeConnection((int)$iInitiator, (int)$iContent))
+            return false;
+
+        /**
+         * @hooks
+         * @hookdef hook-bx_dol_connection-connection_removed '{object_name}', 'connection_removed' - hook after a connection was removed.
+         * - $unit_name - connection object name
+         * - $action - equals `connection_removed`
+         * - $object_id - not used
+         * - $sender_id - logged in profile id
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `initiator` - [int] profile id who created the connection
+         *      - `content` - [int] profile id with whom the connection was created
+         *      - `mutual` - [int] if the relation is mutual or not
+         *      - `object` - [object] an instance of relation, @see BxDolConnection
+         * @hook @ref hook-bx_dol_connection-connection_removed
+         */
+        bx_alert($this->_sObject, 'connection_removed', 0, bx_get_logged_profile_id(), array(
+            'initiator' => (int)$iInitiator,
+            'content' => (int)$iContent,
+            'mutual' => isset($aConnection['mutual']) ? $aConnection['mutual'] : 0,
+            'object' => $this,
+        ));
+
+        $this->onRemoved($iInitiator, $iContent);
+
+        return true;
+    }
+
+    /**
+     * TODO: Improve - add $iMutual like in onAdded
+     */
+    public function onRemoved($iInitiator, $iContent)
+    {
+        /**
+         * Call socket.
+         */
+        if(($oSockets = BxDolSockets::getInstance()) && $oSockets->isEnabled()){
+            $aMessageInitiator = $aMessageContent = [
+                'object' => $this->_sObject, 
+                'action' => 'deleted',
+            ];
+
+            if(bx_is_api()) {
+                $aMessageInitiator = array_merge($aMessageInitiator, [
+                    'user' => BxDolProfile::getDataForPage($iInitiator)
+                ]);
+
+                $aMessageContent = array_merge($aMessageContent, [
+                    'user' => BxDolProfile::getDataForPage($iContent)
+                ]);
+            }
+
+            $oSockets->sendEvent('sys_connections', $iInitiator , 'changed', json_encode($aMessageInitiator));
+            $oSockets->sendEvent('sys_connections', $iContent , 'changed', json_encode($aMessageContent));
+        }
+    }
+
+    /**
+     * Compound function, which calls getCommonContent, getConnectedContent or getConnectedInitiators depending on $sContentType
+     * @param $sContentType content type to get BX_CONNECTIONS_CONTENT_TYPE_CONTENT, BX_CONNECTIONS_CONTENT_TYPE_INITIATORS or BX_CONNECTIONS_CONTENT_TYPE_COMMON
+     * @param $iId1 one content or initiator
+     * @param $iId2 second content or initiator only in case of BX_CONNECTIONS_CONTENT_TYPE_COMMON content type
+     * @param $isMutual get mutual connections only
+     * @return array of available connections
+     */
+    public function getConnectionsAsArray ($sContentType, $iId1, $iId2, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        if (BX_CONNECTIONS_CONTENT_TYPE_COMMON == $sContentType)
+            return $this->getCommonContent($iId1, $iId2, $isMutual, $iStart, $iLimit, $iOrder);
+
+        if (BX_CONNECTIONS_CONTENT_TYPE_INITIATORS == $sContentType)
+            $sMethod = 'getConnectedInitiators';
+        else
+            $sMethod = 'getConnectedContent';
+
+        return $this->$sMethod($iId1, $isMutual, $iStart, $iLimit, $iOrder);
+    }
+
+    /**
+     * Get common content IDs between two initiators
+     * @param $iInitiator1 one initiator
+     * @param $iInitiator2 second initiator
+     * @param $isMutual get mutual connections only
+     * @return array of available connections
+     */
+    public function getCommonContent ($iInitiator1, $iInitiator2, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        return $this->_oQuery->getCommonContent($iInitiator1, $iInitiator2, $isMutual, $iStart, $iLimit, $iOrder);
+    }
+    
+    /**
+     * Get common content count between two initiators
+     * @param $iInitiator1 one initiator
+     * @param $iInitiator2 second initiator
+     * @param $isMutual get mutual connections only
+     * @return number of connections
+     */
+    public function getCommonContentCount ($iInitiator1, $iInitiator2, $isMutual = false)
+    {
+        return $this->_oQuery->getCommonContentCount($iInitiator1, $iInitiator2, $isMutual);
+    }
+
+    /**
+     * Get connected content count
+     * @param $iInitiator initiator of the connection
+     * @param $isMutual get mutual connections only
+     * @return number of connections
+     */
+    public function getConnectedContentCount ($iInitiator, $isMutual = false, $iFromDate = 0)
+    {
+        return $this->_oQuery->getConnectedContentCount($iInitiator, $isMutual, $iFromDate);
+    }
+
+    /**
+     * Get connected content count
+     * @param $iInitiator initiator of the connection
+     * @param $isMutual get mutual connections only
+     * @param $aParams additional params
+     * @return number of connections
+     */
+    public function getConnectedContentCountExt ($iInitiator, $isMutual = false, $aParams = [])
+    {
+        return $this->_oQuery->getConnectedContentCountExt($iInitiator, $isMutual, $aParams);
+    }
+
+    /**
+     * 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 content IDs for specified type
+     * @param $iInitiator initiator of the connection
+     * @param $iDate get connections appeared since this date 
+     * @param $isMutual get mutual connections only
+     * @return array of available connections
+     */
+    public function getConnectedContentSince ($iInitiator, $iDate, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        return $this->_oQuery->getConnectedContentSince($iInitiator, $iDate, $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);
+    }
+
+    /**
+     * Get connected initiators IDs
+     * @param $iContent content of the connection
+     * @param $iDate get connections appeared since this date 
+     * @param $isMutual get mutual connections only
+     * @return array of available connections
+     */
+    public function getConnectedInitiatorsSince ($iContent, $iDate, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        return $this->_oQuery->getConnectedInitiatorsSince($iContent, $iDate, $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;
+
+        /**
+         * @hooks
+         * @hookdef hook-bx_dol_connection-connection_removed_all '{object_name}', 'connection_removed_all' - hook after all connections with deleted 'initiator' were removed.
+         * - $unit_name - connection object name
+         * - $action - equals `connection_removed_all`
+         * - $object_id - not used
+         * - $sender_id - logged in profile id
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `initiator` - [int] profile id who created the connection
+         *      - `object` - [object] an instance of relation, @see BxDolConnection
+         * @hook @ref hook-bx_dol_connection-connection_removed_all
+         */
+        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;
+
+        /**
+         * @hooks
+         * @hookdef hook-bx_dol_connection-connection_removed_all '{object_name}', 'connection_removed_all' - hook after all connections with deleted 'content' were removed.
+         * - $unit_name - connection object name
+         * - $action - equals `connection_removed_all`
+         * - $object_id - not used
+         * - $sender_id - logged in profile id
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `content` - [int] profile id with whom the connection was created
+         *      - `object` - [object] an instance of relation, @see BxDolConnection
+         * @hook @ref hook-bx_dol_connection-connection_removed_all
+         */
+        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');
+    }
+
+    protected function _checkAllowedConnect ($iInitiator, $iContent, $isPerformAction = false, $isMutual = false, $isInvertResult = false, $isSwap = false, $isCheckExists = true)
+    {
+        $sErr = _t('_sys_txt_access_denied');
+
+        if(!$iInitiator || !$iContent || $iInitiator == $iContent)
+            return ['code' => 1, 'message' => $sErr];
+
+        $oInitiator = BxDolProfile::getInstance($iInitiator);
+        $oContent = BxDolProfile::getInstance($iContent);
+        if(!$oInitiator || !$oContent)
+            return ['code' => 2, 'message' => $sErr];
+
+        // check ACL
+        if(($mixedResult = $this->_checkAllowedConnectInitiator($oInitiator, $isPerformAction)) !== CHECK_ACTION_RESULT_ALLOWED)
+            return ['code' => 3, 'message' => $mixedResult];
+
+        $iCode = 0;
+        $sMessage = '';
+
+        // check content's visibility
+        if(!$this->isConnected($iContent, $iInitiator) && ($mixedResult = $this->_checkAllowedConnectContent($oContent)) !== CHECK_ACTION_RESULT_ALLOWED)
+            list($iCode, $sMessage) = [4, $mixedResult];
+
+        if(!$isCheckExists)
+            return ['code' => $iCode, 'message' => $sMessage != '' ? $sMessage : null];
+
+        if($isSwap)
+            $isConnected = $this->isConnected($iContent, $iInitiator, $isMutual);
+        else
+            $isConnected = $this->isConnected($iInitiator, $iContent, $isMutual);
+
+        if($isInvertResult)
+            $isConnected = !$isConnected;
+
+        if($isConnected)
+            list($iCode, $sMessage) = [5, $sErr];
+
+        return ['code' => $iCode, 'message' => $sMessage != '' ? $sMessage : null];
+    }
+
+    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();
+    }
+}
+
+/** @} */

+ 446 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolConnectionQuery.php

@@ -0,0 +1,446 @@
+<?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 Connection objects.
+ * @see BxDolConnection
+ */
+class BxDolConnectionQuery extends BxDolDb
+{
+    protected $_aObject;
+    protected $_sTable;
+    protected $_sType;
+
+    public function __construct($aObject)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+        $this->_sTable = $aObject['table'];
+        $this->_sType = $aObject['type'];
+    }
+
+    static public function getConnectionObject ($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_connection` WHERE `object` = ?", $sObject);
+        $aObject = $oDb->getRow($sQuery);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        return $aObject;
+    }
+
+    public function getCommonContentSQLParts ($sContentTable, $sContentField, $iInitiator1, $iInitiator2, $isMutual = false)
+    {
+        $sWhereJoin1 = $this->prepareAsString(" AND `c`.`initiator` = ?", $iInitiator1);
+        $sWhereJoin2 = $this->prepareAsString(" AND `c2`.`initiator` = ?", $iInitiator2);
+        if (false !== $isMutual) {
+            $sWhereJoin1 .= $this->prepareAsString(" AND `c`.`mutual` = ?", $isMutual);
+            $sWhereJoin2 .= $this->prepareAsString(" AND `c2`.`mutual` = ?", $isMutual);
+        }
+
+        $sJoin = "";
+        if($this->_aObject['profile_content'])
+            $sJoin = " INNER JOIN `sys_profiles` AS `cp1` ON (`cp1`.`id` = `c`.`content` AND `cp1`.`status` = 'active') INNER JOIN `sys_profiles` AS `cp2` ON (`cp2`.`id` = `c2`.`content` AND `cp2`.`status` = 'active')";
+
+        return [
+            'join' => "INNER JOIN `{$this->_sTable}` AS `c` ON (`c`.`content` = `$sContentTable`.`$sContentField` $sWhereJoin1) INNER JOIN `{$this->_sTable}` AS `c2` ON (`c2`.`content` = `c`.`content` $sWhereJoin2)" . $sJoin,
+        ];
+    }
+
+    public function getConnectedContentSQLParts ($sContentTable, $sContentField, $iInitiator, $isMutual = false)
+    {
+        $aResult = $this->getConnectedContentSQLPartsExt($sContentTable, $sContentField, $iInitiator, $isMutual);
+
+        $aFields = [];
+        foreach($aResult['fields'] as $sFieldAlias => $aField)
+            $aFields[$sFieldAlias] = "`" . $aField['table_alias'] . "`.`" . $aField['name'] . "`";
+
+        $sJoin = "";
+        if($this->_aObject['profile_content']) 
+            $sJoin = " INNER JOIN `sys_profiles` AS `cp` ON (`cp`.`id` = `{$aResult['join']['table_alias']}`.`content` AND `cp`.`status` = 'active')";
+
+        return [
+            'fields' => $aFields,
+            'join' => $aResult['join']['type'] . " JOIN `" . $aResult['join']['table'] . "` AS `" . $aResult['join']['table_alias'] . "` ON (" . $aResult['join']['condition'] . ")" . $sJoin,
+        ];
+    }
+
+    public function getConnectedContentSQLPartsExt ($sContentTable, $sContentField, $iInitiator, $isMutual = false)
+    {
+        $sWhere = $this->prepareAsString(" AND `c`.`initiator` = ?", $iInitiator);
+        if(false !== $isMutual)
+            $sWhere .= $this->prepareAsString(" AND `c`.`mutual` = ?", $isMutual);
+
+        return array(
+            'fields' => array(
+                'added' => array('table_alias' => 'c', 'name' => 'added')
+            ),
+            'join' => array(
+                'type' => 'INNER',
+                'table' => $this->_sTable,
+                'table_alias' => 'c',
+            	'condition' => "`c`.`content` = `" . $sContentTable . "`.`" . $sContentField . "`" . $sWhere
+            )
+        );
+    }
+
+    public function getConnectedContentSQLPartsMultiple ($sContentTable, $sContentField, $sInitiatorTable, $sInitiatorField, $isMutual = false)
+    {
+        $sOn = "";
+        if (false !== $isMutual)
+            $sOn .= $this->prepareAsString(" AND `c`.`mutual` = ?", $isMutual);
+        return array(
+            'join' => "INNER JOIN `{$this->_sTable}` AS `c` ON (`c`.`content` = `$sContentTable`.`$sContentField` $sOn)",
+        	'where' => " AND `c`.`initiator` = `$sInitiatorTable`.`$sInitiatorField`"
+        );
+    }
+
+    public function getConnectedInitiatorsSQLParts ($sInitiatorTable, $sInitiatorField, $iContent, $isMutual = false)
+    {
+        $aResult = $this->getConnectedInitiatorsSQLPartsExt($sInitiatorTable, $sInitiatorField, $iContent, $isMutual);
+
+        $sJoin = '';
+        if($this->_aObject['profile_initiator'])
+            $sJoin = " INNER JOIN `sys_profiles` AS `cp` ON (`cp`.`id` = `{$aResult['join']['table_alias']}`.`initiator` AND `cp`.`status` = 'active')";
+
+        return [
+            'join' => $aResult['join']['type'] . " JOIN `" . $aResult['join']['table'] . "` AS `" . $aResult['join']['table_alias'] . "` ON (" . $aResult['join']['condition'] . ")" . $sJoin,
+        ];
+    }
+
+    public function getConnectedInitiatorsSQLPartsExt ($sInitiatorTable, $sInitiatorField, $iContent, $isMutual = false)
+    {
+        $sWhere = $this->prepareAsString(" AND `c`.`content` = ?", $iContent);
+        if(false !== $isMutual)
+            $sWhere .= $this->prepareAsString(" AND `c`.`mutual` = ?", $isMutual);
+
+        return array(
+            'join' => array(
+                'type' => 'INNER',
+        		'table' => $this->_sTable,
+                'table_alias' => 'c',
+        		'condition' => "`c`.`initiator` = `" . $sInitiatorTable . "`.`" . $sInitiatorField . "`" . $sWhere
+            )
+        );
+    }
+
+    public function getConnectedInitiatorsSQLPartsMultiple ($sInitiatorTable, $sInitiatorField, $sContentTable, $sContentField, $isMutual = false)
+    {
+        $sOn = "";
+        if(false !== $isMutual)
+            $sOn .= $this->prepareAsString(" AND `c`.`mutual` = ?", $isMutual);
+
+        return array(
+            'join' => "INNER JOIN `{$this->_sTable}` AS `c` ON (`c`.`initiator` = `$sInitiatorTable`.`$sInitiatorField` $sOn)",
+            'where' => " AND `c`.`content` = `$sContentTable`.`$sContentField`"
+        );
+    }
+
+    public function getCommonContent($iInitiator1, $iInitiator2, $isMutual, $iStart, $iLimit, $iOrder)
+    {
+        $aBindings = [
+            'initiator1' => $iInitiator1,
+            'initiator2' => $iInitiator2,  
+    	];
+
+        $sWhereJoin = "";
+        if($isMutual !== false) {
+            $aBindings['mutual'] = $isMutual;
+
+            $sWhereJoin = " AND `c2`.`mutual` = :mutual";
+        }
+
+        $sJoin = "INNER JOIN `" . $this->_sTable . "` AS `c2` ON (`c2`.`initiator` = :initiator2 AND `c`.`content` = `c2`.`content` $sWhereJoin)";
+        if($this->_aObject['profile_content'])
+            $sJoin .= "INNER JOIN `sys_profiles` AS `p1` ON (`p1`.`id` = `c`.`content` AND `p1`.`status` = 'active') INNER JOIN `sys_profiles` AS `p2` ON (`p2`.`id` = `c2`.`content` AND `p2`.`status` = 'active')";
+
+        $sWhere = " AND `c`.`initiator` = :initiator1";
+
+        return $this->getColumn($this->_getConnectionsQuery($sWhere, $sJoin, '`c`.`content`', $isMutual, $iStart, $iLimit, $iOrder), $aBindings);
+    }
+
+    public function getConnectedContent ($iInitiator, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        $sWhere = " AND `c`.`initiator` = :initiator";
+
+        $sJoin = $this->_aObject['profile_content'] ? "INNER JOIN `sys_profiles` `p` ON `p`.`id` = `c`.`content` AND `p`.`status` = 'active'" : '';
+
+        $sQuery = $this->_getConnectionsQuery($sWhere, $sJoin, '`c`.`content`', $isMutual, $iStart, $iLimit, $iOrder);
+
+        return $this->getColumn($sQuery, array(
+        	'initiator' => $iInitiator
+        ));
+    }
+    
+    public function getConnectedContentByType ($iInitiator, $mixedType, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        $mixedType = is_array($mixedType) ? $mixedType : array($mixedType);
+
+        $sWhere = " AND `c`.`initiator` = :initiator";
+
+        $sJoin = $this->_aObject['profile_content'] ? 'INNER JOIN `sys_profiles` `p` ON `p`.`id` = `c`.`content` AND `p`.`status` = \'active\' AND `p`.`type` IN (' . $this->implode_escape($mixedType) . ')' : '';
+
+        $sQuery = $this->_getConnectionsQuery($sWhere, $sJoin, '`c`.`content`', $isMutual, $iStart, $iLimit, $iOrder);
+
+        return $this->getColumn($sQuery, array(
+            'initiator' => $iInitiator,
+        ));
+    }
+    
+    public function getConnectedContentSince ($iInitiator, $iDate, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        $sWhere = " AND `c`.`initiator` = :initiator AND `c`.`added` >= :date";
+
+        $sJoin = $this->_aObject['profile_content'] ? 'INNER JOIN `sys_profiles` `p` ON `p`.`id` = `c`.`content` AND `p`.`status` = \'active\'' : '';
+
+        $sQuery = $this->_getConnectionsQuery($sWhere, $sJoin, '`c`.`content`', $isMutual, $iStart, $iLimit, $iOrder);
+
+        return $this->getColumn($sQuery, [
+            'initiator' => $iInitiator,
+            'date' => $iDate
+        ]);
+    }
+
+    public function getConnectedInitiators ($iContent, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        $sWhere = " AND `c`.`content` = :content";
+
+        $sJoin = $this->_aObject['profile_initiator'] ? 'INNER JOIN `sys_profiles` `p` ON `p`.`id` = `c`.`initiator` AND `p`.`status` = \'active\'' : ''; 
+
+        $sQuery = $this->_getConnectionsQuery($sWhere, $sJoin, '`c`.`initiator`', $isMutual, $iStart, $iLimit, $iOrder);
+
+        return $this->getColumn($sQuery, array(
+            'content' => $iContent
+        ));
+    }
+
+    public function getConnectedInitiatorsByType ($iContent, $mixedType, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        $mixedType = is_array($mixedType) ? $mixedType : array($mixedType);
+
+        $sWhere = " AND `c`.`content` = :content";
+
+        $sJoin = $this->_aObject['profile_initiator'] ? 'INNER JOIN `sys_profiles` `p` ON `p`.`id` = `c`.`initiator` AND `p`.`status` = \'active\' AND `p`.`type` IN (' . $this->implode_escape($mixedType) . ')' : ''; 
+
+        $sQuery = $this->_getConnectionsQuery($sWhere, $sJoin, '`c`.`initiator`', $isMutual, $iStart, $iLimit, $iOrder);
+
+        return $this->getColumn($sQuery, array(
+            'content' => $iContent,
+        ));
+    }
+
+    public function getConnectedInitiatorsSince ($iContent, $iDate, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        $sWhere = " AND `c`.`content` = :content AND `c`.`added` >= :date";
+
+        $sJoin = $this->_aObject['profile_initiator'] ? 'INNER JOIN `sys_profiles` `p` ON `p`.`id` = `c`.`initiator` AND `p`.`status` = \'active\'' : ''; 
+
+        $sQuery = $this->_getConnectionsQuery($sWhere, $sJoin, '`c`.`initiator`', $isMutual, $iStart, $iLimit, $iOrder);
+
+        return $this->getColumn($sQuery, [
+            'content' => $iContent,
+            'date' => $iDate
+        ]);
+    }
+
+    protected function _getConnectionsQuery ($sWhere, $sJoin = '', $sFields = '*', $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        $sOrder = $this->_getOrderClause($iOrder, '`c`');
+
+        $sWhere .= (false !== $isMutual) ? $this->prepareAsString(" AND `c`.`mutual` = ?", $isMutual) : '';
+
+        $sLimit = "";
+        if($iLimit != BX_CONNECTIONS_LIST_NO_LIMIT)
+            $sLimit = $this->prepareAsString("LIMIT ?, ?", $iStart, $iLimit);
+
+        return "SELECT $sFields FROM `" . $this->_sTable . "` AS `c` $sJoin WHERE 1 $sWhere $sOrder $sLimit";
+    }
+
+    public function getCommonContentCount($iInitiator1, $iInitiator2, $isMutual)
+    {
+        $aBindings = [
+            'initiator1' => $iInitiator1,
+            'initiator2' => $iInitiator2
+        ];
+
+        $sWhereJoin = "";
+        if($isMutual !== false) {
+            $aBindings['mutual'] = $isMutual;
+
+            $sWhereJoin = " AND `c2`.`mutual` = :mutual";
+        }
+
+        $sJoin = "INNER JOIN `" . $this->_sTable . "` AS `c2` ON (`c2`.`initiator` = :initiator2 AND `c`.`content` = `c2`.`content` $sWhereJoin)";
+        if($this->_aObject['profile_content'])
+            $sJoin .= "INNER JOIN `sys_profiles` AS `p1` ON (`p1`.`id` = `c`.`content` AND `p1`.`status` = 'active') INNER JOIN `sys_profiles` AS `p2` ON (`p2`.`id` = `c2`.`content` AND `p2`.`status` = 'active')";
+
+        $sWhere = " AND `c`.`initiator` = :initiator1";
+
+        return $this->getOne($this->_getConnectionsQueryCount($sWhere, $sJoin, $isMutual, '`c`.`content`'), $aBindings);
+    }
+
+    public function getConnectedContentCount ($iInitiator, $isMutual = false, $iFromDate = 0)
+    {
+        return $this->getConnectedContentCountExt($iInitiator, $isMutual, ['from_date' => $iFromDate]);
+    }
+
+    public function getConnectedContentCountExt ($iInitiator, $isMutual = false, $aParams = [])
+    {
+        $sJoin = $this->_aObject['profile_content'] ? 'INNER JOIN `sys_profiles` `p` ON `p`.`id` = `c`.`content` AND `p`.`status` = \'active\'' : ''; 
+        $sWhere = $this->prepareAsString(" AND `c`.`initiator` = ?", $iInitiator);
+        
+        if(isset($aParams['from_date']) && ($iFromDate = (int)$aParams['from_date']) > 0)
+            $sWhere .= $this->prepareAsString(" AND `c`.`added` > ?", $iFromDate);
+        
+        if(!empty($aParams['by_type']) && is_array($aParams['by_type']) && $this->_aObject['profile_content'])
+            $sWhere .= " AND `p`.`type` IN (" . $this->implode_escape($aParams['by_type']) . ")";
+
+        $sQuery = $this->_getConnectionsQueryCount($sWhere, $sJoin, $isMutual);
+        return $this->getOne($sQuery);
+    }
+
+    public function getConnectedInitiatorsCount ($iContent, $isMutual = false)
+    {
+        $sJoin = $this->_aObject['profile_initiator'] ? 'INNER JOIN `sys_profiles` `p` ON `p`.`id` = `c`.`initiator` AND `p`.`status` = \'active\'' : ''; 
+        $sWhere = $this->prepareAsString(" AND `c`.`content` = ?", $iContent);
+        $sQuery = $this->_getConnectionsQueryCount($sWhere, $sJoin, $isMutual);
+        return $this->getOne($sQuery);
+    }
+
+    protected function _getConnectionsQueryCount ($sWhere, $sJoin = '', $isMutual = false, $sFields = '`c`.`id`')
+    {
+        $sWhere .= (false !== $isMutual) ? $this->prepareAsString(" AND `c`.`mutual` = ?", $isMutual) : '';
+        return "SELECT COUNT(" . $sFields . ") FROM `" . $this->_sTable . "` AS `c` $sJoin WHERE 1 $sWhere";
+    }
+
+    protected function _getOrderClause ($iOrder = BX_CONNECTIONS_ORDER_NONE, $sTable = '')
+    {
+        if ($sTable)
+            $sTable .= '.';
+
+        $sOrder = '';
+        switch ($iOrder) {
+            case BX_CONNECTIONS_ORDER_ADDED_ASC:
+                $sOrder = "ORDER BY {$sTable}`added` ASC";
+                break;
+            case BX_CONNECTIONS_ORDER_ADDED_DESC:
+                $sOrder = "ORDER BY {$sTable}`added` DESC";
+                break;
+        }
+
+        return $sOrder;
+    }
+
+    public function getConnection ($iInitiator, $iContent)
+    {
+        $sQuery = $this->prepare("SELECT * FROM `" . $this->_sTable . "` WHERE `initiator` = ? AND `content` = ?", $iInitiator, $iContent);
+        return $this->fromMemory('BxDolConnectionQuery::getConnection' . $this->_sTable . $iInitiator . '_' . $iContent, 'getRow', $sQuery);
+    }
+
+    public function getConnectionById ($iId)
+    {
+        $sQuery = $this->prepare("SELECT * FROM `" . $this->_sTable . "` WHERE `id` = ?", $iId);
+        return $this->fromMemory('BxDolConnectionQuery::getConnectionById' . $this->_sTable . $iId, 'getRow', $sQuery);
+    }
+
+    public function addConnection ($iInitiator, $iContent, &$iMutualParam = null)
+    {
+        if ($this->getConnection($iInitiator, $iContent)) // connection already exists
+            return false;
+
+		$aBindings = array();
+
+        $iMutual = 0;
+        $sMutualField = '';
+        if (BX_CONNECTIONS_TYPE_MUTUAL == $this->_sType) {
+            $aConnectionMutual = $this->getConnection($iContent, $iInitiator);
+            $iMutual = $aConnectionMutual ? 1 : 0;
+
+            $sMutualField = ", `mutual` = :mutual";
+            $aBindings['mutual'] = $iMutual;
+        }
+
+        $sQuery = $this->prepare("INSERT IGNORE INTO `" . $this->_sTable . "` SET `initiator` = :initiator, `content` = :content, `added` = :added" . $sMutualField);
+        if (!$this->query($sQuery, array_merge($aBindings, array('initiator' => $iInitiator, 'content' => $iContent, 'added' => time()))))
+            return false;
+
+        if ($iMutual) // in case of mutual connection update 'mutual' field
+            $this->updateConnectionMutual($iContent, $iInitiator, $iMutual);
+
+        if (null !== $iMutualParam)
+            $iMutualParam = $iMutual;
+
+        $this->cleanMemory('BxDolConnectionQuery::getConnection' . $this->_sTable . $iInitiator . '_' . $iContent);
+            
+        return true;
+    }
+
+    public function updateConnection ($iInitiator, $iContent, $aSet)
+    {
+        if(empty($aSet) || !is_array($aSet))
+            return false;
+
+        $sQuery = $this->prepare("UPDATE `" . $this->_sTable . "` SET " . $this->arrayToSQL($aSet) . " WHERE `initiator` = ? AND `content` = ?", $iInitiator, $iContent);
+        if ($bResult = $this->query($sQuery))
+            $this->cleanMemory('BxDolConnectionQuery::getConnection' . $this->_sTable . $iInitiator . '_' . $iContent);
+
+        return $bResult;
+    }
+
+    public function updateConnectionMutual ($iInitiator, $iContent, $iMutual)
+    {
+        $sQuery = $this->prepare("UPDATE `" . $this->_sTable . "` SET `mutual` = ? WHERE `initiator` = ? AND `content` = ?", $iMutual, $iInitiator, $iContent);
+        if ($bResult = $this->query($sQuery))
+            $this->cleanMemory('BxDolConnectionQuery::getConnection' . $this->_sTable . $iInitiator . '_' . $iContent);
+        return $bResult;
+    }
+
+    public function removeConnection ($iInitiator, $iContent)
+    {
+        if (!($aConnection = $this->getConnection($iInitiator, $iContent))) // connection doesn't exist
+            return true;
+
+        $sQuery = $this->prepare("DELETE FROM `" . $this->_sTable . "` WHERE `initiator` = ? AND `content` = ?", $iInitiator, $iContent);
+        if (!$this->res($sQuery))
+            return false;
+
+        $this->cleanMemory('BxDolConnectionQuery::getConnection' . $this->_sTable . $iInitiator . '_' . $iContent);
+        
+        if (BX_CONNECTIONS_TYPE_MUTUAL == $this->_sType && $aConnection['mutual'])
+            $this->removeConnection($iContent, $iInitiator);
+
+        return true;
+    }
+
+    public function onDelete ($iId, $sField = 'initiator')
+    {
+        $sQuery = $this->prepare("DELETE FROM `{$this->_sTable}` WHERE `$sField` = ?", $iId);
+        return $this->query($sQuery);
+    }
+
+    public function onModuleDelete ($sTable, $sFieldId, $sField = 'initiator')
+    {
+        return $this->onModuleDeleteCustom ($sTable, $sFieldId, $sField);
+    }
+
+    public function onModuleProfileDelete ($sModuleName, $sField = 'initiator')
+    {
+        return $this->onModuleDeleteCustom ('sys_profiles', 'id', $sField, $this->prepareAsString(" AND `sys_profiles`.`type` = ? ", $sModuleName));
+    }
+
+    protected function onModuleDeleteCustom ($sTable, $sFieldId, $sField = 'initiator', $sWhere = '')
+    {
+        $sQuery = $this->prepare("DELETE `" . $this->_sTable . "` FROM `" . $this->_sTable . "` INNER JOIN `{$sTable}` WHERE `" . $this->_sTable . "`.`$sField` = `{$sTable}`.`{$sFieldId}` " . $sWhere);
+        return $this->query($sQuery);
+    }
+
+}
+
+/** @} */

+ 1353 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolDb.php

@@ -0,0 +1,1353 @@
+<?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 '" . (defined('BX_DATABASE_COLLATE') ? BX_DATABASE_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]);
+
+        exit(255);
+    }
+
+    /**
+     * 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 (self::$_rLink) { // connection errors aren't logged since bx_log required DB connection
+            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")
+            );
+        }
+
+        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_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)
+{
+    if (bx_is_api() && $sParamName == 'permalinks_seo_links')
+        return 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);
+}
+
+/** @} */

+ 739 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolGrid.php

@@ -0,0 +1,739 @@
+<?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
+ * @{
+ */
+
+/**
+ * Grid for any content.
+ *
+ * It allows to display some data as grid with ready to use features:
+ * - paginate
+ * - reordering
+ * - sorting
+ * - search
+ * - actions
+ *
+ * The advantages of the this system:
+ * - Less code to write - so you can concentrate on the main functionality.
+ * - Flexibility - you can turn on/off ready features or override it for the custom behavior.
+ *
+ * Grid is working together with Paginate to look through the data in the grid. @see BxDolPaginate
+ *
+ * @section grid_create Creating the new Grid object:
+ *
+ * 1. add record to 'sys_objects_grid' table:
+ *
+ * - object: name of the grid object, in the format: vendor prefix, underscore, module prefix, underscore, internal identifier or nothing; for example: bx_profiles_admin - to display profiles in admin panel.
+ * - source_type: type of the source data:
+ *      - Sql: the source is SQL query.
+ *      - Array: the source is serialized array.
+ * - source: the source data, different for each 'source_type':
+ *      - Sql: the SQL query string, without ORDER BY and LIMIT clauses, these clauses are added automatically for sorting, pagination and filtering.
+ *      - Array: 2 dimentional serialized array string.
+ * - table: table name (if 'source_type' is 'Sql'), to automatically update order field and delete records.
+ * - field_id: name of the ID field.
+ * - field_order: name of the order field.
+ * - field_active: name of the field which determines if the row is active or disabled. This field is used to display row as disabled (ususally as grayed out).
+ *      The following functions can be overrided for custom behavior:
+ *      - '_switcherChecked2State' and '_switcherState2Checked': override these functions if value of 'field_active' field is different from '0' and '1'
+ *      - '_enable': for custom behavior upon activation/deactivation
+ *      - '_isRowDisabled' and '_isCheckboxDisabled': for displaying disabled rows which is not related to 'field_active' field.
+ *      - '_isSwitcherOn': to display rows which are active/disabled by default.
+ * - paginate_url: URL of the page for the grid, set it empty for AJAX paginate, or specify the url for regular page reloading in paginate, with the following markers:
+ *      - {start}: starting record to show data from.
+ *      - {per_page}: number of records per one page.
+ * - paginate_per_page: number of records per one page.
+ * - paginate_simple: show full or simple paginate, the following values are supported:
+ *      - NULL: show full(big) paginate.
+ *      - ''(empty string): show simple(small) paginate.
+ *      - 'some url': show simple(small) paginate with "View All" link to the specified URL.
+ * - paginate_get_start: GET variable name for 'start'.
+ * - paginate_get_per_page: GET variable name for 'per_page'.
+ * - filter_fields: comma separated list of field names to search in; if field contains language key then it is better to specify it in 'filter_fields_translatable'.
+ * - filter_fields_translatable: comma separated list of field names to search in its translations; enter field name here if field contains language key.
+ * - filter_mode: search mode:
+ *      - like: use SQL LIKE expression for search, if 'source_type' is not 'Sql' it doesn't matter.
+ *      - fulltext: use MATCH ... AGAINST expression for search, if 'source_type' is not 'Sql' it doesn't matter.
+ *      - auto: use 'like' or 'fulltext', depending on 'useLikeOperator' setting option.
+ * - sorting_fields: comma separated field names, which will be allowed for sorting, if field contains language keys, specify it in 'sorting_fields_translatable' field instead.
+ * - sorting_fields_translatable: comma separated field names which need to be sorted by translations, enter field name here if field contains language key.
+ * - 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
+ * - override_class_name: user defined class name which is derived from BxTemplGrid.
+ * - override_class_file: the location of the user defined class, leave it empty if class is located in system folders.
+ *
+ * 2. Specify field names (columns in the grid) in sys_grid_fields table:
+ *
+ * - object: name of the Grid object.
+ * - name: name of the field, it must refer to the SQL field name in the case of 'Sql' 'source_type' or index of the 2 dimentional array in the case of 'Array' 'source_type'.
+ * - title: title of the field, the language key.
+ * - width: width of the column in % or px, pt, etc.
+ * - translatable: if field contains language key and it is needed to display translation for this key - set it to 1, by default 0.
+ * - params: searialized array of additional params:
+ *      - display: display function from BxDolFormCheckerHelper class, for example to convert unix timestamp to the regular date/time string.
+ *      - attr_cell: tag attributes for the data cell.
+ *      - attr_head: tag attributes for the header cell.
+ * - order: order of the field.
+ *
+ * There are some fields which are always available, additionally to the provided set of fields:
+ *
+ * - order: display column as dragable handle, it makes sense if you have data ordered by some field
+ *          and it is specified in field_order, field_id and table fields; reordering is not correctly
+ *          working with paginate, so make sure that paginate_per_page number is big enough to show all records;
+ *          reordering is working with Sql source_type.
+ * - checkbox: display column with checkboxes, so several records can be selected for bulk action;
+ *          you need to specify 'field_id' field, so every checkbox have unique row id;
+ *          you need to specify bulk actions separately in 'sys_grid_actions' table;
+ *          you can override '_isCheckboxSelected' function to display checkbox as checked by default.
+ * - actions: display column with single actions, displayed as buttons; you need to specify field_id field,
+ *          so every action is provided with unique row id; you need to specify single actions separately in sys_grid_actions table.
+ *
+ * 3. Add actions to sys_grid_actions table:
+ *
+ * - object: name of the Grid object.
+ * - type: action type, one of the following:
+ *      - bulk: bulk action, to perform on the set of records, the action is usually displaed below the grid.
+ *      - single: simple action, to perform on one record, the action is usually displayed in the grid row.
+ *      - independent: independent actionm which is not related to any rowm the action is usually displayed above the grid.
+ * - name: action name.
+ * - title: title of the action, the language key.
+ * - icon: display action as icon, title need to be empty in this case.
+ * - confirm: ask confirmation before performing the action, 0 or 1.
+ * - order: order of the action in particular actions set by type.
+ *
+ * Usually you need to handle actions manually, but there are several actions which are available by default:
+ *
+ * - delete: delete the record, it works automatically when 'source_type' is 'Sql' and 'field_id', 'table' fields are specified.
+ *
+ *
+ *
+ * @section grid_display_custom_cell Displaying custom cell
+ *
+ * Cell is displayed with default design. It is possible to easily customize its design by specifying custom attributes as 'attr_cell' in params field in sys_grid_fields table.
+ *
+ * If it is not enough, you can customize it even more by adding the method to your custom class with the following format:
+ * _getCell[field name]
+ * where [field name] is the name of the field you want to have custom look with the capital first letter.
+ *
+ * For example:
+ *
+ * @code
+ * protected function _getCellStatus ($mixedValue, $sKey, $aField, $aRow) {
+ *
+ *     $sAttr = $this->_convertAttrs(
+ *         $aField, 'attr_cell',
+ *         false,
+ *         isset($aField['width']) ? 'width:' . $aField['width'] : false // add default styles
+ *     );
+ *     return '<td ' . $sAttr . '><span style="background-color:' . ('Active' == $mixedValue ? '#cfc' : '#fcc') . '">' . $mixedValue . '</span></td>';
+ * }
+ * @endcode
+ *
+ * Above example is displaying user's status using different colors depending on the status value. Please note that you need to convert attributes by adding some default classes or styles if you need.
+ *
+ *
+ *
+ * @section grid_display_custom_header Displaying custom column header
+ *
+ * This is working similar to displaying custom cell. It easily customize its design by specifying custom attributes as 'attr_head' in params field in sys_grid_fields table.
+ * If it is not enough, you can customize it even more by adding the method to your custom class with the following format:
+ * _getCellHeader[field name]
+ * where [field name] is the name of the field you want to have custom look with the capital first letter.
+ *
+ * For example:
+ *
+ * @code
+ * protected function _getCellHeaderStatus ($sKey, $aField) {
+ *     $s = parent::_getCellHeaderDefault($sKey, $aField);
+ *     return preg_replace ('/<th(.*?)>(.*?)<\/th>/', '<th$1><img src="' . BxDolTemplate::getInstance()->getIconUrl('user.png') . '"></th>', $s);
+ * }
+ * @endcode
+ *
+ * The above example replaces column header text with the image.
+ *
+ *
+ *
+ * @section grid_display_custom_action Displaying custom action
+ *
+ * All actions are displayed as buttons. Bulk and independent actions are displaed as big buttons and single actions are displayed as small buttons.
+ *
+ * It is possible to completely customize it by adding the following method to your custom class:
+ * _getAction[action name]
+ * where [action name] is the action name with the capital first letter.
+ *
+ * For example:
+ *
+ * @code
+ * protected function _getActionCustom1 ($sType, $sKey, $a, $isSmall = false) {
+ *     $sAttr = $this->_convertAttrs(
+ *         $a, 'attr',
+ *         'bx-btn bx-def-margin-sec-left' . ($isSmall ? ' bx-btn-small' : '') // add default classes
+ *     );
+ *     return '<button ' . $sAttr . ' onclick="$(this).off(); alert(\'default behaviour is overrided, so the action is not performed\');">' . $a['title'] . '</button>';
+ * }
+ * @endcode
+ *
+ * The above example disables default onclick event and just displays an alert. Please note that you need to convert attributes by adding some default classes or styles if you need.
+ *
+ *
+ *
+ * @section grid_add_action_handler Add action handler
+ *
+ * As it was mentioned earlier only several actions can be handled automatically, all other actions must be processed manually.
+ * To add action handler you need to add method to your custom class with the following format:
+ * performAction[action name]
+ * where [action name] is the action name with the capital first letter.
+ *
+ * For example:
+ *
+ * @code
+ * public function performActionApprove() {
+ *
+ *     $iAffected = 0;
+ *     $aIds = bx_get('ids');
+ *     if (!$aIds || !is_array($aIds)) {
+ *         echoJson(array());
+ *         exit;
+ *     }
+ *
+ *     $aIdsAffected = array ();
+ *     foreach ($aIds as $mixedId) {
+ *         if (!$this->_approve($mixedId))
+ *             continue;
+ *         $aIdsAffected[] = (int)$mixedId;
+ *         $iAffected++;
+ *     }
+ *
+ *     echoJson(array(
+ *         'msg' => $iAffected > 0 ? sprintf("%d profiles successfully activated", $iAffected) : "Profile(s) activation failed",
+ *         'grid' => $this->getCode(false),
+ *         'blink' => $aIdsAffected,
+ *     ));
+ * }
+ *
+ * protected function _approve ($mixedId) {
+ *     $oDb = BxDolDb::getInstance();
+ *     $sTable = $this->_aOptions['table'];
+ *     $sFieldId = $this->_aOptions['field_id'];
+ *     $sQuery = $oDb->prepare("UPDATE `{$sTable}` SET `Status` = 'Active' WHERE `{$sFieldId}` = ?", $mixedId);
+ *     return $oDb->query($sQuery);
+ * }
+ * @endcode
+ *
+ * The action can be used as 'single' or 'bulk', in the case of 'single' action 'ids' array always has one element.
+ *
+ * As the result, action must outputs JSON array, which is done by echoJson function.
+ * The defined indexes in the array determines behavior after action is performed, the following behaviors are supported:
+ *
+ * - msg: display javascript alert message.
+ * - grid: reload grid data with the provided HTML code.
+ * - popup: display popup with the provided HTML code.
+ * - blink: highlight(blink effect) the specified rows, by the ids.
+ *
+ */
+
+class BxDolGrid extends BxDolFactory implements iBxDolFactoryObject, iBxDolReplaceable
+{
+    protected $_bIsApi;
+
+    protected $_aMarkers = array ();
+
+    protected $_sObject;
+    protected $_aOptions;
+
+    protected $_aBrowseParams;
+    protected $_sDefaultSortingOrder = 'ASC';
+    protected $_iTotalCount = 0;
+
+    protected $_bActionCsrfChecking;
+
+    /**
+     * Constructor
+     * @param $aOptions array of grid options
+     */
+    protected function __construct($aOptions)
+    {
+        parent::__construct();
+
+        $this->_bIsApi = bx_is_api();
+
+        $this->_bActionCsrfChecking = true;
+
+        $this->_sObject = $aOptions['object'];
+        $this->_aOptions = $aOptions;
+
+        $sBrowseParams = bx_get('bp');
+        if(!empty($sBrowseParams)) {
+        	$aBrowseParams = bx_process_input(json_decode(urldecode($sBrowseParams), true));
+        	if(!empty($aBrowseParams) && is_array($aBrowseParams))
+            	$this->setBrowseParams($aBrowseParams);
+        }
+    }
+
+    /**
+     * Get grid object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    public static function getObjectInstance($sObject, $oTemplate = false)
+    {
+        if (isset($GLOBALS['bxDolClasses']['BxDolGrid!'.$sObject]))
+            return $GLOBALS['bxDolClasses']['BxDolGrid!'.$sObject];
+
+        $aObject = BxDolGridQuery::getGridObject($sObject);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        $sClass = 'BxTemplGrid';
+        if (!empty($aObject['override_class_name'])) {
+            $sClass = $aObject['override_class_name'];
+            if (!empty($aObject['override_class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aObject['override_class_file']);
+        }
+
+        $o = new $sClass($aObject, $oTemplate);
+
+        if (!$o->_isVisibleGrid($aObject))
+            return false;
+
+        return ($GLOBALS['bxDolClasses']['BxDolGrid!'.$sObject] = $o);
+    }
+
+    public function getObject()
+    {
+        return $this->_sObject;
+    }
+
+    /**
+     * Add replace markers. Curently markers are replaced in 'source' field
+     * @param $a array of markers as key => value
+     * @return true on success or false on error
+     */
+    public function addMarkers ($a)
+    {
+        if (empty($a) || !is_array($a))
+            return false;
+        $this->_aMarkers = array_merge ($this->_aMarkers, $a);
+        return true;
+    }
+
+    public function setBrowseParams($aBrowseParams)
+    {
+    	$this->_aBrowseParams = $aBrowseParams;
+    	$this->_aQueryAppend['bp'] = urlencode(json_encode($this->_aBrowseParams));
+    }
+
+    /**
+     * Replace provided markers in form array
+     * @param $a form description array
+     * @return array where markes are replaced with real values
+     */
+    protected function _replaceMarkers ()
+    {
+        $this->_aOptions['source'] = bx_replace_markers($this->_aOptions['source'], $this->_aMarkers);
+    }
+
+    protected function _getData ($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage)
+    {
+        $sFunc = '_getData' . $this->_aOptions['source_type'];
+        return $this->$sFunc($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage);
+    }
+    
+    protected function _getDataArray ($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage)
+    {
+        if ($this->_aOptions['source'] && !is_array($this->_aOptions['source'])) {
+            $this->_aOptions['source'] = unserialize($this->_aOptions['source']);
+        }
+
+        // apply filter
+        if ($sFilter && (!empty($this->_aOptions['filter_fields']) || !empty($this->_aOptions['filter_fields_translatable']))) {
+            $aSource = array();
+            foreach ($this->_aOptions['source'] as $aRow) {
+                $bFound = false;
+                if (!empty($this->_aOptions['filter_fields'])) {
+                    foreach ($this->_aOptions['filter_fields'] as $sField) {
+                        if (empty($aRow[$sField]) || false === stripos($aRow[$sField], $sFilter))
+                            continue;
+                        $aSource[] = $aRow;
+                        $bFound = true;
+                        break;
+                    }
+                }
+                if (!$bFound && !empty($this->_aOptions['filter_fields_translatable'])) {
+                    foreach ($this->_aOptions['filter_fields_translatable'] as $sField) {
+                        if (empty($aRow[$sField]) || false === stripos(_t($aRow[$sField]), $sFilter))
+                            continue;
+                        $aSource[] = $aRow;
+                        $bFound = true;
+                        break;
+                    }
+                }
+            }
+        } else {
+            $aSource = &$this->_aOptions['source'];
+        }
+
+        // sort
+        $sSortField = false;
+        $iSortDir = 1;
+        if ($sOrderField && ($aSortingFields = $this->_getOrderFields()) && in_array($sOrderField, $aSortingFields)) { // explicit order
+            $sSortField = $sOrderField;
+            $iSortDir = 0 === strcasecmp($sOrderDir, 'desc') ? -1 : 1;
+        } elseif (!empty($this->_aOptions['field_order'])) { // order by "order" field
+            $sSortField = $this->_aOptions['field_order'];
+        }
+
+        if ($sSortField) {
+            $aSourceOrdered = $aSource;
+            $this->_tmpOrderField = $sSortField;
+            $this->_tmpOrderDir = $iSortDir;
+            usort($aSourceOrdered, array($this, '_cmp'));
+        } else {
+            $aSourceOrdered = &$aSource;
+        }
+        
+        // calculate total records count
+        if ($this->_aOptions['show_total_count'] == 1){
+            $this->_iTotalCount =  count($aSourceOrdered);
+        }
+        return array_slice($aSourceOrdered, $iStart, $iPerPage, true);
+    }
+
+   protected function _getDataSql ($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage)
+   {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $this->_aOptions['source'];
+        if (false === stripos($sQuery, ' WHERE '))
+            $sQuery .= " WHERE 1 ";
+
+        $aResults = false;
+        /**
+         * @hooks
+         * @hookdef hook-grid-get_data 'grid', 'get_data' - hook to override the data to be shown in the grid
+         * - $unit_name - equals `grid`
+         * - $action - equals `get_data`
+         * - $object_id - not used
+         * - $sender_id - not used
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `object` - [string] grid object name
+         *      - `options` - [array] grid options array as key&value pairs
+         *      - `markers` - [array] markers array as key&value pairs
+         *      - `filter` - [string] filter value
+         *      - `browse_params` - [array] additional browse params array as key&value pairs
+         *      - `results` - [array] by ref, array of grid rows, where each row is an array of fields values, can be overridden in hook processing
+         * @hook @ref hook-grid-get_data
+         */
+        bx_alert('grid', 'get_data', 0, false, [
+            'object' => $this->_sObject, 
+            'options' => $this->_aOptions, 
+            'markers' => $this->_aMarkers, 
+            'filter' => $sFilter, 
+            'browse_params' => $this->_aBrowseParams, 
+            'results' => &$aResults
+        ]);
+    	if($aResults !== false)
+    	    return $aResults;
+
+        // add filter condition
+        $sQuery = $this->_modifyDataSqlWhereClause($sQuery, $sFilter, $sOrderByFilter);
+
+        // add custom filter condition and order
+        $sOrderByFilter = '';
+        $sQuery .= $this->_getDataSqlWhereClause($sFilter, $sOrderByFilter);
+
+        // calculate total records count
+        if ($this->_aOptions['show_total_count'] == 1){
+             $this->_iTotalCount = $this->_getDataSqlCounter($sQuery, $sFilter);
+        }
+        
+        // add order
+        $sQuery .= $this->_getDataSqlOrderClause ($sOrderByFilter, $sOrderField, $sOrderDir);
+        
+        $sQuery = $sQuery . $oDb->prepareAsString(' LIMIT ?, ?', $iStart, $iPerPage);
+        return $oDb->getAll($sQuery);
+    }
+
+    protected function _getDataSqlCounter($sQuery, $sFilter)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $sQuery = preg_replace("/^SELECT.*FROM/mU", "SELECT COUNT(*) FROM ", $sQuery);
+        if(strpos($sQuery, 'GROUP BY') === false)
+            return $oDb->getOne($sQuery);
+        else
+            return array_sum($oDb->getColumn($sQuery));
+    }
+   
+    protected function _modifyDataSqlWhereClause($sQuery, $sFilter, &$sOrderByFilter)
+    {
+        if(!$sFilter || (empty($this->_aOptions['filter_fields']) && empty($this->_aOptions['filter_fields_translatable']))) 
+            return $sQuery;
+
+        $oDb = BxDolDb::getInstance();
+
+        $sMode = $this->_aOptions['filter_mode'];
+        if($sMode != 'like' && $sMode != 'fulltext')
+            $sMode = getParam('useLikeOperator') ? 'like' : 'fulltext';
+
+        $sCond = '';
+        $sJoin = '';
+        if('like' == $sMode) { // LIKE search
+
+            // condition for regular fields
+            if (!empty($this->_aOptions['filter_fields']))
+                foreach ($this->_aOptions['filter_fields'] as $sField)
+                    $sCond .= $oDb->prepareAsString("`{$sField}` LIKE ? OR ", '%' . $sFilter . '%');
+
+            // condition for translatable fields
+            if (!empty($this->_aOptions['filter_fields_translatable'])) {
+                $sCondFields = '';
+                foreach ($this->_aOptions['filter_fields_translatable'] as $sField)
+                    $sCondFields .= "`k`.`Key` = `{$sField}` OR ";
+
+                $sCondFields = rtrim($sCondFields, ' OR ');
+
+                if ($sCondFields) {
+                    $sJoin .= " INNER JOIN `sys_localization_keys` AS `k` ON " . $sCondFields . " INNER JOIN `sys_localization_strings` AS `s` ON `k`.`ID` = `s`.`IDKey` ";
+                    $sCond .= $oDb->prepareAsString("`s`.`string` LIKE ? ", '%' . $sFilter . '%');
+                }
+            }
+
+            $sCond = rtrim($sCond, ' OR ');
+
+        } 
+        else { // FULLTEXT search
+
+            // condition for regular fields
+            if (!empty($this->_aOptions['filter_fields'])) {
+
+                $sCondFields = '';
+                foreach ($this->_aOptions['filter_fields'] as $sField)
+                    $sCondFields .= "`{$sField}`,";
+
+                $sCondFields = rtrim($sCondFields, ',');
+
+                if ($sCondFields) {
+                    $sCond = $oDb->prepareAsString(" MATCH ($sCondFields) AGAINST (?) ", $sFilter);
+                    $sOrderByFilter = $sCond;
+                    $sCond .= ' > 1 OR ';
+                }
+            }
+
+            // condition for translatable fields
+            if (!empty($this->_aOptions['filter_fields_translatable'])) {
+
+                $sCondFields = '';
+                foreach ($this->_aOptions['filter_fields_translatable'] as $sField)
+                    $sCondFields .= "`k`.`Key` = `{$sField}` OR ";
+
+                $sCondFields = rtrim($sCondFields, ',');
+
+                if ($sCondFields) {
+                    $sJoin .= " INNER JOIN `sys_localization_strings` AS `s` INNER JOIN `sys_localization_keys` AS `k` ON (`k`.`ID` = `s`.`IDKey`) ";
+                    $sCond .= $oDb->prepareAsString(" (MATCH (`s`.`string`) AGAINST (?) AND ($sCondFields)) OR ", $sFilter);
+                }
+            }
+
+            $sCond = rtrim($sCond, ' OR ');
+        }
+
+        /**
+         * @hooks
+         * @hookdef hook-grid-get_data_by_filter 'grid', 'get_data_by_filter' - hook to override the data to be shown in the grid
+         * - $unit_name - equals `grid`
+         * - $action - equals `get_data_by_filter`
+         * - $object_id - not used
+         * - $sender_id - not used
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `object` - [string] grid object name
+         *      - `options` - [array] grid options array as key&value pairs
+         *      - `markers` - [array] markers array as key&value pairs
+         *      - `filter` - [string] filter value
+         *      - `browse_params` - [array] additional browse params array as key&value pairs
+         *      - `conditions` - [string] by ref, 'where' part of SQL query in accordance with provided filter(s), can be overridden in hook processing
+         * @hook @ref hook-grid-get_data_by_filter
+         */
+        bx_alert('grid', 'get_data_by_filter', 0, false, [
+            'object' => $this->_sObject, 
+            'options' => $this->_aOptions, 
+            'markers' => $this->_aMarkers, 
+            'filter' => $sFilter, 
+            'browse_params' => $this->_aBrowseParams, 
+            'query' => &$sQuery,
+            'conditions' => &$sCond,
+            'join' => &$sJoin,
+        ]);
+
+        // add WHERE for searching in translatable fields
+        $sQuery .= $sCond ? ' AND (' . $sCond . ')' : $sCond;
+
+        if (!empty($this->_aOptions['filter_fields_translatable'])) {
+
+            // add JOIN for searching in translatable fields
+            $sQuery = str_replace('WHERE', "$sJoin WHERE", $sQuery);
+
+            // change SELECT * to include table name
+            $sQuery = ltrim($sQuery);
+            if (preg_match("/^SELECT\s\*\sFROM\s`(.*?)`/i", $sQuery)) {
+                $sQuery = preg_replace("/^SELECT\s\*\sFROM\s`(.*?)`/i", "SELECT $1.* FROM `$1`", $sQuery);
+            }
+
+            // add DISTINCT for searching in translatable fields to remove duplicates
+            if (false === mb_stripos($sQuery, 'DISTINCT')) {
+                $sQuery = preg_replace('/^SELECT/i', 'SELECT DISTINCT', $sQuery);
+            }
+        }
+
+        return $sQuery;
+    }
+
+    protected function _getDataSqlWhereClause($sFilter, &$sOrderByFilter)
+    {
+        if(!$sFilter || (empty($this->_aOptions['filter_fields']) && empty($this->_aOptions['filter_fields_translatable']))) 
+            return '';
+
+        /**
+         * @hooks
+         * @hookdef hook-grid-get_data_by_filter 'grid', 'get_data_by_filter' - hook to override the data to be shown in the grid
+         * - $unit_name - equals `grid`
+         * - $action - equals `get_data_by_filter`
+         * - $object_id - not used
+         * - $sender_id - not used
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `object` - [string] grid object name
+         *      - `options` - [array] grid options array as key&value pairs
+         *      - `markers` - [array] markers array as key&value pairs
+         *      - `filter` - [string] filter value
+         *      - `browse_params` - [array] additional browse params array as key&value pairs
+         *      - `conditions` - [string] by ref, 'where' part of SQL query in accordance with provided filter(s), can be overridden in hook processing
+         * @hook @ref hook-grid-get_data_by_filter
+         */
+        bx_alert('grid', 'get_data_by_filter', 0, false, [
+            'object' => $this->_sObject, 
+            'options' => $this->_aOptions, 
+            'markers' => $this->_aMarkers, 
+            'filter' => $sFilter, 
+            'browse_params' => $this->_aBrowseParams, 
+            'conditions' => &$sCond
+        ]);
+
+        return $sCond ? ' AND (' . $sCond . ')' : $sCond;
+    }
+
+    protected function _getDataSqlOrderClause ($sOrderByFilter, $sOrderField, $sOrderDir, $bFieldsOnly = false)
+    {
+        $sOrderClause = '';
+
+        if ($sOrderField && ($aSortingFields = $this->_getOrderFields()) && in_array($sOrderField, $aSortingFields)) { // explicit order
+
+            $sDir = (0 === strcasecmp($sOrderDir, 'desc') ? 'DESC' : 'ASC');
+
+            if (($aSortingFieldsTranslatable = $this->_getOrderFieldsTranslatable()) && in_array($sOrderField, $aSortingFieldsTranslatable)) {
+
+                // translatable fields
+                $iLang = BxDolLanguages::getInstance()->getCurrentLangId();
+                $oDb = BxDolDb::getInstance();
+                $sOrderClause = $oDb->prepareAsString("(SELECT `s`.`string` FROM `sys_localization_strings` AS `s` INNER JOIN `sys_localization_keys` AS `k` ON (`k`.`ID` = `s`.`IDKey`) WHERE `k`.`KEY` = `$sOrderField` AND `s`.`IDLanguage` = ? LIMIT 1) ", $iLang) . $sDir;
+
+            } else {
+
+                // regular fields
+                $sOrderClause = "`" . $sOrderField . "` $sDir";
+
+            }
+
+        } elseif ($sOrderByFilter) { // order by filter
+
+            $sOrderClause = $sOrderByFilter . " DESC";
+
+        } elseif (!empty($this->_aOptions['field_order'])) { // order by "order" field
+
+            if (false == strpos($this->_aOptions['field_order'], ',')) {
+                $sOrderClause = "`" . $this->_aOptions['field_order'] . "` " . $this->_sDefaultSortingOrder;
+            } else {
+                $a = explode(',', $this->_aOptions['field_order']);
+                foreach ($a as $sField)
+                    $sOrderClause .= "`" . trim($sField) . "` " . $this->_sDefaultSortingOrder . ", ";
+
+                if ($sOrderClause)
+                    $sOrderClause = trim($sOrderClause, ', ');
+            }
+
+        }
+
+        return $bFieldsOnly || empty($sOrderClause) ? $sOrderClause : " ORDER BY " . $sOrderClause;
+    }
+
+    protected function _getCellData($sKey, $aField, $aRow)
+    {
+        if (isset($aRow[$sKey])) {
+            if (!empty($aField['display'])) {
+                bx_import('BxDolForm');
+                $sDisplayFunc = 'display' . $aField['display'];
+                $oDisplay = new BxDolFormCheckerHelper();
+                return $oDisplay->$sDisplayFunc($aRow[$sKey]);
+            } else {
+                return bx_process_output($aRow[$sKey]);
+            }
+        } else {
+            return _t('_undefined');
+        }
+    }
+
+    protected function _cmp ($r1, $r2)
+    {
+        $iRet = strcasecmp($r1[$this->_tmpOrderField], $r2[$this->_tmpOrderField]);
+        return $iRet ? $this->_tmpOrderDir * $iRet : 0;
+    }
+
+    protected function _genMethodName ($s)
+    {
+        return bx_gen_method_name($s);
+    }
+
+    protected function _isVisibleGrid ($a)
+    {
+        if (isAdmin() || !isset($a['visible_for_levels']))
+            return true;
+        return BxDolAcl::getInstance()->isMemberLevelInSet($a['visible_for_levels']);
+    }
+
+    protected function _getFilterValue()
+    {
+        return bx_unicode_urldecode(bx_process_input(bx_get($this->_aOptions['filter_get'])));
+    }
+
+    protected function _getOrderFields($bTranslatable = false)
+    {
+        $sKey = 'sorting_fields' . ($bTranslatable ? '_translatable' : '');
+        return !empty($this->_aOptions[$sKey]) && is_array($this->_aOptions[$sKey]) ? $this->_aOptions[$sKey] : [];
+    }
+
+    protected function _getOrderFieldsTranslatable()
+    {
+        return $this->_getOrderFields(true);
+    }
+
+    protected function _getOrderValue()
+    {
+        return bx_unicode_urldecode(bx_process_input(bx_get($this->_aOptions['order_get_field'])));
+    }
+
+    public function setActionCsrfChecking($bCsrfChecking)
+    {
+        $this->_bActionCsrfChecking = $bCsrfChecking;
+    }
+
+    public function isActionCsrfCheckingDisabled() {
+        return !$this->_bActionCsrfChecking;
+    }
+}
+
+/** @} */

+ 214 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolGridConnections.php

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

+ 140 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolLogsStorageFolder.php

@@ -0,0 +1,140 @@
+<?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
+ * @{
+ */
+
+class BxDolLogsStorageFolder 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();
+    }
+
+    /**
+     * 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 BxDolLogsStorageFolder();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    /**
+     * Write to log 
+     * @param $oObject logs object 
+     * @param $mixed string or array to log
+     * @return true on success or false on error
+     */
+    public function add($oObject, $mixed)
+    {
+        if (!$mixed)
+            return true;
+
+        $sFile = BX_DIRECTORY_PATH_LOGS . $oObject->getObjectName() . '.log';
+
+        $s = $this->formatLogString($oObject, $mixed);
+
+        $bNewFile = !file_exists($sFile);
+        $bRet = file_put_contents($sFile, $s, FILE_APPEND) ? true : false;
+        if ($bNewFile && $bRet)
+            chmod($sFile, BX_DOL_FILE_RIGHTS);
+        return $bRet;
+    }
+
+    protected function formatLogString($oObject, $mixed, $bIncludeObjectName = false)
+    {
+        $s = date('M d H:i:s');
+
+        if ($bIncludeObjectName)
+            $s .= ' ' . $oObject->getObjectName();
+
+        if (!function_exists('bx_get_logged_profile_id'))
+            require_once(BX_DIRECTORY_PATH_INC . "profiles.inc.php");
+
+        $s .= ' [' . (int)bx_get_logged_profile_id() . '] ';
+
+        if (!empty($_SERVER['REQUEST_URI']))
+            $s .= $_SERVER['REQUEST_URI'];
+        elseif (defined('BX_DOL_CRON_EXECUTE'))
+            $s .= 'CRON';
+        else
+            $s .= 'EMPTY';
+
+        $s .= ' ' . trim(is_array($mixed) ? print_r($mixed, true) : $mixed);
+
+        $s .= "\n";
+
+        return $s;
+    }
+
+    /**
+     * Get logs
+     * @param $oObject logs object
+     * @param $iLines number of lines from the tail
+     * @param $sFilter filter lines which include this phrase
+     * @return array of strings
+     */
+    public function get($oObject, $iLines, $sFilter = false)
+    {
+        $sFile = BX_DIRECTORY_PATH_LOGS . $oObject->getObjectName() . '.log';
+        $iSize = filesize($sFile);
+        if (!($fd = fopen($sFile, 'r+')))
+            return false;
+        $iPos = $iSize;
+        $n = 0;
+        while ($n < $iLines+1 && $iPos > 0) {
+            if (-1 == fseek($fd, $iPos))
+                break;
+            if (false === ($s = fread($fd, 1)))
+                break;
+            if ($s === "\n")
+                ++$n;
+            $iPos--;
+        }
+        $a = array();
+        for ($i = 0; $i < $iLines; $i++) {
+            if (false === ($s = fgets($fd)))
+                break;
+            if (!$sFilter || false !== stripos($s, $sFilter))
+                array_push($a, $s);
+        }
+        fclose($fd);
+        return $a;
+    }
+
+    /**
+     * Check if filtering supported in `get` method
+     */
+    public function isFilterAvail()
+    {
+        return true;
+    }
+
+    /**
+     * Check if `get` method available
+     */
+    public function isGetAvail()
+    {
+        return true;
+    }
+}
+
+/** @} */

+ 527 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolProfileQuery.php

@@ -0,0 +1,527 @@
+<?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
+ * @{
+ */
+
+/**
+ * All queries related to profiles
+ */
+class BxDolProfileQuery extends BxDolDb implements iBxDolSingleton
+{
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolProfileQuery();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    /**
+     * Get all account profiles.
+     * @param  string  $iAccountId account id
+     * @return profile array
+     */
+    public function getProfilesByAccount ($iAccountId)
+    {
+        if (!$iAccountId)
+            return array();
+
+        $oAccountQuery = BxDolAccountQuery::getInstance();
+        $aAccountInfo = $oAccountQuery->getInfoById($iAccountId);
+
+        $sSql = $this->prepare("SELECT * FROM `sys_profiles` WHERE `account_id` = ? ORDER BY `id` = ? DESC", $iAccountId, $aAccountInfo['profile_id']);
+        return $this->getAllWithKey($sSql, 'id');
+    }
+
+    /**
+     * Get profile(s) by params
+     * @param  array   $aParams browse params
+     * @return array with profile(s)
+     */
+    public function getProfiles($aParams)
+    {
+        $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
+
+    	$sFieldsClause = "`tp`.*"; 
+    	$sJoinClause = $sWhereClause = $sGroupClause = "";
+        $sOrderClause = "`tp`.`id` ASC";
+
+    	switch($aParams['type']) {
+            case 'id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = [
+                    'id' => $aParams['id']
+                ];
+
+                $sWhereClause = " AND `tp`.`id`=:id";
+                break;
+
+            case 'active':
+                $sWhereClause = " AND `tp`.`status`='active'";
+
+                if(isset($aParams['types'])) {
+                    if(!is_array($aParams['types']))
+                        $aParams['types'] = [$aParams['types']];
+
+                    $sWhereClause .= " AND `tp`.`type` IN (" . $this->implode_escape($aParams['types']) . ")";
+                }
+                break;
+                
+            case 'id_by_module_content_id':
+                $sFieldsClause = "`tp`.`id`";
+                $sWhereClause = $this->prepareAsString(" AND `tp`.`type`=?", $aParams['module']);
+                
+                if(!is_array($aParams['content_id'])) {
+                    $aMethod['name'] = 'getOne';
+                    $sWhereClause .= $this->prepareAsString(" AND `tp`.`content_id`=?", $aParams['content_id']);
+                }
+                else {
+                    $aMethod['name'] = 'getColumn';
+                    $sWhereClause .= " AND `tp`.`content_id` IN (" . $this->implode_escape($aParams['content_id']) . ")";
+                }
+                break;
+
+            case 'id_to_type':
+                $aMethod['name'] = 'getPairs';
+                $aMethod['params'][1] = 'id';
+                $aMethod['params'][2] = 'type';
+                $sWhereClause = " AND `tp`.`id` IN (" . $this->implode_escape($aParams['ids']) . ")";
+                break;
+
+            case 'all':
+                break;
+    	}
+
+    	$sGroupClause = $sGroupClause ? "GROUP BY " . $sGroupClause : "";
+        $sOrderClause = $sOrderClause ? "ORDER BY " . $sOrderClause : "";
+
+        $aMethod['params'][0] = "SELECT
+            " . $sFieldsClause . "
+            FROM `sys_profiles` AS `tp`" . $sJoinClause . "
+            WHERE 1" . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause;
+
+        return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
+    }
+
+    /**
+     * Get profile by content id, type and account.
+     * @param  string $iAccountId account id
+     * @return array  if aprofile ids, key is profile id
+     */
+    public function getProfileByContentTypeAccount ($iContentId, $sType, $iAccountId)
+    {
+        $sSql = $this->prepare("SELECT * FROM `sys_profiles` WHERE `account_id` = ? AND `type` = ? AND `content_id` = ?", $iAccountId, $sType, $iContentId);
+        return $this->getRow($sSql);
+    }
+
+    /**
+     * Get profile by content id and type.
+     * @param  string $iAccountId account id
+     * @return array  if aprofile ids, key is profile id
+     */
+    public function getProfileByContentAndType ($iContentId, $sType, $bClearCache = false)
+    {
+        if (!$iContentId)
+            return false;
+
+        $sKey = 'BxDolProfileQuery::getProfileByContentAndType' . $iContentId . $sType;
+        if ($bClearCache)
+            $this->cleanMemory($sKey);
+
+        $sSql = $this->prepare("SELECT * FROM `sys_profiles` WHERE `content_id` = ? AND `type` = ?", $iContentId, $sType);        
+        $mixedResult = $this->fromMemory($sKey, 'getRow', $sSql);
+        if (!$mixedResult)
+            $this->cleanMemory($sKey);
+        return $mixedResult;
+    }
+
+    public function getConnectedProfilesByType ($aSqlParts, $sType, $iStart, $iLimit)
+    {
+        if(empty($aSqlParts['join']))
+            return array();
+
+        $aBindings = array();
+
+        $sSelectClause = '';
+        if(!empty($aSqlParts['fields']))
+            foreach($aSqlParts['fields'] as $sName => $sField)
+                $sSelectClause .= ', ' . $sField;
+
+        $sWhereClause = '';
+        if(!empty($sType)) {
+            $sWhereClause = ' AND `sys_profiles`.`type`=:type';
+
+            $aBindings['type'] = $sType;
+        }
+
+        $sOrderClause = '';
+        if(!empty($aSqlParts['fields']['added']))
+            $sOrderClause = ' ORDER BY ' . $aSqlParts['fields']['added'] . ' DESC';
+
+        $sLimitClause = '';
+        if(!empty($iLimit))
+            $sLimitClause = $this->prepareAsString(' LIMIT ?, ?', $iStart, $iLimit);
+
+        return $this->getAllWithKey("SELECT `sys_profiles`.*" . $sSelectClause . " FROM `sys_profiles` " . $aSqlParts['join'] . " WHERE 1" . $sWhereClause . $sOrderClause . $sLimitClause, 'id', $aBindings);
+    }
+
+    /**
+     * 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
+     */
+    public function insertProfile ($iAccountId, $iContentId, $sStatus, $sType = 'system')
+    {
+        $sSql = $this->prepare("INSERT INTO `sys_profiles` SET `account_id` = ?, `type` = ?, `content_id` = ?, `status` = ?", $iAccountId, $sType, $iContentId, $sStatus);
+        if (!$this->query($sSql))
+            return false;
+        $iProfileId = $this->lastId();
+        $this->getCurrentProfileByAccount($iAccountId); // it updates current profile id automatically if it is not defined
+        return $iProfileId;
+    }
+
+    /**
+     * Update profile's account id
+     * @param $iProfileId profile id
+     * @param $iAccountId new Account ID
+     * @return true on success or false otherwise
+     */
+    public function changeAccountId ($iProfileId, $iAccountId)
+    {
+        return $this->_updateField ($iProfileId, 'account_id', $iAccountId);
+    }
+
+    /**
+     * Update profile's status
+     * @param $iProfileId profile id
+     * @param $sStatus profile status
+     * @return true on success or false otherwise
+     */
+    public function changeStatus ($iProfileId, $sStatus)
+    {
+        return $this->_updateField ($iProfileId, 'status', $sStatus);
+    }
+
+    /**
+     * Update profile's content filter value (watch)
+     * @param $iProfileId profile id
+     * @param $iValue bitmask of selected items 
+     * @return true on success or false otherwise
+     */
+    public function changeCfwValue ($iProfileId, $iValue)
+    {
+        return $this->_updateField ($iProfileId, 'cfw_value', $iValue);
+    }
+
+    /**
+     * Update profile's content filter items (watch)
+     * @param $iProfileId profile id
+     * @param $iValue bitmask of available items 
+     * @return true on success or false otherwise
+     */
+    public function changeCfwItems ($iProfileId, $iValue)
+    {
+        return $this->_updateField ($iProfileId, 'cfw_items', $iValue);
+    }
+
+    /**
+     * Update profile's content filter items (use)
+     * @param $iProfileId profile id
+     * @param $iValue bitmask of available items 
+     * @return true on success or false otherwise
+     */
+    public function changeCfuItems ($iProfileId, $iValue)
+    {
+        return $this->_updateField ($iProfileId, 'cfu_items', $iValue);
+    }
+
+    /**
+     * Get current account profile.
+     * @param  string  $iAccountId account id
+     * @return current account's profile id
+     */
+    public function getCurrentProfileByAccount ($iAccountId, $bClearCache = false)
+    {
+        if (!$iAccountId)
+            return false;
+
+        $sKey = 'BxDolProfileQuery::getCurrentProfileByAccount' . $iAccountId;
+        if ($bClearCache)
+            $this->cleanMemory($sKey);
+
+        $sSql = $this->prepare("SELECT `profile_id` FROM `sys_accounts` AS `a` INNER JOIN `sys_profiles` AS `p` ON (`a`.`profile_id` = `p`.`id`) WHERE `a`.`id` = ? LIMIT 1", $iAccountId);        
+        $iProfileId = $this->fromMemory($sKey, 'getOne', $sSql);
+        if (!$iProfileId) {
+            $this->cleanMemory($sKey);
+            $sSql = $this->prepare("SELECT `id` FROM `sys_profiles` WHERE `account_id` = ? ORDER BY FIELD(`type`, 'system', 'bx_organizations', 'bx_persons') DESC LIMIT 1", $iAccountId);
+            $iProfileId = $this->getOne($sSql);
+            if (!$iProfileId)
+                return false;
+
+            if (!BxDolAccountQuery::getInstance()->updateCurrentProfile($iAccountId, $iProfileId))
+                return false;
+        }
+        return $iProfileId;
+    }
+
+    /**
+     * Get profile by specified field name and value.
+     * It is for internal usage only.
+     * Use other funtions to get profile info, like getInfoById, etc.
+     * @param  string $sField database field name
+     * @param  mixed  $sValue database field value
+     * @return array  with porfile info
+     */
+    protected function _getDataByField ($sField, $sValue)
+    {
+        $sSql = $this->prepare("SELECT * FROM `sys_profiles` WHERE `$sField` = ? LIMIT 1", $sValue);
+        return $this->fromMemory('sys_profiles_' . $sField . '_' . $sValue, 'getRow', $sSql);
+    }
+
+    /**
+     * get profile id by id
+     */
+    public function getIdById($iId, $bClearCache = false)
+    {
+        if (!$iId)
+            return false;
+
+        $sKey = 'BxDolProfileQuery::getIdById' . $iId;
+        if ($bClearCache)
+            $this->cleanMemory($sKey);
+        
+        $sSql = $this->prepare("SELECT `id` FROM `sys_profiles` WHERE `id` = ? LIMIT 1", $iId);
+        $mixedResult = $this->fromMemory($sKey, 'getOne', $sSql);
+        if (!$mixedResult)
+            $this->cleanMemory($sKey);
+        return $mixedResult;
+    }
+
+    /**
+     * get account info by profile id
+     */
+    public function getAccountInfoByProfileId($iId)
+    {
+        $sSql = $this->prepare("SELECT `a`.* FROM `sys_accounts` AS `a` INNER JOIN `sys_profiles` AS `p` ON (`p`.`account_id` = `a`.`id`) WHERE `p`.`id` = ? LIMIT 1", $iId);
+        return $this->getRow($sSql);
+    }
+
+    /**
+     * get account email by profile id
+     */
+    public function getEmailById($iId)
+    {
+        $a = $this->getAccountInfoByProfileId($iId);
+        if (!$a || empty($a['email']))
+            return false;
+        return $a['email'];
+    }
+
+    /**
+     * Get profile info by id
+     * @param  int   $iID profile id
+     * @return array with profile info
+     */
+    public function getInfoById( $iID )
+    {
+        return $this->_getDataByField('id', (int)$iID);
+    }
+
+	/**
+     * Is profile online by id
+     * @param  int $iId profile id
+     * @return profile online status
+     */
+    public function isOnline($iId)
+    {
+        $sSql = $this->prepare("SELECT 
+        		`tp`.`id` 
+        	FROM `sys_profiles` AS `tp` 
+        	INNER JOIN `sys_accounts` AS `ta` ON `tp`.`account_id`=`ta`.`id` 
+        	INNER JOIN `sys_sessions` AS `ts` ON `tp`.`account_id`=`ts`.`user_id` 
+        	WHERE 
+        		`tp`.`id` = ? AND 
+        		`ta`.`profile_id`=`tp`.`id` AND 
+        		`ts`.`date` > (UNIX_TIMESTAMP() - 60 * ?) 
+        	LIMIT 1", $iId, (int)getParam('sys_account_online_time'));
+        return (int)$this->getOne($sSql) > 0;
+    }
+    
+    /**
+     * Get online profiles count 
+     * @return online profiles count 
+     */
+    public function getOnlineCount()
+    {
+        $sSql = $this->prepare("SELECT 
+        		COUNT(`tp`.`id`) 
+        	FROM `sys_profiles` AS `tp` 
+        	INNER JOIN `sys_accounts` AS `ta` ON `tp`.`account_id`=`ta`.`id` 
+        	INNER JOIN `sys_sessions` AS `ts` ON `tp`.`account_id`=`ts`.`user_id` 
+        	WHERE 
+        		`ta`.`profile_id`=`tp`.`id` AND 
+        		`ts`.`date` > (UNIX_TIMESTAMP() - 60 * ?)", (int)getParam('sys_account_online_time'));
+        return (int)$this->getOne($sSql);
+    }
+
+    public function getProfileQuota($iProfileId)
+    {
+        $a = ['current_size' => 0, 'current_number' => 0, 'quota_size' => 0, 'quota_number' => 0, 'max_file_size' => 0];
+
+        if ($iProfileId) {
+            $sQuery = $this->prepare("SELECT `current_size`, `current_number`, 0 as `quota_size`, 0 as `quota_number`, 0 as `max_file_size` FROM `sys_storage_user_quotas` WHERE `profile_id` = ?", $iProfileId);
+            $a = $this->getRow($sQuery);
+            if (!is_array($a) || !$a)
+                $a = ['current_size' => 0, 'current_number' => 0, 'quota_size' => 0, 'quota_number' => 0, 'max_file_size' => 0];
+        }
+
+        // get quota_number and quota_size from user's acl/membership
+        $aMembershipInfo = BxDolAcl::getInstance()->getMemberMembershipInfo($iProfileId);
+        if ($aMembershipInfo) {
+            if (isset($aMembershipInfo['quota_size']))
+                $a['quota_size'] = $aMembershipInfo['quota_size'];
+            if (isset($aMembershipInfo['quota_number']))
+                $a['quota_number'] = $aMembershipInfo['quota_number'];
+            if (isset($aMembershipInfo['quota_max_file_size']))
+                $a['max_file_size'] = $aMembershipInfo['quota_max_file_size'];
+        }
+
+        return $a;
+    }
+
+    public function updateProfileQuota($iProfileId, $iSize, $iNumber = 1)
+    {
+        if (!$iProfileId) // for guests and storages without owner don't update per-user quota
+            return true;
+
+        $iTime = time();
+        $sQuery = $this->prepare("
+            INSERT INTO `sys_storage_user_quotas`
+            SET `profile_id` = ?, `current_size` = `current_size` + ?, `current_number` = `current_number` + ?, `ts` = ?
+            ON DUPLICATE KEY UPDATE `current_size` = `current_size` + ?, `current_number` = `current_number` + ?, `ts` = ?",
+            $iProfileId, $iSize, $iNumber, $iTime, $iSize, $iNumber, $iTime
+        );
+        if ($this->query($sQuery))
+            return true;
+        else
+            return false;
+    }
+
+    /**
+     * Get profile field by specified field name and value.
+     * In most cases it is for internal usage only.
+     * Use other funtions to get profile info, like getIdByEmail, etc.
+     * @param  string    $sFieldRequested database field name to return
+     * @param  string    $sFieldSearch    database field name to search for
+     * @param  mixed     $sValue          database field value
+     * @return specified profile field value
+     */
+    protected function _getFieldByField ($sFieldRequested, $sFieldSearch, $sValue)
+    {
+        $sSql = $this->prepare("SELECT `$sFieldRequested` FROM `sys_profiles` WHERE `$sFieldSearch` = ? LIMIT 1", $sValue);
+        return $this->getOne($sSql);
+    }
+
+    /**
+     * Update some field by profile id
+     * In most cases it is for internal usage only.
+     * Use other funtions to get profile info, like updateLogged, etc.
+     * @param  string    $sFieldRequested database field name to return
+     * @param  string    $sFieldSearch    database field name to search for
+     * @param  mixed     $sValue          database field value
+     * @return specified profile field value
+     */
+    protected function _updateField ($iId, $sFieldForUpdate, $sValue)
+    {
+        $sSql = $this->prepare("UPDATE `sys_profiles` SET `$sFieldForUpdate` = ? WHERE `id` = ? LIMIT 1", $sValue, $iId);
+        return $this->query($sSql);
+    }
+
+    /**
+     * Delete profile info by id
+     * @param  int      $iID profile id
+     * @return affected rows
+     */
+    public function delete($iID)
+    {
+        $aInfo = $this->getInfoById($iID);
+        $sSql = $this->prepare("DELETE FROM `sys_profiles` WHERE `id` = ? LIMIT 1", $iID);
+        if ($res = $this->query($sSql)) {
+            $this->getProfileByContentAndType($aInfo['content_id'], $aInfo['type'], true);
+            $this->getCurrentProfileByAccount($aInfo['account_id'], true);
+            $this->getIdById($iID, true);
+        }
+        return $res;
+    }
+
+    /**
+     * Reset deleted profile ids and assign system profile ids.
+     * Should be called after profiles module deletion.
+     * It can be called automatically if 
+     * @code
+     * 'process_deleted_profiles' => 1 
+     * @endcode
+     * is specified in module config.php file in 'uninstall' section.
+     */
+    public function processDeletedProfiles ()
+    {
+        $bResult = true;
+
+        // reset deleted profiles
+        $bResult &= $this->query("UPDATE `sys_accounts` AS `a` LEFT OUTER JOIN `sys_profiles` AS `p` ON `a`.`profile_id`=`p`.`id` SET `a`.`profile_id`='0' WHERE `p`.`id` IS NULL") !== false;
+
+        // try to assign another non-system profile to reset accounts
+        $bResult &= $this->query("UPDATE `sys_accounts` AS `a` INNER JOIN `sys_profiles` AS `p` ON `a`.`id`=`p`.`account_id` AND `p`.`type`<>'system' AND `a`.`profile_id`='0' SET `a`.`profile_id`=`p`.`id`") !== false;
+
+        // assign system profile to reset accounts
+        $bResult &= $this->query("UPDATE `sys_accounts` AS `a` INNER JOIN `sys_profiles` AS `p` ON `a`.`id`=`p`.`account_id` AND `p`.`type`='system' AND `a`.`profile_id`='0' SET `a`.`profile_id`=`p`.`id`") !== false;
+
+        return $bResult;
+    }
+    
+    public function updateProfileTrack($iProfileId, $sAction)
+    {
+        return $this->query("INSERT INTO `sys_profiles_track` SET `profile_id`=:profile_id, `action`=:action, `date`=:now ON DUPLICATE KEY UPDATE `date`=:now", [
+            'profile_id' => $iProfileId,
+            'action' => $sAction,
+            'now' => time()
+        ]);
+    }
+
+    public function getProfileTrack($iProfileId, $sAction)
+    {
+        return (int)$this->getOne("SELECT `date` FROM `sys_profiles_track` WHERE `profile_id`=:profile_id AND `action`=:action LIMIT 1", [
+            'profile_id' => $iProfileId,
+            'action' => $sAction
+        ]);
+    }
+}
+
+/** @} */

+ 69 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolQueuePush.php

@@ -0,0 +1,69 @@
+<?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 BxDolQueuePush extends BxDolQueue implements iBxDolSingleton
+{
+    protected function __construct()
+    {
+        parent::__construct();
+
+        $this->_oQuery = new BxDolQueuePushQuery();
+
+        $this->_sParamTime = 'sys_push_queue_time';
+
+        $this->_iLimitSend = (int)getParam('sys_push_queue_send_per_start');
+        $this->_iLimitSendPerRecipient = (int)getParam('sys_push_queue_send_per_start_to_recipient');
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolQueuePush();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    /**
+     * Add message to push queue
+     *
+     * @param integer $iProfileId - recipient profile ID
+     * @param array $aMessage - array with Push message 
+     * @return true on success, false on error
+     */
+    public function add($iProfileId, $aMessage)
+    {
+        return (int)$this->_oQuery->insertItem(array(
+        	'profile_id' => $iProfileId,
+            'message' => serialize($aMessage)
+        )) > 0;
+    }
+
+    /**
+     * Internal method which performs sending using predefined list of params.
+     */
+    protected function _send($iProfileId, $sMessage)
+    {
+        if(isset($this->_aSentTo[$iProfileId]) && (int)$this->_aSentTo[$iProfileId] >= $this->_iLimitSendPerRecipient)
+            return false;
+
+        if(!BxDolPush::getObjectInstance()->send($iProfileId, unserialize($sMessage)))
+            return false;
+
+        if(!isset($this->_aSentTo[$iProfileId]))
+            $this->_aSentTo[$iProfileId] = 0;
+        $this->_aSentTo[$iProfileId] += 1;
+
+        return true;
+    }
+}
+
+/** @} */

+ 229 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolSearchExtended.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    UnaCore UNA Core
+ * @{
+ */
+
+bx_import('BxDolForm');
+
+class BxDolSearchExtended extends BxDolFactory implements iBxDolFactoryObject
+{
+    public static $SEARCHABLE_TYPES = array(
+        'text', 'textarea', 'number', 'slider',
+        'select', 'radio_set',
+        'checkbox_set', 'select_multiple',
+        'checkbox', 'switcher',
+        'datepicker', 'datetime',
+        'location', 'location_radius'
+    );
+
+    public static $TYPE_TO_TYPE_SEARCH = array(
+        'text' => array('text', 'text_range'),
+        'textarea' => array('text'),
+        'number' => array('text'),
+        'slider' => array('text_range', 'text'),
+        'text_auto' => array('text_auto'),
+        'select' => array('checkbox_set', 'select_multiple', 'select'),
+        'radio_set' => array('checkbox_set', 'select_multiple', 'select'),
+        'checkbox_set' => array('checkbox_set', 'select_multiple', 'select'),
+        'select_multiple' => array('checkbox_set', 'select_multiple', 'select'),
+        'checkbox' => array('checkbox', 'switcher'),
+        'switcher' => array('checkbox', 'switcher'),
+        'datepicker' => array('datepicker_range', 'datepicker_range_age'),
+        'datetime' => array('datetime_range', 'datetime_range_age'),
+        'location' => array('location', 'location_radius')
+    );
+
+    public static $TYPE_TO_OPERATOR = array(
+        'text' => array('like', '=', 'between'),
+        'textarea' => array('like', '='),
+        'number' => array('='),
+        'slider' => array('between', '='),
+        'text_auto' => array('in'),
+        'select' => array('in'),
+        'radio_set' => array('in'),
+        'checkbox_set' => array('and'), 
+        'select_multiple' => array('and'),
+        'checkbox' => array('='),
+        'switcher' => array('='),
+    	'datepicker' => array('between'), 
+    	'datetime' => array('between'),
+        'location' => array('locate')
+    );
+
+    protected $_bIsApi;
+
+    protected $_oDb;
+
+    protected $_sObject;
+    protected $_aObject;
+
+    protected $_bFilterMode;
+
+    /**
+     * Constructor
+     * @param $aObject array of search options
+     */
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+
+        $this->_bIsApi = bx_is_api();
+
+        $this->_oDb = new BxDolSearchExtendedQuery($this->_aObject);
+
+        $this->_sObject = $aObject['object'];
+        $this->_aObject = $aObject;
+
+        $this->_bFilterMode = false;
+        if(isset($this->_aObject['filter']))
+            $this->_bFilterMode = (int)$this->_aObject['filter'] != 0;
+    }
+
+    /**
+     * Get editor object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    static public function getObjectInstance($sObject, $oTemplate = false)
+    {
+        if(isset($GLOBALS['bxDolClasses']['BxDolSearchExtended!' . $sObject]))
+            return $GLOBALS['bxDolClasses']['BxDolSearchExtended!' . $sObject];
+
+        $aObject = BxDolSearchExtendedQuery::getSearchObject($sObject);
+        if(!$aObject || !is_array($aObject))
+            return false;
+
+        $sClass = 'BxTemplSearchExtended';
+        if(!empty($aObject['class_name'])) {
+            $sClass = $aObject['class_name'];
+            if(!empty($aObject['class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aObject['class_file']);
+        }
+
+        $o = new $sClass($aObject, $oTemplate);
+        return ($GLOBALS['bxDolClasses']['BxDolSearchExtended!' . $sObject] = $o);
+    }
+
+    static public function actionGetAuthors()
+    {
+        self::getMention("@");
+    }
+    
+    static public function actionGetHashtags()
+    {
+        self::getMention("#");
+    }
+    
+    static public function actionGetMention()
+    {
+        self::getMention(bx_get('symbol'));
+    }
+    
+    static function getMention($sSymbol)
+    {
+        $bIsApi = bx_is_api();
+
+        $aResult = [];
+        switch($sSymbol) {
+            case '@':
+                $aResult = BxDolService::call('system', 'profiles_search', array(bx_get('term')), 'TemplServiceProfiles');
+                foreach ($aResult as &$aItem) {
+                    $aItem['symbol'] = bx_get('symbol');
+                    $aItem['url'] = $bIsApi ? bx_api_get_relative_url($aItem['url']) : $aItem['url'];
+                }
+                break;
+
+            case '#':
+                $aData = BxDolMetatags::getMetatagsDataByTerm('keywords', 'keyword', bx_get('term'));
+                foreach ($aData as $aItem)
+                    $aResult[] = [
+                        'label' => $aItem['meta'], 
+                        'value' => $aItem['id'], 
+                        'url' => $bIsApi ? bx_api_get_relative_url($aItem['url']) : $aItem['url'], 
+                        'symbol' => bx_get('symbol')
+                    ];
+                break;
+            
+        }
+
+        $a = bx_get_base_url_inline();
+
+        /**
+         * @hooks
+         * @hookdef hook-search-get_mention 'search', 'get_mention' - hook on get mention list
+         * - $unit_name - equals `search`
+         * - $action - equals `get_mention` 
+         * - $object_id - not used 
+         * - $sender_id - not used 
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `params` - [array] array of parameters
+         *      - `override_result` - [array] by ref, array of colors, can be overridden in hook processing
+         * @hook @ref hook-search-get_mention
+         */
+        bx_alert('search', 'get_mention', 0, 0, array('params' => $a[1], 'override_result' => &$aResult));
+        
+        header('Content-Type:text/javascript; charset=utf-8');
+        echo json_encode($aResult);
+    }
+
+    static public function encodeConditions($aConditions) 
+    {
+        return urlencode(base64_encode(json_encode($aConditions)));
+    }
+
+    static public function decodeConditions($sConditions) 
+    {
+        return json_decode(base64_decode(urldecode($sConditions)), true);
+    }
+
+    public function isEnabled()
+    {
+        return isset($this->_aObject['active']) && (int)$this->_aObject['active'] != 0;
+    }
+
+    public function clean()
+    {
+        return $this->cleanFields() & $this->cleanSortableFields();
+    }
+    
+    public function cleanFields()
+    {
+        return $this->_oDb->deleteFields(array('object' => $this->_sObject)) !== false;
+    }
+    
+    public function cleanSortableFields()
+    {
+        return $this->_oDb->deleteSortableFields(array('object' => $this->_sObject)) !== false;
+    }
+
+    public function reset()
+    {   
+        $this->resetFields();
+        $this->resetSortableFields();
+        return true;
+    }
+    
+    public function resetFields()
+    {
+        if(!$this->cleanFields())
+            return false;
+        
+        $this->_aObject['fields'] = BxDolSearchExtendedQuery::getSearchFields($this->_aObject);
+        return true;
+    }
+    
+    public function resetSortableFields()
+    {
+        if(!$this->cleanSortableFields())
+            return false;
+        
+        $this->_aObject['fields'] = BxDolSearchExtendedQuery::getSearchSortableFields($this->_aObject);
+        return true;
+    }
+}
+
+/** @} */

+ 179 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolService.php

@@ -0,0 +1,179 @@
+<?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
+ * @{
+ */
+
+/**
+ * Service calls to modules' methods.
+ *
+ * The class has one static method to make service calls to the module's methods
+ *
+ *
+ * Example of usage:
+ * @code
+ * $isSpam = BxDolService::call('bx_antispam', 'is_spam', array($sText));
+ * @endcode
+ *
+ *
+ * Memberships/ACL:
+ * Doesn't depend on user's membership.
+ *
+ *
+ * Alerts:
+ * no alerts available
+ *
+ */
+class BxDolService extends BxDol
+{
+    static protected $_aMemoryCache = array();
+
+    /**
+     * 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
+     */
+    public static function call($mixed, $sMethod, $aParams = array(), $sClass = 'Module', $bIgnoreCache = false, $bIgnoreInactive = false)
+    {
+        $aModule = self::getModule($mixed);
+        if (empty($aModule) || ((int)$aModule['enabled'] == 0 && !$bIgnoreInactive))
+            return '';
+
+        $sKey = md5($mixed . $sMethod . print_r($aParams, true) . $sClass . bx_get_logged_profile_id());
+        if (!$bIgnoreCache && isset(self::$_aMemoryCache[$sKey]))
+            return self::$_aMemoryCache[$sKey];
+
+        self::$_aMemoryCache[$sKey] = BxDolRequest::processAsService($aModule, $sMethod, array_values($aParams), $sClass);
+
+        return self::$_aMemoryCache[$sKey];
+    }
+
+    /**
+     * Perform serice call by accepting serialized array of service call parameters:
+     * @code
+     *     array (
+     *         'module' => 'system', // module name
+     *         'method' => 'test', // service method name
+     *         'params' => array(), // array of parameters to pass to service method
+     *         'class' => 'Module', // class to search service method in
+     *     )
+     * @endcode
+     *
+     * @param $s serialized array of serice call
+     * @param $aMarkers service method name in format 'method_name', corresponding class metod is serviceMethodName
+     * @param $sReplaceIn params to pass to service method
+     * @return service call result
+     */
+    public static function callSerialized($s, $aMarkers = array(), $sReplaceIn = 'params')
+    {
+        $a = @unserialize($s);
+        if (false === $a || !is_array($a))
+            return '';
+
+        if (isset($a[$sReplaceIn]) && $aMarkers)
+            $a[$sReplaceIn] = bx_replace_markers($a[$sReplaceIn], $aMarkers);
+
+        return self::call($a['module'], $a['method'], isset($a['params']) ? $a['params'] : array(), isset($a['class']) ? $a['class'] : 'Module', isset($a['ignore_cache']) ? $a['ignore_cache'] : false);
+    }
+
+    public static function callMacro($s, $aMarkers = array())
+    {
+        // replace markers
+        if ($aMarkers)
+            $s = bx_replace_markers($s, $aMarkers);
+
+        // check for correct macros
+        if (!preg_match("/^([a-zA-Z0-9_\:]+)(.*)$/", $s, $aMatches))
+            return _t('_sys_macros_malformed');
+
+        $a = explode(":", $aMatches[1]);
+        if (!isset($a[0]) || !isset($a[1]))
+            return _t('_sys_macros_malformed');
+
+        $aParams = array();
+        if (!empty($aMatches[2]))
+            $aParams = json_decode($aMatches[2], true);
+        if (null === $aParams)
+            return _t('_sys_macros_malformed');
+
+        // check if macros is safe
+        if (!self::call($a[0], 'is_safe_service', array($a[1])))
+            return _t('_sys_macros_unsafe');
+
+        // check module, method and number of arguments
+        $aModule = self::getModule($a[0]);
+        if (empty($aModule))
+            return _t('_sys_macros_method_or_class_not_found');
+        $iCheck = BxDolRequest::checkCall($aModule, $a[1], $aParams, isset($a[2]) ? $a[2] : 'Module');
+        switch ($iCheck) {
+            case 1: 
+                return _t('_sys_macros_method_or_class_not_found');
+            case 2:
+                return _t('_sys_macros_required_args_missing');
+            case 3:
+                return _t('_sys_macros_too_many_args');
+        }
+
+        // perform call
+        $mixed = self::call($a[0], $a[1], $aParams, isset($a[2]) ? $a[2] : 'Module', isset($a[3]) && 'ignore_cache' == $a[3] ? true : false);
+
+        // check result
+        if (is_object($mixed))
+            return _t('_sys_macros_output_not_displayable');
+        if (is_array($mixed) && !isset($mixed['content']))
+            return _t('_sys_macros_output_not_displayable');
+        if (is_array($mixed) && isset($mixed['content']))
+            return $mixed['content'];
+        return $mixed;
+    }
+
+    /**
+     * Check if string is serialized array
+     */
+    public static function isSerializedService($s)
+    {
+        return preg_match('/^a:[\d+]:\{/', $s);
+    }
+
+    /**
+     * Serialized service call array
+     */
+    public static function getSerializedService($mixedModule, $sMethod, $aParams = array(), $sClass = '')
+    {
+		$aService = array(
+			'module' => $mixedModule,
+			'method' => $sMethod,
+		);
+
+		if(!empty($aParams))
+			$aService['params'] = $aParams;
+
+		if(!empty($sClass))
+			$aService['class'] = $sClass;
+
+		return serialize($aService);
+    }
+
+    protected static function getModule($mixed)
+    {
+        $oDb = BxDolModuleQuery::getInstance();
+        if (is_string($mixed))
+            $aModule = $oDb->getModuleByName($mixed);
+        else
+            $aModule = $oDb->getModuleById($mixed);
+        return $aModule;
+    }
+
+    public static function cleanMemoryCache()
+    {
+        self::$_aMemoryCache = [];
+    }
+}
+
+/** @} */

+ 125 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolSessionQuery.php

@@ -0,0 +1,125 @@
+<?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
+ * @{
+ */
+
+/**
+ * @see BxDolSession
+ */
+class BxDolSessionQuery extends BxDolDb
+{
+    protected $sTable;
+
+    public function __construct()
+    {
+        parent::__construct();
+
+        $this->sTable = 'sys_sessions';
+    }
+    function getTableName()
+    {
+        return $this->sTable;
+    }
+    function exists($sId)
+    {
+        $aSession = $this->getRow("SELECT `id`, `user_id`, `data` FROM `" . $this->sTable . "` WHERE `id`=:id LIMIT 1", [
+            'id' => (string)$sId
+        ]);
+
+        return !empty($aSession) ? $aSession : false;
+    }
+    function save($sId, $aSet)
+    {
+        $aSet['date'] = time();
+        $aUpdate = $aSet;
+        $aSet['id'] = $sId;
+        $aBind = $aSet;
+        unset($aBind['date']);
+
+        if ($this->getOne("SELECT `date` FROM `" . $this->sTable . "` WHERE `id` = :id AND `data` = :data AND `user_id` = :user_id AND `date` > UNIX_TIMESTAMP() - " . BX_DOL_SESSION_SKIP_UPDATE, $aBind))
+            return true;
+
+        $this->updateLastActivity($sId);
+
+        return (int)$this->query("INSERT INTO `" . $this->sTable . "` SET " . $this->arrayToSQL($aSet) . " ON DUPLICATE KEY UPDATE " . $this->arrayToSQL($aUpdate)) > 0;
+    }
+	function update($sId, $aSet = array())
+    {
+    	$sSet = !empty($aSet) ? $this->arrayToSQL($aSet) . ", " : "";
+    	$sSet .= "`date`=UNIX_TIMESTAMP()";
+
+        $this->updateLastActivity($sId);
+
+        $iRet = (int)$this->query("UPDATE `" . $this->sTable . "` SET " . $sSet . " WHERE `id`=:id", array('id' => $sId)) > 0;
+        $this->setReadOnlyMode(true);
+        return $iRet;
+    }
+    function delete($sId)
+    {
+        $aRow = $this->getRow("SELECT `user_id`, `date` FROM `" . $this->sTable . "` WHERE `id`=:id LIMIT 1", ['id' => $sId]);
+        if(!empty($aRow) && is_array($aRow))
+            $this->updateLastActivityAccount($aRow['user_id'], $aRow['date']);
+
+        $sSql = $this->prepare("DELETE FROM `" . $this->sTable . "` WHERE `id`=? LIMIT 1", $sId);
+        return (int)$this->query($sSql) > 0;
+    }
+    function deleteExpired()
+    {
+        $iTime = time() - BX_DOL_SESSION_LIFETIME;
+        $sSql = $this->prepare("SELECT `user_id`, `date` FROM `" . $this->sTable . "` WHERE `date` < ? AND `user_id` != 0 ORDER BY `date` DESC LIMIT 50000", $iTime);
+        $aRows = $this->getAll($sSql);
+
+        foreach ($aRows as $aRow) {
+            $this->updateLastActivityAccount($aRow['user_id'], $aRow['date']);
+        }
+
+        $sSql = $this->prepare("DELETE FROM `" . $this->sTable . "` WHERE `date` < ?", $iTime);
+        $iRet = (int)$this->query($sSql);
+        if ($iRet)
+            $this->query("OPTIMIZE TABLE `" . $this->sTable . "`");
+        return $iRet;
+    }
+    
+    function deleteByAccountId($iAccountId)
+    {
+        $iDate = $this->getOne("SELECT `date` FROM `" . $this->sTable . "` WHERE `user_id` = :id ORDER BY `date` DESC", ['id' => $iAccountId]);
+        if ($iDate)
+            $this->updateLastActivityAccount($iAccountId, $iDate);
+        
+        return (int)$this->query("DELETE FROM `" . $this->sTable . "` WHERE `user_id` = :id", ['id' => $iAccountId]);
+    }
+
+    function updateLastActivity($iId)
+    {
+        if (defined('BX_DOL_SESSION_UPDATE_ACTIVE') && ($aRow = $this->getRow("SELECT `date`, `user_id` FROM `" . $this->sTable . "` WHERE `id` = :id AND `date` < UNIX_TIMESTAMP() - " . BX_DOL_SESSION_UPDATE_ACTIVE, ['id' => $iId]))) {
+            $this->updateLastActivityAccount($aRow['user_id'], $aRow['date']);
+        }
+    }
+
+    function updateLastActivityAccount($iAccountId, $iDate)
+    {
+        if ($iDate > 0) {
+            $a = BxDolAccountQuery::getInstance()->getInfoById($iAccountId);
+            if ($a && $iDate > $a['active'])
+                BxDolAccountQuery::getInstance()->_updateField($iAccountId, 'active', $iDate);
+        }
+    }
+
+    function getLastActivityAccount($iUserId)
+    {
+        $sSql = $this->prepare("SELECT `date` FROM `" . $this->sTable . "` WHERE `user_id`=? ORDER BY `date` DESC LIMIT 1", $iUserId);
+        return $this->getOne($sSql);
+    }
+
+    function getOldSession($iUserId) {
+        $sSql = $this->prepare("SELECT `id` FROM `" . $this->sTable . "` WHERE `user_id`=? LIMIT 1", $iUserId);
+        $sSession = $this->getOne($sSql);
+        return !empty($sSession) ? $sSession : false;
+    }
+}
+
+/** @} */

+ 3766 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/inc/classes/BxDolTemplate.php

@@ -0,0 +1,3766 @@
+<?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_CONTENT_PRELOAD', 50); ///< page content preload for dynamic (htmx) loading
+define('BX_PAGE_CONTENT_WITH_COVER', 54); ///< page content with cover and submenu for dynamic (htmx) loading
+define('BX_PAGE_CONTENT_WITH_SUBMENU', 56); ///< page content with submenu for dynamic (htmx) loading
+define('BX_PAGE_CONTENT', 58); ///< page content only for dynamic (htmx) loading
+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 = !bx_is_dbg() && !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', 'unit_showcase.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 = !bx_is_dbg() && !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 = !bx_is_dbg() && !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;
+
+        /**
+         * @hooks
+         * @hookdef hook-system-get_color_palette 'system', 'get_color_palette' - hook on get color palette
+         * - $unit_name - equals `system`
+         * - $action - equals `get_color_palette` 
+         * - $object_id - not used 
+         * - $sender_id - not used 
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `override_result` - [array] by ref, array of colors, can be overridden in hook processing
+         * @hook @ref hook-system-get_color_palette
+         */
+        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(),
+            'title' => '',
+            '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 = 'sys_layout_images_' . $this->_sCode .  '_' . bx_site_hash('images') . '.php';
+        self::$_aImages = BxDolDb::getInstance()->getDbCacheObject()->getData(self::$_sImagesCacheKey);
+        if(!self::$_aImages)
+            self::$_aImages = [];
+
+        /**
+         * @hooks
+         * @hookdef hook-system-get_layout_images 'system', 'get_layout_images' - hook on get layout images
+         * - $unit_name - equals `system`
+         * - $action - equals `get_layout_images` 
+         * - $object_id - not used 
+         * - $sender_id - not used 
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `code` - [string] page code
+         *      - `override_result` - [array] by ref, array of images, can be overridden in hook processing
+         * @hook @ref hook-system-get_layout_images
+         */
+        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;
+    }
+    
+    /**
+     * Set page name index by target.
+     * @param string $s target. 
+     */
+    function setPageNameIndexByTarget($s)
+    {
+        $i = BX_PAGE_DEFAULT;
+
+        switch($s) {
+            case 'bx-content-preload':
+                $i = BX_PAGE_CONTENT_PRELOAD;
+                break;
+
+            case 'bx-content-with-toolbar-wrapper':
+                //$i = 52;
+                break;
+
+            case 'bx-content-with-cover-wrapper':
+                $i = BX_PAGE_CONTENT_WITH_COVER;
+                break;
+
+            case 'bx-content-with-submenu-wrapper':
+                $i = BX_PAGE_CONTENT_WITH_SUBMENU;
+                break;
+
+            case 'bx-content-wrapper':
+                $i = BX_PAGE_CONTENT;
+                break;
+        }
+
+        $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 meta title.
+     *
+     * @param string sTitle necessary page description.
+     */
+    function setPageMetaTitle($sTitle)
+    {
+        $this->aPage['title'] = $sTitle;
+    }
+    
+    /**
+     * 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() : '';
+    }
+
+    /**
+     * get added js translations
+     */ 
+    function getJsTranslation($bDynamic = false)
+    {
+        return $bDynamic ? $this->_processJsTranslations() : $this->aPage['js_translations'];
+    }
+
+    /**
+     * 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'])) {
+                $oImgStorage = BxDolStorage::getObjectInstance(BX_DOL_STORAGE_OBJ_IMAGES);
+                foreach(['icon_android_splash', 'icon_android', 'icon_apple'] as $sIcon)
+                    if(($iIcon = (int)getParam('sys_site_' . $sIcon)) != 0 && ($sUrl = $oImgStorage->getFileUrlById($iIcon))) {
+                        $this->aPage['image'] = $sUrl;
+                        break;
+                    }
+            }
+        }
+
+        // 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);
+    }
+
+    function getIconContent($sName)
+    {
+        if(strpos($sName, '.svg') === false)
+            return false;
+
+        $mixedResult = false;
+        if(($sIconPath = $this->getIconPath($sName)))
+            $mixedResult = file_get_contents($sIconPath);
+        else
+            $mixedResult = bx_file_get_contents($sName);
+
+        return $mixedResult;
+    }
+    /**
+     * 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)
+    {
+        if(($aFile = $this->_locateFile('css', $sName)) !== false)
+            return $aFile[0];
+
+        return $this->_getAbsoluteLocation('url', $this->_sFolderCss, $sName, $sCheckIn);
+    }
+    function getCssUrlWithRevision($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        $sUrl = $this->getCssUrl($sName, $sCheckIn);
+        if(!empty($sUrl))
+            $sUrl = $this->addRevision($sUrl);
+
+        return $sUrl;
+    }
+
+    /**
+     * 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)
+    {
+        if(($aFile = $this->_locateFile('css', $sName)) !== false)
+            return $aFile[1];
+
+        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)
+    {
+        if(($aFile = $this->_locateFile('js', $sName)) !== false)
+            return $aFile[0];
+
+        return $this->_getAbsoluteLocationJs('url', $sName);
+    }
+    function getJsUrlWithRevision($sName)
+    {
+        $sUrl = $this->getJsUrl($sName);
+        if(!empty($sUrl))
+            $sUrl = $this->addRevision($sUrl);
+
+        return $sUrl;
+    }
+    /**
+     * Get full Path of JS file.
+     *
+     * @param  string $sName    - JS file name.
+     * @return string full URL.
+     */
+    function getJsPath($sName)
+    {
+        if(($aFile = $this->_locateFile('js', $sName)) !== false)
+            return $aFile[1];
+
+        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['title']) && !empty($this->aPage['title']))
+                    $sRet = bx_process_output(strip_tags($this->aPage['title']));
+                if(empty($sRet) && 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 'page_viewport':
+                $sRet = getParam('sys_viewport_meta_tag');
+                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_code();
+                break;
+            case 'lang_direction':
+                $sRet = bx_lang_direction();
+                break;
+            case 'lang_country':
+                if (!($sRet = BxDolLanguages::getInstance()->getLangCountryCode()))
+                    $sRet = bx_lang_country();
+                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;
+                }
+
+                $oMenuSubmenu = BxDolMenu::getObjectInstance('sys_site_submenu');
+                $oMenuManage = BxDolMenu::getObjectInstance('sys_site_manage');
+                if($oMenuSubmenu !== false && !$oMenuSubmenu->isVisible() && $oMenuManage !== false && !$oMenuManage->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';
+                }
+
+                $sRet .= ' bx-dir-' . strtolower(bx_lang_direction());
+                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;
+            case 'service_worker':
+                if(getParam('sys_pwa_sw_enable') != 'on')
+                    break;
+                $sRet = "if(navigator && navigator.serviceWorker) navigator.serviceWorker.register('sw.js.php');";
+                break;
+            case 'socket_engine':
+                $sRet = BxDolSockets::getInstance()->getJsCode();
+                break;
+
+            case 'info':
+                $sRet = 'L:' . bx_get_logged_profile_id();
+                if(($oPage = BxDolPage::getObjectInstanceByURI()) !== false && method_exists($oPage, 'getContentInfo')) {
+                    $aContentInfo = $oPage->getContentInfo();
+                    if(isset($aContentInfo['id']))
+                        $sRet .= '-C:' . (int)$aContentInfo['id'];
+                    if(isset($aContentInfo['profile_id']))
+                        $sRet .= '-P:' . (int)$aContentInfo['profile_id'];
+                }
+                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;
+    }
+
+    public function clearTemplatesCache($sType = '')
+    {
+        if(!$sType)
+            $sType = 'template';
+
+        $this->getTemplatesCacheObject()->removeAllByPrefix($this->getCacheFilePrefix($sType));
+    }
+
+    public function clearImagesCache($sCode = '')
+    {
+        if(!$sCode)
+            $sCode = $this->_sCode;
+
+        BxDolDb::getInstance()->getDbCacheObject()->removeAllByPrefix('sys_layout_images_' . $sCode .  '_');
+    }
+
+    /**
+     * 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);}";
+
+        $aFilesLocated = [];
+        foreach($aFiles as $sFile) {
+            $mixedFile = $this->_locateFile('js', $sFile);
+            if($mixedFile === false)
+                continue;
+
+            list($sUrl) = $mixedFile;
+
+            $aFilesLocated[] = $this->addRevision($sUrl);
+        }
+        $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)
+    {
+        if (false !== strpos($sFile, '.mjs.js'))
+            return "<script type=\"module\" src=\"" . $sFile . "\"></script>";
+        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);";
+
+        $aFilesLocated = [];
+        foreach($aFiles as $sFile) {
+            $mixedFile = $this->_locateFile('css', $sFile);
+            if($mixedFile === false)
+                continue;
+
+            list($sUrl) = $mixedFile;
+
+            $aFilesLocated[] = $this->addRevision($sUrl);
+        }
+        $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, $bWrap = true)
+    {
+        $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, $bWrap);
+
+        //--- 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 $bWrap ? $this->$sMethodWrap($sCacheAbsoluteUrl) : $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, $bWrap);
+
+        if($this->{'_b' . $sUpcaseType . 'Archive'})
+            $sCacheAbsoluteUrl = $this->_getLoaderUrl($sType, $sName);
+
+        return $bWrap ? $this->$sMethodWrap($sCacheAbsoluteUrl) : $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, $bWrap = true)
+    {
+        $sUpcaseType = ucfirst($sType);
+
+        $sMethodWrap = '_wrapInTag' . $sUpcaseType;
+        $sMethodLess = '_less' . $sUpcaseType;
+
+        $mixedResult = $bWrap ? "" : [];
+        foreach($aFiles as $aFile) {
+            if($this->{'_b' . $sUpcaseType . 'Less'})
+                $aFile = $this->$sMethodLess($aFile);
+
+            $sFileUrl = $aFile['url'];
+            if(!$this->{'_b' . $sUpcaseType . 'Cache'})
+                $sFileUrl = $this->addRevision($sFileUrl);
+
+            if($bWrap)
+                $mixedResult .= $this->$sMethodWrap($sFileUrl);
+            else
+                $mixedResult[] = $sFileUrl;
+        }
+
+        return $mixedResult;
+    }
+    /**
+     * 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);
+
+        $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':
+                    /**
+                     * @hooks
+                     * @hookdef hook-system-add_files 'system', 'add_files' - hook on add file to page 
+                     * - $unit_name - equals `system`
+                     * - $action - equals `add_files` 
+                     * - $object_id - not used 
+                     * - $sender_id - not used 
+                     * - $extra_params - array of additional params with the following array keys:
+                     *      - `file` - [string] file name
+                     *      - `type` - [string] file type
+                     *      - `dynamic` - [bool] true if added as dynamic
+                     *      - `system` - [bool] true if system file
+                     *      - `url` - [string] by ref, file url, can be overridden in hook processing
+                     *      - `path` - [string] by ref, file path, can be overridden in hook processing
+                     * @hook @ref hook-system-add_files
+                     */
+                    bx_alert('system', 'add_files', 0, 0, [
+                        'file' => $sFile,
+                        'type' => $sType,
+                        'dynamic' => $bDynamic,
+                        'system' => $bSystem,
+                        'url' => &$sUrl,
+                        'path' => &$sPath,
+                    ]);
+
+                    if($bDynamic)
+                        $sResult .= $this->$sMethodWrap($this->addRevision($sUrl));
+                    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++) {
+            $sVarNameKey = $aVarName . "['" . $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(" . $sVarNameKey . ")) for(" . $sIndex . "=0; " . $sIndex . "<count(" . $sVarNameKey . "); " . $sIndex . "++){ ?>";
+                    if(($sInnerValue = $this->_compileContent($aMatches[1], $sVarNameKey . "[" . $sIndex . "]", $iVarDepth + 1, current($aValues[$i]), $mixedKeyWrapperHtml)) === false)
+                        return false;
+
+                    $sValue .= $sInnerValue;
+                    $sValue .= "<?php } else if(is_string(" . $sVarNameKey . ")) echo " . $sVarNameKey . "; ?>";
+                }
+            } 
+            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]) || empty($aValues[$i]['content']) || !is_array($aValues[$i]['content']))
+                        return false;
+
+                    $sValue .= "<?php if(isset(" . $sVarNameKey .") && " . $sVarNameKey . "['condition']){ ?>";
+                    if(($sInnerValue = $this->_compileContent($aMatches[1], $sVarNameKey . "['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(isset(" . $sVarNameKey . ") ? " . $sVarNameKey . " : '');?>";
+            }
+
+            $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_icon_inline:([^\s]+) \/>'s" => "get_icon_inline",
+            "'<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_icon_inline':
+                    if(($sResult = $this->getIconContent($aMatches[1])) === false)
+                        $sResult = '';
+                    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;
+        }
+
+        $aParts = [];
+        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(in_array($sLocation, ['system', 'studio']))
+                $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 && isset($aParts[0], $aParts[1]) && @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
+     */
+    public function getRevision()
+    {
+        return (int)getParam('sys_revision');
+    }
+
+    /**
+     * Add current revision number to URL.
+     * 
+     * @return string with URL 
+     */
+    public function addRevision($sUrl)
+    {
+        return bx_append_url_params($sUrl, ['rev' => $this->getRevision()]);
+    }
+
+    /**
+     *
+     * 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');
+        
+        $a = [
+            'title' => _t('_access_denied_page_title'),
+            'content' => _t('_access_denied_page_content'),
+        ];
+        
+        $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);
+        }
+        
+        if (bx_is_api())
+            return [bx_api_get_msg($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;
+
+        /**
+         * @hooks
+         * @hookdef hook-system-design_before_output 'system', 'design_before_output' - hook on before page's html generated
+         * - $unit_name - equals `system`
+         * - $action - equals `design_before_output` 
+         * - $object_id - not used 
+         * - $sender_id - not used 
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `page` - [array] by ref, page object paramenters, can be overridden in hook processing
+         *      - `page_content` - [array] by ref, page content values, can be overridden in hook processing
+         * @hook @ref hook-system-design_before_output
+         */
+        bx_alert('system', 'design_before_output', 0, 0, ['page' => &$this->aPage, 'page_content' => &$this->aPageContent]);
+
+        header( 'Content-type: text/html; charset=utf-8' );
+        $sXFrameOpts = getParam('sys_x_frame_options');
+        if ('Off' !== $sXFrameOpts)
+            header( 'X-Frame-Options: ' . $sXFrameOpts );
+        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());
+        
+        /**
+         * @hooks
+         * @hookdef hook-system-design_after_output 'system', 'design_after_output' - hook on after page's html generated
+         * - $unit_name - equals `system`
+         * - $action - equals `design_after_output` 
+         * - $object_id - not used 
+         * - $sender_id - not used 
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `override_result` - [string] by ref, html content for current page, can be overridden in hook processing
+         * @hook @ref hook-system-design_after_output
+         */
+        bx_alert('system', 'design_after_output', 0, false, ['override_result' => &$sResult]);
+        
+        echo $sResult;
+    }
+}
+
+/** @} */

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

+ 875 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/modules/base/general/classes/BxBaseModGeneralFormsEntryHelper.php

@@ -0,0 +1,875 @@
+<?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;
+    protected $_bDynamicMode;
+
+    /**
+     * '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 $_mixedContextId;    
+
+    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);
+
+        $this->_mixedContextId = false;
+    }
+
+    public function setAjaxMode($bAjaxMode)
+    {
+        $this->_bAjaxMode = (bool)$bAjaxMode;
+        if($this->_bAjaxMode)
+            $this->setDynamicMode(true);
+    }
+
+    public function setAbsoluteActionUrl($bAbsoluteActionUrl)
+    {
+        $this->_bAbsoluteActionUrl = (bool)$bAbsoluteActionUrl;
+    }
+
+    public function setContextId($mixedContextId)
+    {
+        $this->_mixedContextId = is_numeric($mixedContextId) ? (int)$mixedContextId : (bool)$mixedContextId;
+    }
+
+    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 ($sMsg = '_sys_txt_error_occured') && $this->_bIsApi ? $this->prepareResponseAPI('msg', $sMsg) : $this->prepareResponse(MsgBox(_t($sMsg)), $bAsJson, 'msg');
+
+        $bAsJson = $this->_bAjaxMode && $oForm->isSubmitted();
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->$sCheckFunction())) {
+            if (($oProfile = BxDolProfile::getInstance()) !== false && ($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 ($sMsg = '_sys_txt_access_denied') && $this->_bIsApi ? $this->prepareResponseAPI('msg', $sMsg) : $this->prepareResponse(MsgBox(_t($sMsg)), $bAsJson, 'msg');
+            else
+                return $this->_bIsApi ? $this->prepareResponseAPI('msg', $sMsg) : $this->prepareResponse(MsgBox($sMsg), $bAsJson, 'msg');
+        }
+
+        // check and display form
+        $oForm->initChecker();
+        if (!$oForm->isSubmittedAndValid()) {
+            if(!$oForm->isSubmitted() && isset($CNF['FIELD_ALLOW_VIEW_TO']) && ($oProfile = BxDolProfile::getInstance()) !== false && ($oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW'])) !== false)
+                $oPrivacy->deleteGroupCustomByProfileIdUnused($oProfile->id());
+
+            return $this->_bIsApi ? $this->prepareResponseAPI('form', $oForm->getCodeAPI(), ['request_uri' => 'entity_create']) : $this->prepareResponse($oForm->getCode($this->_bDynamicMode), $bAsJson, 'form', [
+            	'form_id' => $oForm->getId()
+            ]);
+        }
+
+        // insert data into database
+        $aValsToAdd = array ();
+        $iContentId = $oForm->insert ($aValsToAdd);
+        if (!$iContentId) {
+            if (!$oForm->isValid())
+                return $this->_bIsApi ? $this->prepareResponseAPI('form', $oForm->getCodeAPI(), ['request_uri' => 'entity_create']) : $this->prepareResponse($oForm->getCode($this->_bDynamicMode), $bAsJson, 'form', [
+                    'form_id' => $oForm->getId()
+                ]);
+            else
+                return ($sMsg = '_sys_txt_error_entry_creation') && $this->_bIsApi ? $this->prepareResponseAPI('msg', $sMsg) : $this->prepareResponse(MsgBox(_t($sMsg)), $bAsJson, 'msg');
+        }
+
+        $sResult = $this->onDataAddAfter (getLoggedId(), $iContentId);
+        if ($sResult)
+            return $this->_bIsApi ? $this->prepareResponseAPI('msg', $sResult) : $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 ($this->_bIsApi)
+            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']];
+
+        if(($mixedUrl = $this->_getRedirectFromContext('add', $aContentInfo)) !== false)
+              $sUrl = $mixedUrl;
+
+        /**
+         * @hooks
+         * @hookdef hook-bx_base_general-redirect_after_add '{module_name}', 'redirect_after_add' - hook to override redirect URL which is used after content creation
+         * - $unit_name - module name
+         * - $action - equals `redirect_after_add`
+         * - $object_id - not used
+         * - $sender_id - not used
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `ajax_mode` - [boolean] dynamic loading is used or not
+         *      - `content` - [array] content info array as key&value pairs
+         *      - `override_result` - [string] by ref, redirect URL, can be overridden in hook processing
+         * @hook @ref hook-bx_base_general-redirect_after_add
+         */
+        bx_alert($this->_oModule->getName(), 'redirect_after_add', 0, false, [
+            'ajax_mode' => $this->_bAjaxMode,
+            'content' => $aContentInfo,
+            'override_result' => &$sUrl,
+        ]);
+
+        if($this->_bIsApi)
+            return !empty($sUrl) ? [bx_api_get_block('redirect', ['uri' => bx_api_get_relative_url(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 ? $this->prepareResponseAPI('msg', $sMsg) : MsgBox(_t($sMsg))) : '';
+
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->$sCheckFunction($aContentInfo)))
+            return $bErrorMsg ? ($this->_bIsApi ? $this->prepareResponseAPI('msg', $sMsg) : MsgBox($sMsg)) : '';
+
+        // check and display form
+        $oForm = $this->getObjectFormEdit($sDisplay);
+        if (!$oForm)
+            return $bErrorMsg && ($sMsg = '_sys_txt_error_occured') ? ($this->_bIsApi ? $this->prepareResponseAPI('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 ? $this->prepareResponseAPI('form', $oForm->getCodeAPI(), ['request_uri' => 'entity_edit', 'request_params' => ['params' => [$iContentId, $oForm->aParams['display']]]]) : $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 ? $this->prepareResponseAPI('form', $oForm->getCodeAPI(), ['request_uri' => 'entity_edit', 'request_params' => ['params' => [$iContentId, $oForm->aParams['display']]]]) : $oForm->getCode();
+            else
+                return ($sMsg = '_sys_txt_error_entry_update') && $this->_bIsApi ? $this->prepareResponseAPI('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 ? $this->prepareResponseAPI('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
+        if ($this->_bIsApi)
+            return $this->redirectAfterEdit($aContentInfo);
+        else
+            $this->redirectAfterEdit($aContentInfo);
+    }
+
+    /**
+     * @return empty string on success or error message string on error
+     */
+    public function editData ($iContentId, $aValues, $sDisplay = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // get content data and profile info
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        if (!$aContentInfo)
+            return _t('_sys_txt_error_entry_is_not_defined');
+
+        // get display form
+        $oForm = $this->getObjectFormEdit($sDisplay);
+        if (!$oForm)
+            return _t('_sys_txt_error_occured');
+
+        foreach ($aValues as $k => $v) {
+            if (!isset($oForm->aInputs[$k]))
+                return _t('_sys_txt_forms_unknown_field_err', $k);
+        }
+
+        $oForm->aFormAttrs['method'] = BX_DOL_FORM_METHOD_SPECIFIC;
+        $oForm->aParams['csrf']['disable'] = true;
+        $oForm->initChecker($aContentInfo, $aContentInfo);
+
+        // before edit hook
+        $aTrackTextFieldsChanges = null;
+        $this->onDataEditBefore ($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $aTrackTextFieldsChanges, $oProfile, $oForm);
+
+        // update data in the DB
+        if (!$oForm->update ($aContentInfo[$CNF['FIELD_ID']], $aValues, $aTrackTextFieldsChanges)) {
+            return _t('_sys_txt_error_entry_update');
+        }
+
+        // after edit hook
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        $sResult = $this->onDataEditAfter ($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $aTrackTextFieldsChanges, $oProfile, $oForm);
+        if ($sResult)
+            return $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);
+
+        // Create alert about the completed action.
+        $this->_oModule->alertAfterEdit($aContentInfo);
+
+        return '';
+    }
+
+    protected function redirectAfterEdit($aContentInfo, $sUrl = '')
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if ($sUrl == '')
+            $sUrl = 'page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']];
+
+        if(($mixedUrl = $this->_getRedirectFromContext('edit', $aContentInfo)) !== false)
+            $sUrl = $mixedUrl;
+
+        /**
+         * @hooks
+         * @hookdef hook-bx_base_general-redirect_after_edit '{module_name}', 'redirect_after_edit' - hook to override redirect URL which is used after content changing
+         * It's equivalent to @ref hook-bx_base_general-redirect_after_add
+         * except `ajax_mode` parameter in $extra_params is missing
+         * @hook @ref hook-bx_base_general-redirect_after_edit
+         */
+        bx_alert($this->_oModule->getName(), 'redirect_after_edit', 0, false, [
+            'content' => $aContentInfo,
+            'override_result' => &$sUrl,
+        ]);
+
+        if($this->_bIsApi)
+            return !empty($sUrl) ? bx_api_get_block('redirect', ['uri' => bx_api_get_relative_url(BxDolPermalinks::getInstance()->permalink($sUrl)), 'timeout' => 1000]) : [];
+
+        $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
+        if (bx_is_api())
+            return $this->redirectAfterDelete($aContentInfo);
+        else
+            $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(),
+        );
+
+        /**
+         * @hooks
+         * @hookdef hook-bx_base_general-redirect_after_delete '{module_name}', 'redirect_after_delete' - hook to override redirect URL which is used after content deletion
+         * It's equivalent to @ref hook-bx_base_general-redirect_after_add
+         * except `markers` parameter was added to $extra_params. It allows to override an array of markers, which can be parsed in URL.
+         * @hook @ref hook-bx_base_general-redirect_after_delete
+         */
+        bx_alert($this->_oModule->getName(), 'redirect_after_delete', 0, false, [
+            'content' => $aContentInfo,
+            'markers' => &$aMarkers,
+            'override_result' => &$sUrl,
+        ]);
+
+        if($this->_bIsApi)
+            return bx_api_get_block('redirect', ['uri' => bx_api_get_relative_url(BxDolPermalinks::getInstance()->permalink($sUrl)), 'timeout' => 1000]);
+
+        $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;
+
+        /**
+         * @hooks
+         * @hookdef hook-bx_base_general-deleted '{module_name}', 'deleted' - hook after content was deleted
+         * - $unit_name - module name
+         * - $action - equals `deleted`
+         * - $object_id - content id
+         * - $sender_id - not used
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `content` - [array] by ref, content info array as key&value pairs, can be overridden in hook processing
+         * @hook @ref hook-bx_base_general-deleted
+         */
+        bx_alert($this->_oModule->getName(), 'deleted', $aContentInfo[$CNF['FIELD_ID']], false, [
+            '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
+        $aSpecificValues =[];
+        if (!empty($CNF['OBJECT_METATAGS']) && ($oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS'])) !== false) {
+            if ($oMetatags->keywordsIsEnabled()) {
+                $aFields = $oMetatags->metaFields($aContentInfo, $CNF, $CNF['OBJECT_FORM_ENTRY_DISPLAY_VIEW']);
+                $oForm->setMetatagsKeywordsData($iContentId, $aFields, $oMetatags);
+            }
+
+            if ($oMetatags->locationsIsEnabled())
+                $aSpecificValues = $oMetatags->locationGet($iContentId, empty($CNF['FIELD_LOCATION_PREFIX']) ? '' : $CNF['FIELD_LOCATION_PREFIX']);
+        }        
+
+        // display profile
+        $oForm->initChecker($aContentInfo, $aSpecificValues);
+        
+        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);
+
+        $sKey = 'FIELD_ALLOW_VIEW_TO';
+        if(isset($CNF[$sKey], $aContentInfo[$CNF[$sKey]]) && ($iContextPid = (int)$aContentInfo[$CNF[$sKey]]) < 0) {
+            $iContextPid = abs($iContextPid);
+            if(($oContext = BxDolProfile::getInstance($iContextPid)) !== false) {
+                $sModule = $oContext->getModule();
+                $sMethod = 'on_content_deleted';
+                if(bx_is_srv($sModule, $sMethod))
+                    bx_srv($sModule, $sMethod, [$this->_oModule->getName(), $iContentId, $oContext->getContentId()]);
+            }
+        }
+
+        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 prepareResponseAPI($sType, $mixedData, $aParams = [])
+    {
+        $aResult = [];
+        switch($sType) {
+            case 'msg':
+                $aResult = bx_api_get_msg($mixedData);
+                break;
+
+            case 'form':
+                $sModule = $this->_oModule->getName();
+                $sRequestUrl = '/api.php?r=' . $sModule . '/' . $aParams['request_uri'];
+                if(!empty($aParams['request_params']) && is_array($aParams['request_params']))
+                    $sRequestUrl = bx_append_url_params($sRequestUrl, $aParams['request_params']);
+
+                bx_append_url_params($sUrl, $mixedParams);
+                $aResult = bx_api_get_block('form', $mixedData, [
+                    'ext' => [
+                        'name' => $sModule, 
+                        'request' => ['url' => $sRequestUrl, 'immutable' => true]
+                    ]
+                ]);
+                break;
+        }
+        
+        return [$aResult];
+    }
+
+    protected function _setAbsoluteActionUrl($sType, &$oForm)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sUri = '';
+        if(($sKeyUri = 'URI_' . strtoupper($sType) . '_ENTRY') && !empty($CNF[$sKeyUri]))
+            $sUri = $CNF[$sKeyUri];
+        if(!$sUri && $sType == 'add' && ($sKeyUri = 'URI_EDIT_ENTRY') && !empty($CNF[$sKeyUri]))
+            $sUri = str_replace('edit', 'create', $CNF[$sKeyUri]);
+        if(!$sUri)
+            return;
+
+        $oForm->setAbsoluteActionUrl(bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $sUri)));
+    }
+
+    protected function _getRedirectFromContext($sAction, $aContentInfo)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $sKey = 'FIELD_ALLOW_VIEW_TO';
+        if(empty($CNF[$sKey]) || !isset($aContentInfo[$CNF[$sKey]]) || (int)$aContentInfo[$CNF[$sKey]] >= 0) 
+            return false;
+
+        $iContextPid = abs($aContentInfo[$CNF[$sKey]]);
+        $oContext = BxDolProfile::getInstance($iContextPid);
+        if(!$oContext)
+            return false;
+
+        $sModuleContext = $oContext->getModule();
+        $sMethodContext = 'on_content_' . $sAction . 'ed_redirect';
+        if(!bx_is_srv($sModuleContext, $sMethodContext))
+            return false;
+
+        return bx_srv($sModuleContext, $sMethodContext, [$this->_oModule->getName(), $aContentInfo[$CNF['FIELD_ID']]]);
+    }
+}
+
+/** @} */

+ 0 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/modules/base/general/template/css/manage_tools.css


+ 2532 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/modules/base/groups/classes/BxBaseModGroupsModule.php

@@ -0,0 +1,2532 @@
+<?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));
+    }
+    
+    public function serviceGetInitialMembers ($sParams)
+    {
+        $aOptions = json_decode($sParams, true);
+        if (!$sParams || !isset($aOptions['term']))
+            return [];
+        
+        $a = BxDolService::call('system', 'profiles_search', [$aOptions['term'], ['module' => $this->_oConfig->getName()]], 'TemplServiceProfiles');
+
+        return $a;
+    }
+    
+    /**
+     * Process Process Invitation
+     */
+    public function actionProcessInvite ($sKey, $iGroupProfileId, $bAccept)
+    {
+        $aData = $this->_oDb->getInviteByKey($sKey, $iGroupProfileId);
+        if (isset($aData['invited_profile_id'])){
+            $CNF = &$this->_oConfig->CNF;
+            if (!isset($CNF['OBJECT_CONNECTIONS']) || !($oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])))
+                return '';
+            $iInvitedProfileId = $aData['invited_profile_id'];
+            if ($iInvitedProfileId != bx_get_logged_profile_id())
+                return '';
+            if ($bAccept){
+                if($oConnection && !$oConnection->isConnected($iInvitedProfileId, $iGroupProfileId)){
+                    $oConnection->addConnection($iInvitedProfileId, $iGroupProfileId);
+                    $oConnection->addConnection($iGroupProfileId, $iInvitedProfileId);
+                }
+            }
+            $this->_oDb->deleteInviteByKey($sKey, $iGroupProfileId);
+        }   
+    }
+
+    public function actionCheckName()
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+    	$sName = bx_process_input(bx_get('name'));
+    	if(empty($sName))
+            return echoJson(array());
+
+        $sResult = '';
+
+        $iId = (int)bx_get('id');
+        if(!empty($iId)) {
+            $aPrice = $this->_oDb->getPrices(array('type' => 'by_id', 'value' => $iId)); 
+            if(strcmp($sName, $aPrice[$CNF['FIELD_PRICE_NAME']]) == 0) 
+                $sResult = $sName;
+        }
+
+    	echoJson(array(
+            'name' => !empty($sResult) ? $sResult : $this->_oConfig->getPriceName($sName)
+    	));
+    }
+
+    public function actionGetQuestionnaire()
+    {
+        $sSource = '';
+        if(($sSource = bx_get('s')) !== false)
+            $sSource = bx_process_url_param($sSource);
+
+        $sObject = '';
+        if(($sObject = bx_get('o')) !== false)
+            $sObject = bx_process_url_param($sObject);
+        
+        $sAction = '';
+        if(($sAction = bx_get('a')) !== false)
+            $sAction = bx_process_url_param($sAction);
+
+        $iContentProfileId = 0;
+        if(($iContentProfileId = bx_get('cpi')) !== false)
+            $iContentProfileId = (int)$iContentProfileId;
+
+        echoJson($this->serviceGetQuestionnaire($sSource, $sObject, $sAction, $iContentProfileId));
+    }
+
+    public function serviceManageTools($sType = 'common')
+    {
+        $this->_oTemplate->addJs(['modules/base/groups/js/|manage_tools.js']);
+
+        return parent::serviceManageTools($sType);
+    }
+    
+    public function decodeDataAPI($aData, $aParams = [])
+    {
+        $CNF = $this->_oConfig->CNF;
+        
+        $aResult = parent::decodeDataAPI($aData, $aParams);
+
+        if(isset($aParams['template']) && $aParams['template'] == 'unit_wo_info')
+            return $aResult;
+
+        if(getParam('sys_api_conn_in_prof_units') == 'on' && ($oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])) !== false) {
+            $oProfile = BxDolProfile::getInstanceByContentAndType($aData[$CNF['FIELD_ID']], $this->_aModule['name']);
+
+            $aResult['members_count'] = $oConnection->getConnectedInitiatorsCount($oProfile->id(), false);
+            $aResult['members_list'] = $oConnection->getConnectedListAPI($oProfile->id(), false, BX_CONNECTIONS_CONTENT_TYPE_INITIATORS);
+        }
+
+        return array_merge($aResult, [
+            'author_data' => BxDolProfile::getData($aData[$CNF['FIELD_AUTHOR']]),
+            'visibility' => $aData[$CNF['FIELD_ALLOW_VIEW_TO']]
+        ]);
+    }
+
+    public function serviceGetMenuAddonManageTools()
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aResult = parent::serviceGetMenuAddonManageTools();
+
+        if(!empty($CNF['FIELD_STATUS_ADMIN']))
+            $aResult['counter1_value'] = $this->_oDb->getEntriesNumByParams([[
+                'key' => $CNF['FIELD_STATUS_ADMIN'],
+                'value' => BX_BASE_MOD_GENERAL_STATUS_PENDING, 
+                'operator' => '='
+            ]]);
+
+        return $aResult;
+    }
+
+    public function serviceGetOptionsMembersMode()
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        return array(
+            array('key' => '', 'value' => _t('_None')),
+            array('key' => BX_BASE_MOD_GROUPS_MMODE_MULTI_ROLES, 'value' => _t($CNF['T']['option_members_mode_' . BX_BASE_MOD_GROUPS_MMODE_MULTI_ROLES])),
+        );
+    }
+
+    public function serviceGetSearchResultUnit ($iContentId, $sUnitTemplate = '')
+    {
+        if(empty($sUnitTemplate))
+            $sUnitTemplate = 'unit.html';
+
+        return parent::serviceGetSearchResultUnit($iContentId, $sUnitTemplate);
+    }
+
+    /**
+     * @see BxBaseModProfileModule::serviceGetSpaceTitle
+     */ 
+    public function serviceGetSpaceTitle()
+    {
+        return _t($this->_oConfig->CNF['T']['txt_sample_single']);
+    }
+    
+    /**
+     * @see iBxDolProfileService::serviceGetParticipatingProfiles
+     */ 
+    public function serviceGetParticipatingProfiles($iProfileId, $aConnectionObjects = false)
+    {
+        if (isset($this->_oConfig->CNF['OBJECT_CONNECTIONS'])){
+            $aConnectionObjects = array($this->_oConfig->CNF['OBJECT_CONNECTIONS'], 'sys_profiles_subscriptions');
+            return parent::serviceGetParticipatingProfiles($iProfileId, $aConnectionObjects);
+        }
+        return parent::serviceGetParticipatingProfiles($iProfileId);
+    }
+
+    public function serviceGetSafeServices()
+    {
+        return array_merge(parent::serviceGetSafeServices(), [
+            'GetQuestionnaire' => '',
+            'GetInitialMembers' => '',
+            'EntityInvite' => '',
+        ]);
+    }
+
+    /**
+     * 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) {
+            /**
+             * @hooks
+             * @hookdef hook-bx_base_groups-join_invitation '{module_name}', 'join_invitation' - hook before adding (sending) new join to context invitation
+             * - $unit_name - module name
+             * - $action - equals `join_invitation`
+             * - $object_id - context id
+             * - $sender_id - context profile id
+             * - $extra_params - array of additional params with the following array keys:
+             *      - `content` - [array] context info array as key&value pairs
+             *      - `entry_title` - [string] context title
+             *      - `entry_url` - [string] context URL
+             *      - `group_profile` - [int] context profile id
+             *      - `profile` - [int] profile id who was invited
+             *      - `notification_subobject_id` - [int] profile id who was invited
+             *      - `object_author_id` - [int] context profile id
+             * @hook @ref hook-bx_base_groups-join_invitation
+             */
+            bx_alert($sModule, 'join_invitation', $aContentInfo[$CNF['FIELD_ID']], $iGroupProfileId, [
+                '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.
+             */
+            /**
+             * @hooks
+             * @hookdef hook-bx_base_groups-join_invitation_notif '{module_name}', 'join_invitation_notif' - hook before adding new join to context invitation. Is needed for Notifications module.
+             * - $unit_name - module name
+             * - $action - equals `join_invitation_notif`
+             * - $object_id - context id
+             * - $sender_id - context profile id
+             * - $extra_params - array of additional params with the following array keys:
+             *      - `object_author_id` - [int] profile id who was invited
+             *      - `privacy_view` - [int] or [string] privacy for view context action, @see BxDolPrivacy
+             * @hook @ref hook-bx_base_groups-join_invitation_notif
+             */
+            bx_alert($sModule, 'join_invitation_notif', $aContentInfo[$CNF['FIELD_ID']], $iGroupProfileId, [
+                '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']) {
+            /**
+             * @hooks
+             * @hookdef hook-bx_base_groups-join_request '{module_name}', 'join_request' - hook before adding new join to context request
+             * - $unit_name - module name
+             * - $action - equals `join_request`
+             * - $object_id - context id
+             * - $sender_id - context profile id
+             * - $extra_params - array of additional params with the following array keys:
+             *      - `object_author_id` - [int] context profile id
+             *      - `performer_id` - [int] profile id who wants to join
+             *      - `content` - [array] context info array as key&value pairs
+             *      - `entry_title` - [string] context title
+             *      - `entry_url` - [string] context URL
+             *      - `group_profile` - [int] context profile id
+             *      - `profile` - [int] profile id who wants to join
+             * @hook @ref hook-bx_base_groups-join_request
+             */
+            bx_alert($this->getName(), 'join_request', $aContentInfo[$CNF['FIELD_ID']], $iGroupProfileId, [
+            	'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) {
+            /**
+             * @hooks
+             * @hookdef hook-bx_base_groups-join_request_accepted '{module_name}', 'join_request_accepted' - hook before accepting join to context request
+             * It's equivalent to @ref hook-bx_base_groups-join_request
+             * @hook @ref hook-bx_base_groups-join_request_accepted
+             */
+            bx_alert($this->getName(), 'join_request_accepted', $aContentInfo[$CNF['FIELD_ID']], $iGroupProfileId, [
+            	'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()); 
+            }
+
+            /**
+             * @hooks
+             * @hookdef hook-bx_base_groups-fan_added '{module_name}', 'fan_added' - hook before adding (registering) new context member
+             * It's equivalent to @ref hook-bx_base_groups-join_request
+             * @hook @ref hook-bx_base_groups-fan_added
+             */
+            bx_alert($this->getName(), 'fan_added', $aContentInfo[$CNF['FIELD_ID']], $iGroupProfileId, [
+            	'object_author_id' => $iGroupProfileId,
+            	'performer_id' => $iProfileId,
+
+            	'content' => $aContentInfo,
+            	'entry_title' => $sEntryTitle, 
+            	'entry_url' => $sEntryUrl,
+
+            	'group_profile' => $iGroupProfileId, 
+            	'profile' => $iProfileId,
+            ]);
+
+            $this->doAudit($iGroupProfileId, $iInitiatorId, '_sys_audit_action_group_join_request_accepted');
+            
+            return false;
+        }
+
+        // don't automatically add connection (mutual) if group requires manual join confirmation
+        if ($bSendInviteOnly || $aContentInfo['join_confirmation'])
+            return false;
+
+        // check if connection already exists
+        if ($oConnection->isConnected($oGroupProfile->id(), (int)$iInitiatorId, true) || $oConnection->isConnected($oGroupProfile->id(), (int)$iInitiatorId))
+            return false;
+
+        if (!$oConnection->addConnection($oGroupProfile->id(), (int)$iInitiatorId))
+            return false;
+
+        return true;
+    }
+
+    public function serviceFansTable ()
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $oGrid = BxDolGrid::getObjectInstance($CNF['OBJECT_GRID_CONNECTIONS']);
+        if(!$oGrid)
+            return false;
+
+        if($this->_bIsApi){
+            return [
+                bx_api_get_block('grid', $oGrid->getCodeAPI())
+            ];
+        }
+
+        return $oGrid->getCode();
+    }
+    
+    public function serviceInvitesTable ()
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!isset($CNF['OBJECT_GRID_INVITES']))
+            return false;
+
+        $oGrid = BxDolGrid::getObjectInstance($CNF['OBJECT_GRID_INVITES']);
+        if(!$oGrid)
+            return false;
+
+        if($this->_bIsApi)
+            return [
+                bx_api_get_block('grid', $oGrid->getCodeAPI())
+            ];
+
+        return $oGrid->getCode();
+    }
+
+    public function serviceBansTable ()
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!isset($CNF['OBJECT_GRID_BANS']))
+            return false;
+
+        $oGrid = BxDolGrid::getObjectInstance($CNF['OBJECT_GRID_BANS']);
+        if(!$oGrid)
+            return false;
+
+        if($this->_bIsApi)
+            return [
+                bx_api_get_block('grid', $oGrid->getCodeAPI())
+            ];
+
+        return $oGrid->getCode();
+    }
+
+    public function serviceFans ($iContentId = 0, $bAsArray = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+
+        if (!$iContentId)
+            return false;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if (!$aContentInfo)
+            return false;
+
+        if (!($oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName())))
+            return false;
+
+        if(!$bAsArray) {
+            bx_import('BxDolConnection');
+            $mixedResult = $this->serviceBrowseConnectionsQuick ($oGroupProfile->id(), $CNF['OBJECT_CONNECTIONS'], BX_CONNECTIONS_CONTENT_TYPE_CONTENT, true);
+            if (!$mixedResult)
+                return MsgBox(_t('_sys_txt_empty'));
+        }
+        else
+            $mixedResult = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])->getConnectedContent($oGroupProfile->id(), true);
+
+        return $mixedResult;
+    }
+    
+    public function serviceFansWithoutAdmins ($iContentId = 0, $bAsArray = false)
+    {
+        if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iContentId)
+            return false;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if (!$aContentInfo)
+            return false;
+
+        if (!($oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName())))
+            return false;
+        
+        $CNF = &$this->_oConfig->CNF;
+
+        $aFans = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])->getConnectedContent($oGroupProfile->id(), true);
+        if(empty($aFans) || !is_array($aFans))
+            return false;
+
+        $aAdmins = $this->_oDb->getAdmins($oGroupProfile->id());
+        if(!empty($aAdmins) && is_array($aAdmins))
+            $aFans = array_diff($aFans, $aAdmins);
+
+        $iStart = (int)bx_get('start');
+        $iLimit = !empty($CNF['PARAM_NUM_CONNECTIONS_QUICK']) ? getParam($CNF['PARAM_NUM_CONNECTIONS_QUICK']) : 4;
+        if(!$iLimit)
+            $iLimit = 4;
+
+        return $this->_serviceBrowseQuick($aFans, $iStart, $iLimit);
+    }
+
+    public function serviceAdmins ($iContentId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if(!$iContentId)
+            return false;
+
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName());
+        if(!$oGroupProfile)
+            return false;
+
+        $iStart = (int)bx_get('start');
+        $iLimit = !empty($CNF['PARAM_NUM_CONNECTIONS_QUICK']) ? getParam($CNF['PARAM_NUM_CONNECTIONS_QUICK']) : 4;
+        if(!$iLimit)
+            $iLimit = 4;
+        
+        $aProfiles = $this->_oDb->getAdmins($oGroupProfile->id(), $iStart,  $iLimit+1);
+        if(empty($aProfiles) || !is_array($aProfiles))
+            return false;
+
+        return $this->_serviceBrowseQuick($aProfiles, $iStart, $iLimit);
+    }
+
+    public function serviceMembersByRole ($iContentId = 0, $iRole = BX_BASE_MOD_GROUPS_ROLE_COMMON)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if(!$iContentId)
+            return false;
+
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName());
+        if(!$oGroupProfile)
+            return false;
+
+        $iStart = (int)bx_get('start');
+        $iLimit = !empty($CNF['PARAM_NUM_CONNECTIONS_QUICK']) ? getParam($CNF['PARAM_NUM_CONNECTIONS_QUICK']) : 4;
+        if(!$iLimit)
+            $iLimit = 4;
+
+        $aProfiles = $this->_oDb->getRoles([
+            'type' => 'fan_pids_by_group_pid', 
+            'group_profile_id' => $oGroupProfile->id(), 
+            'role' => $iRole,
+            'start' => $iStart,  
+            'limit' => $iLimit + 1
+        ]);
+
+        if(empty($aProfiles) || !is_array($aProfiles))
+            return false;
+
+        return $this->_serviceBrowseQuick($aProfiles, $iStart, $iLimit);
+    }
+
+    public function serviceBrowseJoinedEntries ($iProfileId = 0, $bDisplayEmptyMsg = false)
+    {
+        if (!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if (!$iProfileId)
+            return '';
+
+        return $this->_serviceBrowse ('joined_entries', array('joined_profile' => $iProfileId), BX_DB_PADDING_DEF, $bDisplayEmptyMsg);
+    }
+    
+    public function serviceBrowseFollowedEntries ($iProfileId = 0, $bDisplayEmptyMsg = false)
+    {
+        if (!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if (!$iProfileId)
+            return '';
+
+        return $this->_serviceBrowse ('followed_entries', array('followed_profile' => $iProfileId), BX_DB_PADDING_DEF, $bDisplayEmptyMsg);
+    }
+
+    public function serviceBrowseCreatedEntries ($iProfileId = 0, $bDisplayEmptyMsg = false)
+    {
+        if (!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if (!$iProfileId)
+            return '';
+
+        return $this->_serviceBrowse ('created_entries', array('author' => $iProfileId), BX_DB_PADDING_DEF, $bDisplayEmptyMsg);
+    }
+
+    public function serviceBrowseRecommendationsFans ($iProfileId = 0, $aParams = [])
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if($this->_bIsApi)
+            $aParams = bx_api_get_browse_params($aParams, true);
+
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+        if(!$iProfileId)
+            return '';
+
+        $aParams = array_merge([
+            'empty_message' => false,
+            'start' => 0,
+            'per_page' => 0
+        ], $aParams);
+
+        if(($iStartGet = bx_get('start')) !== false)
+            $aParams['start'] = (int)$iStartGet;
+
+        if(($iPerPageGet = bx_get('per_page')) !== false)
+            $aParams['per_page'] = (int)$iPerPageGet;
+
+        $oRecommendation = BxDolRecommendation::getObjectInstance($CNF['OBJECT_RECOMMENDATIONS_FANS']);
+        if(!$oRecommendation)
+            return false;
+
+        if($this->_bIsApi) {
+            $aData = $oRecommendation->getCodeAPI($iProfileId, $aParams);
+            $aData = array_merge($aData, [
+                'module' => 'system',
+                'unit' => 'mixed', 
+                'request_url' => '/api.php?r=bx_groups/browse_recommendations_fans&params[]=' . $iProfileId . '&params[]='
+            ]);
+
+            return [bx_api_get_block('browse', $aData)];
+        }
+
+        $sCode = $oRecommendation->getCode($iProfileId, $aParams);
+        if(!$sCode && $aParams['empty_message'])
+            $sCode = MsgBox(_t(!empty($aParams['empty_message_text']) ? $aParams['empty_message_text'] : '_Empty'));
+
+        return $sCode;
+    }
+
+    public function serviceBrowseMembers ($iProfileId = 0, $aParams = [])
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if(!$iProfileId)
+            return '';
+
+        $oGroupProfile = BxDolProfile::getInstance($iProfileId);
+        if(!($oGroupProfile))
+            return '';
+
+        return bx_srv('system', 'browse_members', [$oGroupProfile->id(), $CNF['OBJECT_CONNECTIONS'], $aParams], 'TemplServiceProfiles');
+    }
+
+    public function serviceEntityEditQuestionnaire($iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if(!$iProfileId)
+            return '';
+
+        $aProfileInfo = BxDolProfileQuery::getInstance()->getInfoById($iProfileId);
+        if(empty($aProfileInfo) || !is_array($aProfileInfo))
+            return '';
+        
+        $aContentInfo = $this->_oDb->getContentInfoById($aProfileInfo['content_id']);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return '';
+
+        if($this->checkAllowedEdit($aContentInfo) !== CHECK_ACTION_RESULT_ALLOWED)
+            return MsgBox(_t('_Access denied'));
+
+        $oGrid = BxDolGrid::getObjectInstance($CNF['OBJECT_GRID_QUESTIONS_MANAGE']);
+        if(!$oGrid)
+            return '';
+        
+        if($this->_bIsApi){
+            return [
+                bx_api_get_block('grid', $oGrid->getCodeAPI())
+            ];
+            
+        }
+
+        return $oGrid->getCode();
+    }
+
+    public function serviceEntityPricing($iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if(!$iProfileId)
+            return '';
+
+        if(!$this->_oConfig->isPaidJoin())
+            return '';
+
+        $oPayments = BxDolPayments::getInstance();
+    	if(!$oPayments->isActive())
+            return MsgBox(_t('_sys_payments_err_no_payments'));
+
+        if($this->checkAllowedUsePaidJoin() !== CHECK_ACTION_RESULT_ALLOWED)
+            return MsgBox(_t('_Access denied'));
+
+        if($this->checkAllowedManageAdmins($iProfileId) !== CHECK_ACTION_RESULT_ALLOWED)
+            return MsgBox(_t('_Access denied'));
+
+        $oGrid = BxDolGrid::getObjectInstance($CNF['OBJECT_GRID_PRICES_MANAGE']);
+        if(!$oGrid)
+            return '';
+
+        $sNote = '';
+        if(!$oPayments->isAcceptingPayments($this->_iProfileId))
+            $sNote = MsgBox(_t('_sys_payments_err_not_accept_payments', $oPayments->getDetailsUrl()));
+
+        return $sNote . $oGrid->getCode();
+    }
+
+    public function serviceEntityJoin($iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if(!$iProfileId)
+            return '';
+
+        if(!$this->_oConfig->isPaidJoin())
+            return '';
+
+        $oGrid = BxDolGrid::getObjectInstance($CNF['OBJECT_GRID_PRICES_VIEW']);
+        if(!$oGrid)
+            return '';
+
+        return $oGrid->getCode();
+    }
+    
+    public function serviceEntityInvite ($iContentId = 0, $bErrorMsg = true)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!isset($CNF['OBJECT_FORM_ENTRY_DISPLAY_INVITE']))
+            return false;
+
+        return $this->_serviceEntityForm ('inviteForm', $iContentId, false, false, $bErrorMsg);
+    }
+    
+    /**
+     * Entry social sharing block
+     */
+    public function serviceEntitySocialSharing ($mixedContent = false, $aParams = array())
+    {
+        if(!empty($mixedContent)) {
+            if(!is_array($mixedContent))
+               $mixedContent = array((int)$mixedContent, array());
+        }
+        else {
+            $mixedContent = $this->_getContent();
+            if($mixedContent === false)
+                return false;
+        }
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType((int)$iContentId, $this->getName());
+        if(!$oGroupProfile)
+            return false;
+
+        return parent::serviceEntitySocialSharing(array($iContentId, $aContentInfo), array(
+            'title' => $oGroupProfile->getDisplayName()
+        ));
+    }
+
+    public function serviceIsPricingAvaliable($iProfileId)
+    {
+        if(!$this->_oConfig->isPaidJoin())
+            return false;
+
+        if($this->checkAllowedUsePaidJoin() !== CHECK_ACTION_RESULT_ALLOWED)
+            return false;        
+
+        if($this->checkAllowedManageAdmins($iProfileId) !== CHECK_ACTION_RESULT_ALLOWED)
+            return false;
+
+        return true;
+    }
+
+    public function serviceIsPaidJoinAvaliable($iGroupProfileId, $iProfileId = 0)
+    {
+        return $this->isPaidJoinByProfileForProfile($iGroupProfileId, $iProfileId);
+    }
+
+    public function serviceIsPaidJoinAvaliableByContent($iGroupContentId, $iProfileId = 0)
+    {
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($iGroupContentId, $this->getName());
+        if(!$oGroupProfile)
+            return false;
+
+        return $this->isPaidJoinByProfileForProfile($oGroupProfile->id(), $iProfileId);
+    }
+
+    public function serviceIsFreeJoinAvaliable($iGroupProfileId, $iProfileId = 0)
+    {
+        return !$this->isPaidJoinByProfileForProfile($iGroupProfileId, $iProfileId);
+    }
+
+    public function serviceIsFreeJoinAvaliableByContent($iGroupContentId, $iProfileId = 0)
+    {
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($iGroupContentId, $this->getName());
+        if(!$oGroupProfile)
+            return false;
+
+        return !$this->isPaidJoinByProfileForProfile($oGroupProfile->id(), $iProfileId);
+    }
+
+    /**
+     * Is Paid Join enabled in the group and whether a profile can use it.
+     * 
+     * @param type $iGroupProfileId - Group profile ID.
+     * @param type $iProfileId - Profile ID of the user who wants to join.
+     * @return boolean
+     */
+    public function isPaidJoinByProfileForProfile($iGroupProfileId, $iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($iProfileId))
+            $iProfileId = $this->_iProfileId;
+
+        if(BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])->isConnected($iProfileId, $iGroupProfileId))
+            return false;
+
+        return $this->isPaidJoinByProfile($iGroupProfileId);
+    }
+
+    /**
+     * Is Paid Join enabled as is and whether a group has pricing plans added.
+     * 
+     * @param type $iGroupProfileId - Group profile ID.
+     * @return boolean
+     */
+    public function isPaidJoinByProfile($iGroupProfileId)
+    {
+        if(!$this->_oConfig->isPaidJoin())
+            return false;
+
+        $aPrices = $this->_oDb->getPrices(array('type' => 'by_profile_id', 'profile_id' => $iGroupProfileId));
+        if(empty($aPrices) || !is_array($aPrices))
+            return false;
+
+        return true;
+    }
+
+    /**
+     * Integration with Payments.
+     */
+    public function serviceGetPaymentData()
+    {
+        return $this->_aModule;
+    }
+
+    public function serviceGetCartItem($mixedItemId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!$mixedItemId)
+            return array();
+
+        if(is_numeric($mixedItemId))
+            $aItem = $this->_oDb->getPrices(array('type' => 'by_id', 'value' => (int)$mixedItemId));
+        else 
+            $aItem = $this->_oDb->getPrices(array('type' => 'by_name', 'value' => $mixedItemId));
+
+        if(empty($aItem) || !is_array($aItem))
+            return array();
+
+        if(!$this->isPaidJoinByProfile($aItem['profile_id']))
+            return array();
+
+        $oGroupProfile = BxDolProfile::getInstance($aItem['profile_id']);
+        if(!$oGroupProfile)
+            return array();
+
+        $aGroupProfile = $this->_oDb->getContentInfoById($oGroupProfile->getContentId());
+        
+        $aRoles = BxDolFormQuery::getDataItems($CNF['OBJECT_PRE_LIST_ROLES']);
+
+        $sTitle = '';
+        if(!empty($aItem['period']) && !empty($aItem['period_unit']))
+            $sTitle = _t($CNF['T']['txt_cart_item_title'], $oGroupProfile->getDisplayName(), $aRoles[$aItem['role_id']], $aItem['period'], $aItem['period_unit']);
+        else
+            $sTitle = _t($CNF['T']['txt_cart_item_title_lifetime'], $oGroupProfile->getDisplayName(), $aRoles[$aItem['role_id']]);
+
+        return array (
+            'id' => $aItem['id'],
+            'author_id' => $aGroupProfile[$CNF['FIELD_AUTHOR']],
+            'name' => $aItem['name'],
+            'title' => $sTitle,
+            'description' => '',
+            'url' => $oGroupProfile->getUrl(),
+            'price_single' => $aItem['price'],
+            'price_recurring' => $aItem['price'],
+            'period_recurring' => $aItem['period'],
+            'period_unit_recurring' => $aItem['period_unit'],
+            'trial_recurring' => 0
+        );
+    }
+
+    public function serviceGetCartItems($iSellerId)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	if(empty($iSellerId))
+    	    return array();
+
+        $sModule = $this->getName();
+        $aRoles = BxDolFormQuery::getDataItems($CNF['OBJECT_PRE_LIST_ROLES']);
+
+        $aGroups = $this->_oDb->getEntriesBy(array('type' => 'author', 'author' => $iSellerId));
+
+        $aResult = array();
+        foreach($aGroups as $aGroup) {
+            $oGroupProfile = BxDolProfile::getInstanceByContentAndType($aGroup[$CNF['FIELD_ID']], $sModule);
+            if(!$oGroupProfile)
+                continue;
+
+            $aPrices = $this->_oDb->getPrices(array('type' => 'by_profile_id', 'profile_id' => $oGroupProfile->id()));
+            if(empty($aPrices) || !is_array($aPrices))
+                continue;
+
+            $sTitle = $oGroupProfile->getDisplayName();
+            $sUrl = $oGroupProfile->getUrl();
+
+            foreach($aPrices as $aPrice)
+                $aResult[] = array(
+                    'id' => $aPrice['id'],
+                    'author_id' => $iSellerId,
+                    'name' => $aPrice['name'],
+                    'title' => _t($CNF['T']['txt_cart_item_title'], $sTitle, $aRoles[$aPrice['role_id']], $aPrice['period'], $aPrice['period_unit']),
+                    'description' => '',
+                    'url' => $sUrl,
+                    'price_single' => $aPrice['price'],
+                    'price_recurring' => $aPrice['price'],
+                    'period_recurring' => $aPrice['period'],
+                    'period_unit_recurring' => $aPrice['period_unit']
+               );
+        }
+
+        return $aResult;
+    }
+
+    public function serviceRegisterCartItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder, $sLicense)
+    {
+        return $this->_serviceRegisterItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder);
+    }
+
+    public function serviceRegisterSubscriptionItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder, $sLicense)
+    {
+        return $this->_serviceRegisterItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder);
+    }
+
+    public function serviceReregisterCartItem($iClientId, $iSellerId, $iItemIdOld, $iItemIdNew, $sOrder)
+    {
+        return $this->_serviceReregisterItem($iClientId, $iSellerId, $iItemIdOld, $iItemIdNew, $sOrder);
+    }
+
+    public function serviceReregisterSubscriptionItem($iClientId, $iSellerId, $iItemIdOld, $iItemIdNew, $sOrder)
+    {
+        return $this->_serviceReregisterItem($iClientId, $iSellerId, $iItemIdOld, $iItemIdNew, $sOrder);
+    }
+
+    public function serviceUnregisterCartItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder, $sLicense)
+    {
+        return $this->_serviceUnregisterItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder);
+    }
+
+    public function serviceUnregisterSubscriptionItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder, $sLicense)
+    {
+    	return $this->_serviceUnregisterItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder); 
+    }
+
+    public function serviceCancelSubscriptionItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder)
+    {
+    	return true;
+    }
+
+    protected function _serviceRegisterItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+    	$aItem = $this->serviceGetCartItem($iItemId);
+        if(empty($aItem) || !is_array($aItem))
+            return array();
+
+        $aItemInfo = $this->_oDb->getPrices(array('type' => 'by_id', 'value' => $iItemId));
+
+        $mixedPeriod = false;
+        if((int)$aItemInfo['period'] != 0)
+            $mixedPeriod = array(
+                'period' => (int)$aItemInfo['period'], 
+                'period_unit' => $aItemInfo['period_unit'], 
+                'period_reserve' => $CNF['PARAM_RECURRING_RESERVE']
+            );
+
+        if(!$this->setRole($aItemInfo['profile_id'], $iClientId, $aItemInfo['role_id'], $mixedPeriod, $sOrder))
+            return array();
+
+        return $aItem;
+    }
+
+    protected function _serviceReregisterItem($iClientId, $iSellerId, $iItemIdOld, $iItemIdNew, $sOrder)
+    {
+        $aItem = $this->serviceGetCartItem($iItemIdNew);
+        if(empty($aItem) || !is_array($aItem))
+            return array();
+
+        $aItemInfoOld = $this->_oDb->getPrices(array('type' => 'by_id', 'value' => $iItemIdOld));
+        if(empty($aItemInfoOld) || !is_array($aItemInfoOld))
+            return array();
+
+        $aItemInfoNew = $this->_oDb->getPrices(array('type' => 'by_id', 'value' => $iItemIdNew));
+        if(empty($aItemInfoNew) || !is_array($aItemInfoNew))
+            return array();
+
+        if(!$this->unsetRole($aItemInfoOld['profile_id'], $iClientId))
+            return array();
+        
+        $aResult = $this->_serviceRegisterItem($iClientId, $iSellerId, $iItemIdNew, 1, $sOrder);
+        if(empty($aResult) || !is_array($aResult))
+            return array();
+
+    	return $aItem;
+    }
+
+    protected function _serviceUnregisterItem($iClientId, $iSellerId, $iItemId, $iItemCount, $sOrder)
+    {
+        $aItemInfo = $this->_oDb->getPrices(array('type' => 'by_id', 'value' => $iItemId));
+        if(empty($aItemInfo) || !is_array($aItemInfo))
+            return false;
+
+        return $this->unsetRole($aItemInfo['profile_id'], $iClientId);
+    }
+
+    public function serviceGetQuestionnaire($sSource, $sObject, $sAction, $iContentProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        $sMsg = _t('_sys_txt_not_found');
+
+        $aContentInfo = $this->_oDb->getContentInfoByProfileId($iContentProfileId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return $this->_bIsApi ? [bx_api_get_msg($sMsg)] : ['code' => 1, 'msg' => $sMsg];
+        
+        $oForm = null;
+        if(($oConnection = BxDolConnection::getObjectInstance($sObject)) !== false) {
+            $oForm = $oConnection->getQuestionnaireForm($sAction, $iContentProfileId, ['request' => [
+                's' => $sSource,
+            ]]);
+
+            if($oForm === false)
+                return $this->_bIsApi ? [bx_api_get_msg($sMsg)] : ['code' => 3, 'msg' => $sMsg];
+        }
+        else
+            return $this->_bIsApi ? [bx_api_get_msg($sMsg)] : ['code' => 2, 'msg' => $sMsg];
+
+        $oForm->initChecker();
+        if($oForm->isSubmittedAndValid()) {
+            $iProfileId = bx_get_logged_profile_id();
+
+            $aQuestions = $this->_oDb->getQuestions(['sample' => 'content_pid', 'content_pid' => $iContentProfileId]);
+            foreach($aQuestions as $aQuestion)
+                $this->_oDb->insertAnswer((int)$aQuestion['id'], $iProfileId, $oForm->getCleanValue('question_' . $aQuestion['id']));
+
+            return $this->_bIsApi ? [] : ['code' => 0, 'o' => $sObject, 'a' => $sAction, 'cpi' => $iContentProfileId, 'ci' => $aContentInfo[$CNF['FIELD_ID']], 'eval' => $this->_oConfig->getJsObject($sSource) . '.connActionPerformed(oData)'];
+        }
+
+        if($this->_bIsApi)
+            return [bx_api_get_block('form', $oForm->getCodeAPI(), ['ext' => [
+                'name' => $this->_oModule->getName(), 
+                'request' => ['url' => $oForm->aFormAttrs['action'], 'immutable' => true]]
+            ])];
+
+        bx_import('BxTemplFunctions');
+        $sContent = BxTemplFunctions::getInstance()->popupBox($this->_oConfig->getHtmlIds('popup_questionnaire'), _t($CNF['T']['popup_title_questionnaire']), $this->_oTemplate->parseHtmlByName('popup_qnr_questionnaire.html', [
+            'form_id' => $oForm->getId(),
+            'form' => $oForm->getCode(true),
+        ]));
+
+        return ['popup' => ['html' => $sContent, 'options' => ['closeOnOuterClick' => false]]];
+    }
+
+    /**
+     * Data for Notifications module
+     */
+    public function serviceGetNotificationsData()
+    {
+    	$sModule = $this->_aModule['name'];
+
+        $aSettingsTypes = ['follow_member', 'follow_context'];
+        if($this->serviceActAsProfile())
+            $aSettingsTypes = ['personal', 'follow_member'];
+
+        return [
+            'handlers' => [
+                ['group' => $sModule . '_vote', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'doVote', 'module_name' => $sModule, 'module_method' => 'get_notifications_vote', 'module_class' => 'Module'],
+                ['group' => $sModule . '_vote', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'undoVote'],
+                
+                ['group' => $sModule . '_score_up', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'doVoteUp', 'module_name' => $sModule, 'module_method' => 'get_notifications_score_up', 'module_class' => 'Module'],
+
+                ['group' => $sModule . '_score_down', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'doVoteDown', 'module_name' => $sModule, 'module_method' => 'get_notifications_score_down', 'module_class' => 'Module'],
+
+                ['group' => $sModule . '_fan_added', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'fan_added', 'module_name' => $sModule, 'module_method' => 'get_notifications_fan_added', 'module_class' => 'Module'],
+
+                ['group' => $sModule . '_join_invitation', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'join_invitation_notif', 'module_name' => $sModule, 'module_method' => 'get_notifications_join_invitation', 'module_class' => 'Module'],
+                
+                ['group' => $sModule . '_join_request', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'join_request', 'module_name' => $sModule, 'module_method' => 'get_notifications_join_request', 'module_class' => 'Module', 'module_event_privacy' => $this->_oConfig->CNF['OBJECT_PRIVACY_VIEW_NOTIFICATION_EVENT']],
+                
+                ['group' => $sModule . '_timeline_post_common', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'timeline_post_common', 'module_name' => $sModule, 'module_method' => 'get_notifications_timeline_post_common', 'module_class' => 'Module'],
+                
+                //--- Moderation related: For 'admins'.
+                ['group' => $sModule . '_object_pending_approval', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'pending_approval', 'module_name' => $sModule, 'module_method' => 'get_notifications_post_pending_approval', 'module_class' => 'Module'],
+            ],
+            'settings' => [
+                ['group' => 'vote', 'unit' => $sModule, 'action' => 'doVote', 'types' => $aSettingsTypes],
+
+                ['group' => 'score_up', 'unit' => $sModule, 'action' => 'doVoteUp', 'types' => $aSettingsTypes],
+
+                ['group' => 'score_down', 'unit' => $sModule, 'action' => 'doVoteDown', 'types' => $aSettingsTypes],
+                
+                ['group' => 'fan', 'unit' => $sModule, 'action' => 'fan_added', 'types' => $aSettingsTypes],
+
+                ['group' => 'invite', 'unit' => $sModule, 'action' => 'join_invitation_notif', 'types' => ['personal']],
+
+                ['group' => 'join', 'unit' => $sModule, 'action' => 'join_request', 'types' => $aSettingsTypes],
+
+                ['group' => 'timeline_post', 'unit' => $sModule, 'action' => 'timeline_post_common', 'types' => $aSettingsTypes],
+
+                //--- Moderation related: For 'admins'.
+                ['group' => 'action_required', 'unit' => $sModule, 'action' => 'pending_approval', 'types' => ['personal']],
+            ],
+            'alerts' => [
+                ['unit' => $sModule, 'action' => 'doVote'],
+                ['unit' => $sModule, 'action' => 'undoVote'],
+
+                ['unit' => $sModule, 'action' => 'doVoteUp'],
+                ['unit' => $sModule, 'action' => 'doVoteDown'],
+
+                ['unit' => $sModule, 'action' => 'fan_added'],
+
+                ['unit' => $sModule, 'action' => 'join_invitation_notif'],
+
+                ['unit' => $sModule, 'action' => 'join_request'],
+
+                ['unit' => $sModule, 'action' => 'timeline_post_common'],
+                
+                //--- Moderation related: For 'admins'.
+                ['unit' => $sModule, 'action' => 'pending_approval'],
+            ]
+        ];
+    }
+
+    public function serviceGetNotificationsInsertData($oAlert, $aHandler, $aDataItems)
+    {
+        if($oAlert->sAction != 'join_invitation_notif' || empty($aDataItems) || !is_array($aDataItems))
+            return $aDataItems;
+
+        foreach($aDataItems as $iIndex => $aDataItem)
+            $aDataItems[$iIndex]['object_privacy_view'] = BX_DOL_PG_ALL;
+
+        return $aDataItems;
+    }
+
+    /**
+     * Notification about new invitation to join the group
+     */
+    public function serviceGetNotificationsJoinInvitation($aEvent)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $iContentId = (int)$aEvent['object_id'];
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType((int)$iContentId, $this->getName());
+        if(!$oGroupProfile)
+            return array();
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return array();
+
+        /*
+         * It's essential that 'object_owner_id' contains invited member profile id.
+         */
+        $oProfile = BxDolProfile::getInstance((int)$aEvent['object_owner_id']);
+        if(!$oProfile)
+            return array();
+
+        /*
+         * Note. Group Profile URL is used for both Entry and Subentry URLs, 
+         * because Subentry URL has higher display priority and notification
+         * should be linked to Group Profile (Group Profile -> Members tab) 
+         * instead of Personal Profile of invited member.
+         */
+        $sEntryUrl = bx_absolute_url(str_replace(BX_DOL_URL_ROOT, '', $oGroupProfile->getUrl()), '{bx_url_root}');
+
+        return array(
+            'entry_sample' => $CNF['T']['txt_sample_single'],
+            'entry_url' => $sEntryUrl,
+            'entry_caption' => $oGroupProfile->getDisplayName(),
+            'subentry_sample' => $oProfile->getDisplayName(),
+            'subentry_url' => $sEntryUrl,
+            'lang_key' => $this->_oConfig->CNF['T']['txt_ntfs_join_invitation']
+        );
+    }
+
+    /**
+     * Notification about new member requst in the group
+     */
+    public function serviceGetNotificationsJoinRequest($aEvent)
+    {
+        return $this->_serviceGetNotification($aEvent, 'join_request', $this->_oConfig->CNF['T']['txt_ntfs_join_request']);
+    }
+
+	/**
+     * Notification about new member in the group
+     */
+    public function serviceGetNotificationsFanAdded($aEvent)
+    {
+        return $this->_serviceGetNotification($aEvent, 'fan_added', $this->_oConfig->CNF['T']['txt_ntfs_fan_added']);
+    }
+
+    protected function _serviceGetNotification($aEvent, $sType, $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();
+
+        $iGroupProfileId = $oGroupProfile->id();
+
+        /*
+         * 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 or Manage page) 
+         * instead of Personal Profile of a member, who performed an action.
+         */
+        if($sType == 'join_request' && !empty($CNF['URL_ENTRY_MANAGE']))
+            $sEntryUrl = bx_absolute_url(BxDolPermalinks::getInstance()->permalink($CNF['URL_ENTRY_MANAGE'], [
+                'profile_id' => $iGroupProfileId
+            ]), '{bx_url_root}');
+        else if(!empty($CNF['URL_ENTRY_FANS']))
+            $sEntryUrl = bx_absolute_url(BxDolPermalinks::getInstance()->permalink($CNF['URL_ENTRY_FANS'], [
+                'profile_id' => $iGroupProfileId
+            ]), '{bx_url_root}');
+        else
+            $sEntryUrl = bx_absolute_url(str_replace(BX_DOL_URL_ROOT, '', $oGroupProfile->getUrl()), '{bx_url_root}');
+
+        return [
+            'entry_sample' => $CNF['T']['txt_sample_single'],
+            'entry_url' => $sEntryUrl,
+            'entry_caption' => $oGroupProfile->getDisplayName(),
+            'entry_author' => $iGroupProfileId,
+            'subentry_sample' => $oProfile->getDisplayName(),
+            'subentry_url' => $sEntryUrl,
+            'lang_key' => $sLangKey
+        ];
+    }
+
+    /**
+     * Data for Reputation module
+     */
+    public function serviceGetReputationData()
+    {
+        $aResult = parent::serviceGetReputationData();
+
+    	$sModule = $this->_aModule['name'];
+
+        if(!empty($aResult['handlers']) && is_array($aResult['handlers']))
+            foreach($aResult['handlers'] as $iKey => $aHandler)
+                if(in_array($aHandler['group'], [$sModule . '_comment', $sModule . '_reaction']))
+                    unset($aResult['handlers'][$iKey]);
+
+        if(!empty($aResult['alerts']) && is_array($aResult['alerts']))
+            foreach($aResult['alerts'] as $iKey => $aAlert)
+                if($aAlert['unit'] == $sModule . '_reactions' || in_array($aAlert['action'], ['commentPost', 'commentRemoved']))
+                    unset($aResult['alerts'][$iKey]);
+
+        return $aResult;
+    }
+
+    /**
+     * Data for Timeline module
+     */
+    public function serviceGetTimelineData()
+    {
+        $aResult = BxBaseModGeneralModule::serviceGetTimelineData();
+
+        $sModule = $this->_aModule['name'];
+        $aResult['handlers'] = array_merge($aResult['handlers'], [
+            ['group' => $sModule . '_object', 'type' => 'update', 'alert_unit' => $sModule, 'alert_action' => 'context_cover_changed'],
+            ['group' => $sModule . '_object', 'type' => 'update', 'alert_unit' => $sModule, 'alert_action' => 'context_cover_deleted']
+        ]);
+        $aResult['alerts'] = array_merge($aResult['alerts'], [
+            ['unit' => $sModule, 'action' => 'context_cover_changed'],
+            ['unit' => $sModule, 'action' => 'context_cover_deleted']
+        ]);
+
+        return $aResult;
+    }
+
+    /**
+     * Entry post for Timeline module
+     */
+    public function serviceGetTimelinePost($aEvent, $aBrowseParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $a = parent::serviceGetTimelinePost($aEvent, $aBrowseParams);
+        if($a === false)
+            return false;
+
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($aEvent['object_id'], $this->getName());
+        $a['content']['url'] = $oGroupProfile->getUrl();
+        $a['content']['title'] = $oGroupProfile->getDisplayName();
+        
+        $oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS']);
+        if($oConnection)
+            $a['content']['members'] = $oConnection->getConnectedInitiatorsCount($oGroupProfile->id(), true);
+
+        if(isset($aEvent['object_privacy_view']))
+            $a['content']['visibility'] = $aEvent['object_privacy_view'];
+
+        if(isset($CNF['FIELD_PUBLISHED'])) {
+            $aContentInfo = $this->_oDb->getContentInfoById($aEvent['object_id']);
+            if($aContentInfo[$CNF['FIELD_PUBLISHED']] > $a['date'])
+                $a['date'] = $aContentInfo[$CNF['FIELD_PUBLISHED']];
+        }
+
+        return $a;
+    }
+
+
+    // ====== PERMISSION METHODS
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedUsePaidJoin($isPerformAction = false)
+    {
+        // check ACL
+        $aCheck = checkActionModule($this->_iProfileId, 'use paid join', $this->getName(), $isPerformAction);
+        if($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedView ($aDataEntry, $isPerformAction = false)
+    {
+        return $this->serviceCheckAllowedViewForProfile ($aDataEntry, $isPerformAction);
+    }
+
+    public function serviceCheckAllowedViewForProfile ($aDataEntry, $isPerformAction = false, $iProfileId = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+        $iGroupContentId = (int)$aDataEntry[$CNF['FIELD_ID']];
+
+        $bInvited = false;
+        if(!empty($CNF['TABLE_INVITES'])) {
+            $iGroupProfileId = BxDolProfile::getInstanceByContentAndType($iGroupContentId, $this->getName())->id();
+
+            if(($sKey = bx_get('key')) !== false) {
+                $mixedInvited = $this->isInvited($sKey, $iGroupProfileId);
+                if($mixedInvited === true)
+                    $bInvited = true;
+            }
+            else {
+                $mixedInvited = $this->isInvitedByProfileId($iProfileId ? $iProfileId : bx_get_logged_profile_id(), $iGroupProfileId);
+                if($mixedInvited === true)
+                    $bInvited = true;
+            }
+        }
+
+        if ($this->isFan($iGroupContentId, $iProfileId) || $bInvited)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return parent::serviceCheckAllowedViewForProfile ($aDataEntry, $isPerformAction, $iProfileId);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedCompose(&$aDataEntry, $isPerformAction = false)
+    {
+        if(!$this->isFan($aDataEntry[$this->_oConfig->CNF['FIELD_ID']]))
+            return _t('_sys_txt_access_denied');
+
+        return parent::checkAllowedCompose ($aDataEntry, $isPerformAction);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedFanAdd(&$aDataEntry, $isPerformAction = false)
+    {
+        $mixedResult = $this->_modGroupsCheckAllowedFanAdd($aDataEntry, $isPerformAction);
+
+        /**
+         * @hooks
+         * @hookdef hook-system-check_allowed_fan_add 'system', 'check_allowed_fan_add' - hook to override the result of checking whether 'fan add' to context action is allowed or not to currently logged in user
+         * - $unit_name - equals `system`
+         * - $action - equals `check_allowed_fan_add`
+         * - $object_id - not used
+         * - $sender_id - not used
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `module` - [string] module name
+         *      - `content_info` - [array] context info array as key&value pairs
+         *      - `profile_id` - [int] currently logged in profile id to be checked the availability of the action to
+         *      - `override_result` - [string] or [int] by ref, check action result, can be overridden in hook processing. Return string with an error if action isn't allowed or CHECK_ACTION_RESULT_ALLOWED, @see BxDolAcl
+         * @hook @ref hook-system-check_allowed_fan_add
+         */
+        bx_alert('system', 'check_allowed_fan_add', 0, 0, [
+            'module' => $this->getName(), 
+            'content_info' => $aDataEntry, 
+            'profile_id' => bx_get_logged_profile_id(), 
+            'override_result' => &$mixedResult
+        ]);
+
+        return $mixedResult;
+    }
+
+    public function _modGroupsCheckAllowedFanAdd (&$aDataEntry, $isPerformAction = false)
+    {
+        if ($this->isFan($aDataEntry[$this->_oConfig->CNF['FIELD_ID']]) || !isLogged())
+            return _t('_sys_txt_access_denied');
+
+        return $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, $this->_oConfig->CNF['OBJECT_CONNECTIONS'], false, false);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedFanRemove (&$aDataEntry, $isPerformAction = false)
+    {
+        if (CHECK_ACTION_RESULT_ALLOWED === $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, $this->_oConfig->CNF['OBJECT_CONNECTIONS'], false, true, true))
+            return CHECK_ACTION_RESULT_ALLOWED;
+        return $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, $this->_oConfig->CNF['OBJECT_CONNECTIONS'], false, true, false);
+    }
+
+    protected function _checkAllowedActionByFan($sAction, $aDataEntry, $iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $bRoles = $this->_oConfig->isRoles();
+        if(empty($iProfileId))
+            $iProfileId = bx_get_logged_profile_id();
+
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$CNF['FIELD_ID']], $this->getName());
+        if(!$oGroupProfile)
+            return $sAction == BX_BASE_MOD_GROUPS_ACTION_DELETE ? CHECK_ACTION_RESULT_ALLOWED : _t('_sys_txt_not_found');
+
+        $iGroupProfileId = $oGroupProfile->id();
+
+        if(!$bRoles && $this->_oDb->isAdmin($iGroupProfileId, $iProfileId, $aDataEntry))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        if($bRoles && $this->isAllowedActionByRole($sAction, $aDataEntry, $iGroupProfileId, $iProfileId))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return _t('_sys_txt_access_denied');
+    }
+
+    public function isAllowedActionByRole($mAction, $aDataEntry, $iGroupProfileId, $iProfileId)
+    {
+        $iProfileRole = $this->_oDb->getRole($iGroupProfileId, $iProfileId);
+
+        if (is_array($mAction)) {
+            $sAction = $mAction['action'];
+            $sActionModule = $mAction['module'];
+        } else {
+            $sAction = $mAction;
+            $sActionModule = $this->getName();
+        }
+
+        $bResult = false;
+        if($iProfileId)
+            $bResult = $this->isAllowedModuleActionByRole($sActionModule, $sAction, $iProfileRole);
+
+        // in case neither of the profile's roles are having permissions set explicitly then fallback to an old way
+        if ($bResult === NULL) {
+            $bResult = false;
+            if($this->isFanByGroupProfileId($iGroupProfileId)) {
+                switch ($sAction) {
+                    case BX_BASE_MOD_GROUPS_ACTION_DELETE:
+                    case BX_BASE_MOD_GROUPS_ACTION_EDIT:
+                    case BX_BASE_MOD_GROUPS_ACTION_CHANGE_COVER:
+                    case BX_BASE_MOD_GROUPS_ACTION_MANAGE_ROLES:
+                        if($this->isRole($iProfileRole, BX_BASE_MOD_GROUPS_ROLE_ADMINISTRATOR)) 
+                            $bResult = true;
+                        break;
+
+                    case BX_BASE_MOD_GROUPS_ACTION_MANAGE_FANS:
+                    case BX_BASE_MOD_GROUPS_ACTION_INVITE:
+                    case BX_BASE_MOD_GROUPS_ACTION_EDIT_CONTENT:
+                    case BX_BASE_MOD_GROUPS_ACTION_DELETE_CONTENT:
+                    case BX_BASE_MOD_GROUPS_ACTION_TIMELINE_POST_PIN:
+                        if($this->isRole($iProfileRole, BX_BASE_MOD_GROUPS_ROLE_ADMINISTRATOR) || $this->isRole($iProfileRole, BX_BASE_MOD_GROUPS_ROLE_MODERATOR)) 
+                            $bResult = true;
+                        break;
+
+                    default:
+                        $bResult = true;
+                }
+            }
+        }
+
+        // in case current user is not allowed to edit/delete this group then
+        // if it is a subgroup inside a context then give to the admin roles of a parent context the ability to edit/delete that subgroup.
+        if (
+            !$bResult &&
+            ($sAction == BX_BASE_MOD_GROUPS_ACTION_EDIT || $sAction == BX_BASE_MOD_GROUPS_ACTION_DELETE) &&
+            isset($this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']) &&
+            $this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO'] &&
+            isset($aDataEntry[$this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']]) &&
+            $aDataEntry[$this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']] < 0)
+        {
+            $iParentContextProfileId = -$aDataEntry[$this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']];
+            $oParentContext = BxDolProfile::getInstance($iParentContextProfileId);
+            if ($oParentContext) {
+                $sCheckAction = $sAction == BX_BASE_MOD_GROUPS_ACTION_EDIT ? BX_BASE_MOD_GROUPS_ACTION_EDIT_CONTENT : BX_BASE_MOD_GROUPS_ACTION_DELETE_CONTENT;
+                $aParentDataEntry = bx_srv($oParentContext->getModule(), 'get_info', [$oParentContext->getContentId(), false]);
+                return $this->isAllowedActionByRole(['action' => $sCheckAction, 'module' => $oParentContext->getModule()], $aParentDataEntry, $iParentContextProfileId, $iProfileId);
+            }
+        }
+
+        /**
+         * @hooks
+         * @hookdef hook-system-check_allowed_action_by_role 'system', 'check_allowed_action_by_role' - hook to override the result of checking whether an action is allowed or not to context member by his role in the context
+         * - $unit_name - equals `system`
+         * - $action - equals `check_allowed_action_by_role`
+         * - $object_id - not used
+         * - $sender_id - not used
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `module` - [string] module name
+         *      - `multi_roles` - [boolean] whether multi roles are enabled in context or not
+         *      - `action` - [string] action to be checked
+         *      - `action_module` - [string] module name which the action belongs to
+         *      - `content_profile_id` - [int] context profile id
+         *      - `content_info` - [array] context info array as key&value pairs
+         *      - `profile_id` - [int] profile id to be checked the availability of the action to
+         *      - `profile_role` - [int] profile role in the context
+         *      - `override_result` - [boolean] by ref, check action result, can be overridden in hook processing.
+         * @hook @ref hook-system-check_allowed_action_by_role
+         */
+        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);
+
+        /**
+         * @hooks
+         * @hookdef hook-system-check_allowed_subscribe_add 'system', 'check_allowed_subscribe_add' - hook to override the result of checking whether currently logged in user can subscribe (follow) the context or not
+         * - $unit_name - equals `system`
+         * - $action - equals `check_allowed_subscribe_add`
+         * - $object_id - not used
+         * - $sender_id - not used
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `module` - [string] module name
+         *      - `content_info` - [array] context info array as key&value pairs
+         *      - `profile_id` - [int] currently logged in profile id to be checked the availability of the action to
+         *      - `override_result` - [string] or [int] by ref, check action result, can be overridden in hook processing. Return string with an error if action isn't allowed or CHECK_ACTION_RESULT_ALLOWED, @see BxDolAcl
+         * @hook @ref hook-system-check_allowed_subscribe_add
+         */
+        bx_alert('system', 'check_allowed_subscribe_add', 0, 0, [
+            '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']);
+
+        // when context is in another context
+        if ($aDataEntry[$this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']] < 0 && getParam('sys_check_fan_in_parent_context')) {
+            $oParent = BxDolProfile::getInstance(abs($aDataEntry[$this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']]));
+            $oModule = $oParent ? BxDolModule::getInstance($oParent->getModule()) : null;
+            if ($oModule && method_exists($oModule, 'isFan') && !$oModule->isFan($oParent->getContentId()))
+                return _t('_sys_txt_access_denied');
+        }
+
+        // if profile view isn't allowed but visibility is in partially visible groups 
+        // then display buttons to connect (befriend, join) to profile, 
+        // if other conditions (in parent::_checkAllowedConnect) are met as well
+        if (CHECK_ACTION_RESULT_ALLOWED !== $sResult && !in_array($aDataEntry[$this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']], array_merge($oPrivacy->getPartiallyVisiblePrivacyGroups(), array('s'))))
+            return $sResult;
+
+        return parent::_checkAllowedConnect ($aDataEntry, $isPerformAction, $sObjConnection, $isMutual, $isInvertResult, $isSwap);
+    }
+
+
+    // ====== COMMON METHODS
+    public function onUpdateImage($iContentId, $sFiledName, $sFiledValue, $iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+
+        $aField2Method = [
+            $CNF['FIELD_PICTURE'] => 'picture',
+            $CNF['FIELD_COVER'] => 'cover',
+        ];
+
+        if(!empty($aField2Method[$sFiledName]))
+            /**
+             * @hooks
+             * @hookdef hook-bx_base_groups-context_picture_changed '{module_name}', 'context_picture_changed' - hook after context picture was changed
+             * - $unit_name - module name
+             * - $action - equals `context_picture_changed`
+             * - $object_id - context id
+             * - $sender_id - profile id who performed the action
+             * - $extra_params - array of additional params with the following array keys:
+             *      - `status` - [string] context status
+             *      - `status_admin` - [string] context admin status
+             *      - `privacy_view` - [int] or [string] privacy for view context action, @see BxDolPrivacy
+             *      - `cf` - [int] context's audience filter value
+             * @hook @ref hook-bx_base_groups-context_picture_changed
+             */
+            /**
+             * @hooks
+             * @hookdef hook-bx_base_groups-context_cover_changed '{module_name}', 'context_cover_changed' - hook after context cover was changed
+             * It's equivalent to @ref hook-bx_base_groups-context_picture_changed 
+             * @hook @ref hook-bx_base_groups-context_cover_changed
+             */
+            bx_alert($this->getName(), 'context_' . $aField2Method[$sFiledName] . '_changed', $iContentId, $iProfileId, $this->_alertParams($aContentInfo));
+    }
+
+    protected function _alertParams($aContentInfo)
+    {
+        $aParams = parent::_alertParams($aContentInfo);
+
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!empty($CNF['FIELD_CF']) && isset($aContentInfo[$CNF['FIELD_CF']]))
+            $aParams['cf'] = $aContentInfo[$CNF['FIELD_CF']];
+
+        return $aParams;
+    }
+
+    public function alertAfterAdd($aContentInfo)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $iId = (int)$aContentInfo[$CNF['FIELD_ID']];
+        $iAuthorId = (int)$aContentInfo[$CNF['FIELD_AUTHOR']];
+
+        $sAction = 'added';
+        if(isset($CNF['FIELD_STATUS_ADMIN']) && isset($aContentInfo[$CNF['FIELD_STATUS_ADMIN']]) && $aContentInfo[$CNF['FIELD_STATUS_ADMIN']] == BX_BASE_MOD_GENERAL_STATUS_PENDING)
+            $sAction = 'deferred';        
+
+        $sModule = $this->getName();
+        $aParams = $this->_alertParams($aContentInfo);
+        /**
+         * @hooks
+         * @hookdef hook-system-prepare_alert_params 'system', 'prepare_alert_params' - hook to override alert (hook) params
+         * - $unit_name - equals `system`
+         * - $action - equals `prepare_alert_params`
+         * - $object_id - not used
+         * - $sender_id - not used
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `unit` - [string] unit name
+         *      - `action` - [string] by ref, action, can be overridden in hook processing
+         *      - `object_id` - [int] by ref, object id, can be overridden in hook processing
+         *      - `sender_id` - [int] by ref, action performer profile id, can be overridden in hook processing
+         *      - `extras` - [array] by ref, extra params array as key&value pairs, can be overridden in hook processing
+         * @hook @ref hook-system-prepare_alert_params
+         */
+        bx_alert('system', 'prepare_alert_params', 0, 0, [
+            'unit'=> $sModule, 
+            'action' => &$sAction, 
+            'object_id' => &$iId, 
+            'sender_id' => &$iAuthorId, 
+            'extras' => &$aParams
+        ]);
+        /**
+         * @hooks
+         * @hookdef hook-bx_base_groups-added '{module_name}', 'added' - hook after context was added (published)
+         * - $unit_name - module name
+         * - $action - equals `added`
+         * - $object_id - context id
+         * - $sender_id - not used
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `status` - [string] context status
+         *      - `status_admin` - [string] context admin status
+         *      - `privacy_view` - [int] or [string] privacy for view context action, @see BxDolPrivacy
+         *      - `cf` - [int] context's audience filter value
+         * @hook @ref hook-bx_base_groups-added
+         */
+        /**
+         * @hooks
+         * @hookdef hook-bx_base_groups-deferred '{module_name}', 'deferred' - hook after context was added with pending approval status
+         * It's equivalent to @ref hook-bx_base_groups-added
+         * @hook @ref hook-bx_base_groups-deferred
+         */
+        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);
+        }
+
+        /**
+         * @hooks
+         * @hookdef hook-bx_base_groups-set_role '{module_name}', 'set_role' - hook after 'set role' action was applied to context member
+         * - $unit_name - module name
+         * - $action - equals `set_role`
+         * - $object_id - context id
+         * - $sender_id - context profile id
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `object_author_id` - [int] context profile id
+         *      - `performer_id` - [int] performer profile id
+         *      - `fan_id` - [int] context member profile id
+         *      - `content` - [array] context info array as key&value pairs
+         *      - `role` - [int] or [array] role or an array of roles to be set
+         *      - `group_profile` - [int] context profile id
+         *      - `profile` - [int] performer profile id
+         * @hook @ref hook-bx_base_groups-set_role
+         */
+        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());
+
+        /**
+         * @hooks
+         * @hookdef hook-bx_base_groups-set_role '{module_name}', 'set_role' - hook after 'set role' action was applied to context member
+         * - $unit_name - module name
+         * - $action - equals `set_role`
+         * - $object_id - context id
+         * - $sender_id - context profile id
+         * - $extra_params - array of additional params with the following array keys:
+         *      - `object_author_id` - [int] context profile id
+         *      - `performer_id` - [int] performer profile id
+         *      - `fan_id` - [int] context member profile id
+         *      - `content` - [array] context info array as key&value pairs
+         *      - `role` - [int] or [array] role or an array of roles to be set
+         *      - `group_profile` - [int] context profile id
+         *      - `profile` - [int] performer profile id
+         * @hook @ref hook-bx_base_groups-set_role
+         */
+        bx_alert($this->getName(), 'set_role', $aGroupProfileInfo[$CNF['FIELD_ID']], $iGroupProfileId, array(
+            'object_author_id' => $iGroupProfileId,
+            'performer_id' => $iProfileId, 
+            'fan_id' => $iFanProfileId,
+
+            'content' => $aGroupProfileInfo,
+            'role' => $iRole,
+
+            'group_profile' => $iGroupProfileId, 
+            'profile' => $iProfileId
+        ));
+
+        $this->doAudit($iGroupProfileId, $iFanProfileId, '_sys_audit_action_group_role_changed');
+    }
+
+    public function getGroupsByFan($iProfileId, $mixedRole = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!isset($CNF['OBJECT_CONNECTIONS']))
+            return false;
+
+        if($mixedRole === false)
+            $mixedRole = BX_BASE_MOD_GROUPS_ROLE_COMMON;
+
+        if(!is_array($mixedRole))
+            $mixedRole = [$mixedRole];
+
+        $aResult = [];
+        foreach($mixedRole as $iRole) {
+            switch($iRole) {
+                case BX_BASE_MOD_GROUPS_ROLE_COMMON:
+                    $aIds = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])->getConnectedContent($iProfileId);
+                    break;
+
+                default:
+                    $aIds = $this->_oDb->getRoles([
+                        'type' => 'group_pids_by_fan_id', 
+                        'fan_id' => $iProfileId,
+                        'role' => $iRole
+                    ]);
+            }
+
+            $aResult = array_merge($aResult, $aIds);
+        }
+
+        return $aResult;
+    }
+
+    public function getMenuItemTitleByConnection($sConnection, $sAction, $iContentProfileId, $iInitiatorProfileId = 0)
+    {
+        $CNF = $this->_oConfig->getCNF();
+
+        $aResult = parent::getMenuItemTitleByConnection($sConnection, $sAction, $iContentProfileId, $iInitiatorProfileId);
+        if(!empty($aResult))
+            return $aResult;
+
+        $oConnection = BxDolConnection::getObjectInstance($sConnection);
+        if(!$oConnection)
+            return false;
+
+        if(!$iInitiatorProfileId)
+            $iInitiatorProfileId = bx_get_logged_profile_id();
+
+        $aResult = [];
+        if($oConnection->isConnectedNotMutual($iInitiatorProfileId, $iContentProfileId))
+            $aResult = [
+                'add' => _t(!empty($CNF['T']['menu_item_title_sm_join_requested']) ? $CNF['T']['menu_item_title_sm_join_requested'] : '_sys_menu_item_title_sm_join_requested'),
+                'remove' => _t(!empty($CNF['T']['menu_item_title_sm_leave_cancel']) ? $CNF['T']['menu_item_title_sm_leave_cancel'] : '_sys_menu_item_title_sm_leave_cancel'),
+            ];
+        else if($oConnection->isConnectedNotMutual($iContentProfileId, $iInitiatorProfileId))
+            $aResult = [
+                'add' => _t(!empty($CNF['T']['menu_item_title_sm_join_confirm']) ? $CNF['T']['menu_item_title_sm_join_confirm'] : '_sys_menu_item_title_sm_join_confirm'),
+                'remove' => _t(!empty($CNF['T']['menu_item_title_sm_leave_reject']) ? $CNF['T']['menu_item_title_sm_leave_reject'] : '_sys_menu_item_title_sm_leave_reject'),
+            ];
+        else if($oConnection->isConnected($iInitiatorProfileId, $iContentProfileId, true))
+            $aResult = [
+                'add' => '',
+                'remove' => _t(!empty($CNF['T']['menu_item_title_sm_leave']) ? $CNF['T']['menu_item_title_sm_leave'] : '_sys_menu_item_title_sm_leave'),
+            ];
+        else
+            $aResult = [
+                'add' => _t(!empty($CNF['T']['menu_item_title_sm_join']) ? $CNF['T']['menu_item_title_sm_join'] : '_sys_menu_item_title_sm_join'),
+                'remove' => '',
+            ];
+
+        return !empty($sAction) && isset($aResult[$sAction]) ? $aResult[$sAction] : $aResult;
+    }
+
+    protected function _getImagesForTimelinePost($aEvent, $aContentInfo, $sUrl, $aBrowseParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+        
+        $iImageId = 0;
+        $sImageMd = $sImageXl = '';
+        if(isset($CNF['FIELD_COVER']) && !empty($aContentInfo[$CNF['FIELD_COVER']])) {
+            $iImageId = (int)$aContentInfo[$CNF['FIELD_COVER']];
+            $sImageMd = $this->_oConfig->getImageUrl($iImageId, ['OBJECT_IMAGES_TRANSCODER_GALLERY']);
+            $sImageXl = $this->_oConfig->getImageUrl($iImageId, ['OBJECT_IMAGES_TRANSCODER_COVER']);
+        }
+
+        if($sImageXl == '' && isset($CNF['FIELD_PICTURE']) && !empty($aContentInfo[$CNF['FIELD_PICTURE']])) {
+            $iImageId = (int)$aContentInfo[$CNF['FIELD_PICTURE']];
+            $sImageMd = $this->_oConfig->getImageUrl($iImageId, ['OBJECT_IMAGES_TRANSCODER_GALLERY']);
+            $sImageXl = $this->_oConfig->getImageUrl($iImageId, ['OBJECT_IMAGES_TRANSCODER_COVER']);
+        }
+
+        if(empty($sImageXl))
+            return [];
+
+        $aImage = [
+            'id' => $iImageId, 
+            'url' => $sUrl, 
+            'src' => $sImageXl, 
+            'src_orig' => $sImageXl
+        ];
+
+        if(!empty($sImageMd))
+            $aImage['src_medium'] = $sImageMd;
+
+        return [
+            $aImage
+        ];
+    }
+
+    protected function _prepareProfileAndGroupProfile($iGroupProfileId, $iInitiatorId)
+    {
+        if (!($oGroupProfile = BxDolProfile::getInstance($iGroupProfileId)))
+            return array(0, 0, null);
+
+        if ($oGroupProfile->getModule() == $this->getName()) {
+            $iProfileId = $iInitiatorId;
+            $iGroupProfileId = $oGroupProfile->id();
+        } else {
+            $iProfileId = $oGroupProfile->id();
+            $iGroupProfileId = $iInitiatorId;
+        }
+
+        return array($iProfileId, $iGroupProfileId, $oGroupProfile);
+    }
+}
+
+/** @} */

+ 24 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/modules/base/groups/template/css/main.css

@@ -0,0 +1,24 @@
+.bx-base-group-cover-wrapper .bx-base-pofile-cover-info .bx-def-ava {
+    border-width:0.0625rem;
+    border-radius:0.1875rem;
+    -moz-border-radius:0.1875rem;
+    -webkit-border-radius:0.1875rem;
+}
+
+/*--- Set Role popup ---*/
+.bx-base-group-sr-role-cnt {
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+}
+.bx-base-group-sr-role-cnt > * {
+    display: inline-block;
+}
+.bx-base-group-sr-role-cnt > input {
+    margin: 0;
+}
+.bx-base-group-sr-done .bx-btn {
+    display: inline-block;
+    float: none;
+}

+ 94 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/modules/base/profile/classes/BxBaseModProfileUploaderCoverCrop.php

@@ -0,0 +1,94 @@
+<?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 BxBaseModProfileUploaderCoverCrop extends BxTemplUploaderHTML5
+{
+    protected $_sModule;
+    protected $_oModule;
+
+    public function __construct ($aObject, $sStorageObject, $sUniqId, $oTemplate)
+    {
+        if(!empty($this->_sModule) && empty($this->_oModule))
+            $this->_oModule = BxDolModule::getInstance($this->_sModule);
+
+        parent::__construct($aObject, $sStorageObject, $sUniqId, $oTemplate ? $oTemplate : $this->_oModule->_oTemplate);
+
+        $this->_sUploaderFormTemplate = 'uploader_form_crop_cover.html';
+    }
+
+    public function deleteGhostsForProfile($iProfileId, $iContentId = false)
+    {
+        if (!$iContentId)
+            return parent::deleteGhostsForProfile($iProfileId, $iContentId);
+
+        $CNF = $this->_oModule->_oConfig->CNF;
+        $aContentInfo = $this->_oModule->_oDb->getContentInfoById($iContentId);
+        
+        $iCount = 0;
+
+        $oStorage = BxDolStorage::getObjectInstance($this->_sStorageObject);
+
+        $aGhosts = $oStorage->getGhosts($iProfileId, $iContentId, $iContentId ? true : false);
+        foreach ($aGhosts as $aFile) {
+
+            // for cover image delete only unassigned ghosts and currently set covers
+            if ($aFile['id'] == $aContentInfo[$CNF['FIELD_COVER']] || $aFile['id'] != $aContentInfo[$CNF['FIELD_PICTURE']])
+                $iCount += $oStorage->deleteFile($aFile['id']);
+
+        }
+
+        return $iCount;
+    }
+    
+    public function getGhosts($iProfileId, $sFormat, $sImagesTranscoder = false, $iContentId = false)
+    {
+        $s = parent::getGhosts($iProfileId, $sFormat, $sImagesTranscoder, $iContentId);
+        if (!$s || !$iContentId) // if we're creating new profile return all ghosts
+            return $s;
+
+        $a = array();
+        if ($sFormat == 'array')
+            $a = $s;
+        else if ($sFormat == 'json')
+            $a = json_decode($s, true);
+
+        if (!$a)
+            return $s;
+
+        // filter out thumbnails
+        $CNF = $this->_oModule->_oConfig->CNF;
+        $aContentInfo = $this->_oModule->_oDb->getContentInfoById($iContentId);
+        $aResult = array();
+        foreach ($a as $aFile) {
+
+            // for cover image show only unassigned ghosts and currently set covers
+            if ($aFile['file_id'] == $aContentInfo[$CNF['FIELD_COVER']] || $aFile['file_id'] != $aContentInfo[$CNF['FIELD_PICTURE']])
+                $aResult[$aFile['file_id']] = $aFile;
+
+        }
+
+        if ('array' == $sFormat) {
+            return $aResult;
+        }
+        else if ('json' == $sFormat) {
+            return json_encode($aResult);
+        } else { // html format is not suported for this data type
+            return false;
+        }
+    }
+
+    protected function isAdmin ($iContentId = 0)
+    {
+        return $this->_oModule->_isModerator (false);
+    }
+}
+
+/** @} */

+ 92 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/modules/base/profile/classes/BxBaseModProfileUploaderPictureCrop.php

@@ -0,0 +1,92 @@
+<?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 BxBaseModProfileUploaderPictureCrop extends BxTemplUploaderCrop
+{
+    protected $_sModule;
+    protected $_oModule;
+
+    public function __construct ($aObject, $sStorageObject, $sUniqId, $oTemplate)
+    {
+        if(!empty($this->_sModule))
+            $this->_oModule = BxDolModule::getInstance($this->_sModule);
+
+        parent::__construct($aObject, $sStorageObject, $sUniqId, $oTemplate);
+        
+    }
+
+    public function deleteGhostsForProfile($iProfileId, $iContentId = false)
+    {
+        if (!$iContentId)
+            return parent::deleteGhostsForProfile($iProfileId, $iContentId);
+
+        $CNF = $this->_oModule->_oConfig->CNF;
+        $aContentInfo = $this->_oModule->_oDb->getContentInfoById($iContentId);
+        
+        $iCount = 0;
+
+        $oStorage = BxDolStorage::getObjectInstance($this->_sStorageObject);
+
+        $aGhosts = $oStorage->getGhosts($iProfileId, $iContentId, $iContentId ? true : false);
+        foreach ($aGhosts as $aFile) {
+
+            // for profile image delete only unassigned ghosts and currently set profile pictures
+            if ($aFile['id'] == $aContentInfo[$CNF['FIELD_PICTURE']] || $aFile['id'] != $aContentInfo[$CNF['FIELD_COVER']])
+                $iCount += $oStorage->deleteFile($aFile['id']);
+
+        }
+
+        return $iCount;
+    }
+    
+    public function getGhosts($iProfileId, $sFormat, $sImagesTranscoder = false, $iContentId = false)
+    {
+        $s = parent::getGhosts($iProfileId, $sFormat, $sImagesTranscoder, $iContentId);
+        if (!$s || !$iContentId) // if we're creating new profile return all ghosts
+            return $s;
+
+        $a = array();
+        if ($sFormat == 'array')
+            $a = $s;
+        else if ($sFormat == 'json')
+            $a = json_decode($s, true);
+
+        if (!$a)
+            return $s;
+
+        // filter out thumbnails
+        $CNF = $this->_oModule->_oConfig->CNF;
+        $aContentInfo = $this->_oModule->_oDb->getContentInfoById($iContentId);
+        $aResult = array();
+        foreach ($a as $aFile) {
+
+            // for profile pictures show only unassigned ghosts and currently set profile pictures
+            if (($aFile['file_id'] == $aContentInfo[$CNF['FIELD_PICTURE']] || $aFile['file_id'] != $aContentInfo[$CNF['FIELD_COVER']]))
+                $aResult[$aFile['file_id']] = $aFile;            
+        }
+
+        if ('array' == $sFormat) {
+            return $aResult;
+        }
+        else if ('json' == $sFormat) {
+            return json_encode($aResult);
+        } else { // html format is not suported for this data type
+            return false;
+        }
+    }
+
+    protected function isAdmin ($iContentId = 0)
+    {
+        return $this->_oModule->_isModerator (false);
+    }
+}
+
+/** @} */

+ 23 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/modules/base/profile/template/unit.html

@@ -0,0 +1,23 @@
+<div class="__class__ bx-def-unit flex items-center w-46 mb-4 mr-2">
+    <div class="bx-def-unit-img bx-def-__size__ bx-def-__size__-size __class_size__ relative flex-none">
+        <a class="text-center hover:no-underline" href="__content_url__" onclick="__content_click__">
+            <bx_if:show_thumb_image>
+                <img class="bx-base-pofile-unit-thumb bx-def-__size__ bx-def-__size__-size __class_size__ rounded-full 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 box-border m-0 rounded-full text-white font-bold" style="background-color:rgba(__color__)">__letter__</p>
+            </bx_if:show_thumb_letter>
+        </a>
+        <bx_if:show_online>
+            <div class="bx-base-pofile-unit-online"></div>
+        </bx_if:show_online>
+    </div>
+    <div class="bx-def-unit-info bx-def-__size__-size-min-height bx-def-__size__-size-max-height flex items-center max-w-32 ltr:ml-4 rtl:mr-4">
+        <div class="bx-def-unit-info-cnt box-border overflow-hidden">
+            <div class="bx-def-unit-line truncate">
+                <a class="bx-def-unit-info-title" href="__content_url__" onclick="__content_click__">__title__</a>
+            </div>
+            <div class="bx-def-unit-line text-sm text-gray-500 dark:text-gray-400">__addon__</div>
+        </div>
+    </div>
+</div>

+ 268 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/modules/base/text/template/css/main.css

@@ -0,0 +1,268 @@
+/* unit */
+.bx-base-text-unit h2,
+.bx-base-text-unit h1 {
+    margin:0;
+}
+
+.bx-base-text-unit .sys-icon {
+    vertical-align:middle;
+}
+
+.bx-base-text-unit-cnt {
+    width: 100%;
+}
+
+.bx-base-text-unit-no-thumb,
+.bx-base-text-unit-thumb {
+    display:block;
+    overflow:hidden;    
+}
+
+.bx-base-text-unit-title,
+.bx-base-text-unit-summary {
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+.bx-base-text-unit-summary p {
+    margin:0;
+}
+
+.bx-base-text-unit-cover {
+    position: relative;
+}
+.bx-base-text-unit-text {
+    width: 100%;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+.bx-base-text-unit-title {
+    display: flex;
+    white-space: nowrap;
+}
+.bx-media-phone .bx-base-text-unit-title {
+    white-space: normal;
+}
+
+.bx-base-text-unit-title,
+.bx-base-text-unit-title h2 {
+    text-overflow: ellipsis;
+    overflow: hidden;
+}
+
+.bx-base-text-unit-cover img {
+    position: relative;
+    display: block;
+
+    width: 100%;
+    min-height: 300px;
+    object-fit: cover;
+}
+.bx-media-phone .bx-base-text-unit-cover img {
+    height: 150px;
+}
+
+.bx-base-text-unit-info {
+    white-space:nowrap;
+    overflow:hidden;
+    text-overflow:ellipsis;
+}
+
+.bx-base-text-unit-date {
+    white-space: nowrap;
+}
+
+.bx-base-text-unit-gallery .bx-base-text-unit-no-thumb,
+.bx-base-text-unit-gallery .bx-base-text-unit-thumb {
+    float:none;
+    display:block;
+    width:100%;
+
+    background-size:cover;
+    background-repeat:no-repeat;
+    background-position:center center;
+
+    line-height:1.25em;
+}
+
+.bx-base-text-unit-gallery h2 {
+    margin-bottom: 0;
+    max-height: 3.75em;
+    line-height: 1.25em;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    text-align: left;
+    overflow: hidden;
+}
+
+.bx-base-text-wrapper {
+    margin-bottom:0;
+}
+
+.bx-base-text-unit-ext .bx-base-text-unit-thumb {
+    float:left;
+}
+
+.bx-base-text-unit-strecher {
+    visibility: hidden;
+}
+
+/* unit meta */
+.bx-base-general-unit-meta ul.bx-menu-custom.bx-menu-custom-hor > li {
+    margin-left: 0;
+}
+
+
+/* flexbox */
+.bx-base-text-unit-gallery-wrapper {
+    display: -webkit-box;
+    display: -webkit-flex;
+    display: -ms-flexbox;
+    display: flex;
+
+    /* makes all items on a row equal height */
+    -webkit-box-align: stretch;
+    -webkit-align-items: stretch;
+    -ms-flex-align: stretch;
+    align-items: stretch;
+
+    /* wrap onto the next line rather than stretching or shrinking */
+    -webkit-flex-wrap: wrap;
+    -ms-flex-wrap: wrap;
+    flex-wrap: wrap;
+
+    /* tells flexbox to start items from the left */
+    -webkit-box-pack: start;
+    -ms-flex-pack: start;
+    flex-pack: start;
+    -webkit-justify-content: flex-start;
+    justify-content: flex-start;
+}
+
+.bx-base-text-unit-gallery-wrapper .bx-base-text-unit-gallery {
+    display: -webkit-box;
+    display: -webkit-flex;
+    display: -ms-flexbox;
+    display: flex;
+
+    -webkit-box-flex: 0;
+
+    -webkit-flex: 0 0 100%;
+    -ms-flex: 0 0 100%;
+    flex: 0 0 100%;
+
+    overflow:hidden;
+}
+
+/* MEDIA: phone2 */
+
+.bx-media-phone2 .bx-layout-column-half .bx-base-text-unit-gallery-wrapper .bx-base-text-unit-gallery,
+.bx-media-phone2 .bx-layout-column-thin .bx-base-text-unit-gallery-wrapper .bx-base-text-unit-gallery {
+    -webkit-flex: 0 0 49.8%;
+    -ms-flex: 0 0 49.8%;
+    flex: 0 0 49.8%;
+}
+
+.bx-media-phone2 .bx-base-unit-showcase-wrapper .bx-base-text-unit-showcase {
+   width:calc(50%);
+}
+
+/* MEDIA: tablet2 */
+
+.bx-media-tablet2 .bx-layout-column-wide .bx-base-text-unit-gallery-wrapper .bx-base-text-unit-gallery,
+.bx-media-tablet2 #sys_search_results .bx-base-text-unit-gallery-wrapper .bx-base-text-unit-gallery,
+.bx-media-tablet2 .bx-layout-column-fullwidth .bx-base-text-unit-gallery-wrapper .bx-base-text-unit-gallery {
+    -webkit-flex: 0 0 49.8%;
+    -ms-flex: 0 0 49.8%;
+    flex: 0 0 49.8%;
+}
+
+.bx-media-tablet2 .bx-base-unit-showcase-wrapper .bx-base-text-unit-showcase {
+   width:calc(50%);
+}
+
+/* MEDIA: desktop */
+
+.bx-media-desktop .bx-layout-column-wide .bx-base-text-unit-gallery-wrapper .bx-base-text-unit-gallery {
+    -webkit-flex: 0 0 49.8%;
+    -ms-flex: 0 0 49.8%;
+    flex: 0 0 49.8%;
+}
+.bx-media-desktop #sys_search_results .bx-base-text-unit-gallery-wrapper .bx-base-text-unit-gallery,
+.bx-media-desktop .bx-layout-column-fullwidth .bx-base-text-unit-gallery-wrapper .bx-base-text-unit-gallery {
+    -webkit-flex: 0 0 33.25%;
+    -ms-flex: 0 0 33.25%;
+    flex: 0 0 33.25%;
+}
+
+.bx-media-desktop .bx-layout-column-thin .bx-base-unit-showcase-wrapper .bx-base-text-unit-showcase {
+   width:calc(100%);
+}
+
+.bx-media-desktop .bx-layout-column-half .bx-base-unit-showcase-wrapper .bx-base-text-unit-showcase,
+.bx-media-desktop .bx-layout-column-wide .bx-base-unit-showcase-wrapper .bx-base-text-unit-showcase {
+   width:calc(50%);
+}
+
+.bx-media-desktop #sys_search_results  .bx-base-unit-showcase-wrapper .bx-base-text-unit-showcase,
+.bx-media-desktop .bx-layout-column-fullwidth  .bx-base-unit-showcase-wrapper .bx-base-text-unit-showcase {
+   width:calc(33.3%);
+}
+
+
+    
+/* view page */
+.bx-base-text-title-bages h1 {
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+.bx-content-description-after .bx-vote-counter-wrapper {
+    padding-top: 1rem;
+    line-height: 2.5rem;
+    font-size: 1.5rem;
+}
+
+.bx-base-text-entity-image img {
+    width: 100%;
+}
+
+.bx-base-text-entity-actions-all .bx-base-text-entity-share,
+.bx-base-text-entity-actions-all .bx-menu-btns-hor {
+    display:inline-block;
+    vertical-align:top;
+}
+
+/* MEDIA: desktop */
+
+.bx-media-desktop .bx-layout-column-wide .bx-base-pofile-units-wrapper .bx-base-pofile-unit-with-cover {
+    -webkit-flex: 0 0 49.8%;
+    -ms-flex: 0 0 49.8%;
+    flex: 0 0 49.8%;
+}
+.bx-media-desktop #sys_search_results .bx-base-pofile-units-wrapper .bx-base-pofile-unit-with-cover,
+.bx-media-desktop .bx-layout-column-fullwidth .bx-base-pofile-units-wrapper .bx-base-pofile-unit-with-cover {
+    -webkit-flex: 0 0 33.25%;
+    -ms-flex: 0 0 33.25%;
+    flex: 0 0 33.25%;
+}
+
+.bx-media-desktop .bx-base-unit-showcase-wrapper .bx-base-pofile-unit-showcase {
+    width:calc(33.3%);
+}
+.bx-media-desktop .bx-layout-column-thin .bx-base-unit-showcase-wrapper .bx-base-pofile-unit-showcase {
+    width:calc(100%);
+}
+.bx-media-desktop .bx-layout-column-half .bx-base-unit-showcase-wrapper .bx-base-pofile-unit-showcase {
+    width:calc(50%);
+}
+
+.bx-media-desktop .bx-base-unit-showcase-wrapper .bx-base-pofile-unit-wo-info-showcase {
+    width:calc(6.25%);
+}
+.bx-media-desktop .bx-layout-column-thin .bx-base-unit-showcase-wrapper .bx-base-pofile-unit-wo-info-showcase {
+    width:calc(25.0%);
+}
+.bx-media-desktop .bx-layout-column-half .bx-base-unit-showcase-wrapper .bx-base-pofile-unit-wo-info-showcase {
+    width:calc(10.0%);
+}

+ 7 - 0
upgrade/files/14.0.0.RC3-14.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 ComposerAutoloaderInit43178b10e702286464b42711c8770d30::getLoader();

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

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

+ 226 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/plugins/composer/autoload_static.php

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

+ 366 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/plugins/composer/installed.php

@@ -0,0 +1,366 @@
+<?php return array(
+    'root' => array(
+        'pretty_version' => 'dev-master',
+        'version' => 'dev-master',
+        'type' => 'library',
+        'install_path' => __DIR__ . '/../../',
+        'aliases' => array(),
+        'reference' => '3753d96129980c541caa599e230c68919aa5887b',
+        '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' => 'deeafb9e3f041c3ce851cd6abbe0fb30e6e4060d',
+            'dev_requirement' => false,
+        ),
+        'avto-dev/composer-cleanup-plugin' => array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'type' => 'composer-plugin',
+            'install_path' => __DIR__ . '/../avto-dev/composer-cleanup-plugin',
+            'aliases' => array(
+                0 => '9999999-dev',
+            ),
+            'reference' => '75187ca4854265174021765e3b67cb973dc37723',
+            'dev_requirement' => false,
+        ),
+        'bshaffer/oauth2-server-php' => array(
+            'pretty_version' => 'v1.14.1',
+            'version' => '1.14.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../bshaffer/oauth2-server-php',
+            'aliases' => array(),
+            'reference' => '096db2c86a7d67a2ba45e72be7d208c342694542',
+            'dev_requirement' => false,
+        ),
+        'chargebee/chargebee-php' => array(
+            'pretty_version' => 'v3.37.0',
+            'version' => '3.37.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../chargebee/chargebee-php',
+            'aliases' => array(),
+            'reference' => '11b3d9407ed9886fee49fdda072200e62053ecc3',
+            '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' => '999fcf788675b65b5344e2744443fc4d5f76cc14',
+            'dev_requirement' => false,
+        ),
+        'ezyang/htmlpurifier' => array(
+            'pretty_version' => 'v4.18.0',
+            'version' => '4.18.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../ezyang/htmlpurifier',
+            'aliases' => array(),
+            'reference' => 'cb56001e54359df7ae76dc522d08845dc741621b',
+            '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.9.2',
+            'version' => '7.9.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/guzzle',
+            'aliases' => array(),
+            'reference' => 'd281ed313b989f213357e3be1a179f02196ac99b',
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/promises' => array(
+            'pretty_version' => '2.0.4',
+            'version' => '2.0.4.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/promises',
+            'aliases' => array(),
+            'reference' => 'f9c436286ab2892c7db7be8c8da4ef61ccf7b455',
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/psr7' => array(
+            'pretty_version' => '2.7.0',
+            'version' => '2.7.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/psr7',
+            'aliases' => array(),
+            'reference' => 'a70f5c95fb43bc83f07c9c948baa0dc1829bf201',
+            '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.73',
+            'version' => '1.3.73.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../matthiasmullie/minify',
+            'aliases' => array(),
+            'reference' => 'cb7a9297b4ab070909cefade30ee95054d4ae87a',
+            '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.41',
+            'version' => '2.0.41.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../mpratt/embera',
+            'aliases' => array(),
+            'reference' => '069305b9252a428ba4ae6eb58d3dc8c6d9be5cd0',
+            'dev_requirement' => false,
+        ),
+        'paragonie/sodium_compat' => array(
+            'pretty_version' => 'v2.1.0',
+            'version' => '2.1.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../paragonie/sodium_compat',
+            'aliases' => array(),
+            'reference' => 'a673d5f310477027cead2e2f2b6db5d8368157cb',
+            'dev_requirement' => false,
+        ),
+        'paypal/paypal-checkout-sdk' => array(
+            'pretty_version' => '1.0.2',
+            'version' => '1.0.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../paypal/paypal-checkout-sdk',
+            'aliases' => array(),
+            'reference' => '19992ce7051ff9e47e643f28abb8cc1b3e5f1812',
+            'dev_requirement' => false,
+        ),
+        'paypal/paypalhttp' => array(
+            'pretty_version' => '1.0.1',
+            'version' => '1.0.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../paypal/paypalhttp',
+            'aliases' => array(),
+            'reference' => '7b09c89c80828e842c79230e7f156b61fbb68d25',
+            'dev_requirement' => false,
+        ),
+        'phpmailer/phpmailer' => array(
+            'pretty_version' => 'v6.9.3',
+            'version' => '6.9.3.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../phpmailer/phpmailer',
+            'aliases' => array(),
+            'reference' => '2f5c94fe7493efc213f643c23b1b1c249d40f47e',
+            'dev_requirement' => false,
+        ),
+        'psr/http-client' => array(
+            'pretty_version' => '1.0.3',
+            'version' => '1.0.3.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-client',
+            'aliases' => array(),
+            'reference' => 'bb5906edc1c324c9a05aa0873d40117941e5fa90',
+            'dev_requirement' => false,
+        ),
+        'psr/http-client-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        'psr/http-factory' => array(
+            'pretty_version' => '1.1.0',
+            'version' => '1.1.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-factory',
+            'aliases' => array(),
+            'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a',
+            'dev_requirement' => false,
+        ),
+        'psr/http-factory-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        'psr/http-message' => array(
+            'pretty_version' => '2.0',
+            'version' => '2.0.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-message',
+            'aliases' => array(),
+            'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71',
+            'dev_requirement' => false,
+        ),
+        'psr/http-message-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        'psr/log' => array(
+            'pretty_version' => '3.0.2',
+            'version' => '3.0.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/log',
+            'aliases' => array(),
+            'reference' => 'f16e1d5863e37f8d8c2a01719f5b34baa2b714d3',
+            'dev_requirement' => false,
+        ),
+        'pusher/pusher-php-server' => array(
+            'pretty_version' => '7.2.6',
+            'version' => '7.2.6.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../pusher/pusher-php-server',
+            'aliases' => array(),
+            'reference' => 'd89e9997191d18fb0fe03a956fa3ccfe0af524ea',
+            'dev_requirement' => false,
+        ),
+        'ralouphie/getallheaders' => array(
+            'pretty_version' => '3.0.3',
+            'version' => '3.0.3.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../ralouphie/getallheaders',
+            'aliases' => array(),
+            'reference' => '120b605dfeb996808c31b6477290a714d356e822',
+            'dev_requirement' => false,
+        ),
+        'snipe/banbuilder' => array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../snipe/banbuilder',
+            'aliases' => array(
+                0 => '9999999-dev',
+            ),
+            'reference' => '5e76a97b3ff80e2beaae75ac140c38573ea027a0',
+            'dev_requirement' => false,
+        ),
+        'spatie/calendar-links' => array(
+            'pretty_version' => '1.11.0',
+            'version' => '1.11.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../spatie/calendar-links',
+            'aliases' => array(),
+            'reference' => 'da41e7edae84859d3d598b0b3ce3233c843c3e46',
+            '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' => 'v3.5.1',
+            'version' => '3.5.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
+            'aliases' => array(),
+            'reference' => '74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6',
+            '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' => '8.3.8',
+            'version' => '8.3.8.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../twilio/sdk',
+            'aliases' => array(),
+            'reference' => 'd05c08d0b9ed017f61ebbc31bc4e5f4d758a0326',
+            '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' => '3753d96129980c541caa599e230c68919aa5887b',
+            'dev_requirement' => false,
+        ),
+        'wikimedia/less.php' => array(
+            'pretty_version' => 'v5.1.2',
+            'version' => '5.1.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../wikimedia/less.php',
+            'aliases' => array(),
+            'reference' => '39b1fbbdd3d95c5279f231d2298f3866fa869fcd',
+            'dev_requirement' => false,
+        ),
+    ),
+);

File diff suppressed because it is too large
+ 315 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/plugins_public/highlight/highlight.min.js


+ 1 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/plugins_public/history.adapter.jquery.js

@@ -0,0 +1 @@
+(function(e,t){"use strict";var n=e.History=e.History||{},r=e.jQuery;if(typeof n.Adapter!="undefined")throw new Error("History.js Adapter has already been loaded...");n.Adapter={bind:function(e,t,n){r(e).bind(t,n)},trigger:function(e,t,n){r(e).trigger(t,n)},extractEventData:function(e,n,r){var i=n&&n.originalEvent&&n.originalEvent[e]||r&&r[e]||t;return i},onDomLoad:function(e){r(e)}},typeof n.init!="undefined"&&n.init()})(window)

File diff suppressed because it is too large
+ 0 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/plugins_public/tailwind/css/tailwind.max.css


File diff suppressed because it is too large
+ 0 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/plugins_public/tailwind/css/tailwind.min.css


+ 271 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/classes/BxDolStudioAgentsAsstChatsCmts.php

@@ -0,0 +1,271 @@
+<?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 BxDolStudioAgentsAsstChatsCmts extends BxTemplCmts
+{
+    protected static $_sPrefixLoad = '#-#';
+    protected static $_sPrefixRetrieve = '|-|';
+    protected static $_sParamAllowDelete = 'allow_delete';
+
+    protected $_oQueryAgents;
+    
+    protected $_oAI;
+    protected $_iProfileIdAi;
+    
+    protected $_iAssistantId;
+    protected $_sAssistantUrl;
+
+    protected $_bAuto;
+
+    public function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
+    {
+        parent::__construct($sSystem, $iId, $iInit, $oTemplate);
+
+        if ($oTemplate)
+            $this->_oTemplate = $oTemplate;
+        else
+            $this->_oTemplate = BxDolStudioTemplate::getInstance();
+                
+        $this->_sTmplNameItemContent = 'agents_comment_content.html';
+        $this->_bLiveUpdates = false;
+
+        $this->_oQueryAgents = new BxDolStudioAgentsQuery();
+
+        $this->_oAI = BxDolAI::getInstance();
+        $this->_iProfileIdAi = $this->_oAI->getProfileId();
+
+        $this->_iAssistantId = 0;
+        if(($iAssistantId = bx_get('aid')) !== false) {
+            $this->_iAssistantId = bx_process_input($iAssistantId, BX_DATA_INT);
+            $this->_aMarkers['assistant_id'] = $this->_iAssistantId;
+        }
+        $this->_sAssistantUrl = BX_DOL_URL_STUDIO . bx_append_url_params('agents.php', ['page' => 'assistants', 'spage' => 'chats', 'aid' => $this->_iAssistantId]);
+
+        if(!$this->isParam(self::$_sParamAllowDelete))
+            $this->setAllowDelete(true);
+
+        $this->_bAuto = false;
+    }
+
+    public function actionGetCmt ()
+    {
+        if(!$this->isEnabled())
+            return echoJson([]);
+
+        if($this->isViewAllowed() !== CHECK_ACTION_RESULT_ALLOWED)
+            return echoJson([]);
+
+        $mixedCmtId = bx_process_input(bx_get('Cmt'));
+        $sCmtBrowse = ($sCmtBrowse = bx_get('CmtBrowse')) !== false ? bx_process_input($sCmtBrowse, BX_DATA_TEXT) : '';
+        $sCmtDisplay = ($sCmtDisplay = bx_get('CmtDisplay')) !== false ? bx_process_input($sCmtDisplay, BX_DATA_TEXT) : '';
+
+        $aCmtIds = strpos($mixedCmtId, ',') !== false ? explode(',', $mixedCmtId) : [$mixedCmtId];
+
+        $sContent = '';
+        foreach($aCmtIds as $iCmtId)
+            $sContent .= $this->getComment((int)$iCmtId, ['type' => $sCmtBrowse], ['type' => $sCmtDisplay, 'dynamic_mode' => true]);
+
+        $aCmt = $this->getCommentRow((int)reset($aCmtIds));
+        echoJson([
+            'parent_id' => $aCmt['cmt_parent_id'],
+            'vparent_id' => $aCmt['cmt_parent_id'],
+            'content' => $sContent
+        ]);
+    }
+
+    public function getPageJsObject()
+    {
+        return 'oBxDolStudioPageAgents';
+    }
+
+    public function getCommentsBlock($aBp = [], $aDp = [])
+    {
+        $aComments = parent::getCommentsBlock($aBp, array_merge($aDp, ['in_designbox' => false]));
+        if(empty($aComments['content']))
+            return MsgBox(_t('_error occured'));
+
+        $aChat = $this->_oQueryAgents->getChatsBy(['sample' => 'id', 'id' => (int)$this->getId()]);
+        if(empty($aChat) || !is_array($aChat))
+            return MsgBox(_t('_error occured'));
+        
+        $aAssistant = $this->_oQueryAgents->getAssistantsBy(['sample' => 'id', 'id' => (int)$aChat['assistant_id']]);
+        if(empty($aAssistant) || !is_array($aAssistant))
+            return MsgBox(_t('_error occured'));
+
+        return $aComments['content'];
+    }
+
+    public function getComment($mixedCmt, $aBp = [], $aDp = [])
+    {
+        $aCmt = !is_array($mixedCmt) ? $this->getCommentRow((int)$mixedCmt) : $mixedCmt;
+        if(!$aCmt)
+            return '';
+
+        $iObjId = (int)$this->getId();
+        $iCmtId = (int)$aCmt['cmt_id'];
+        if($this->_isShowAnswerLoad($aCmt))
+            $this->_oQuery->updateComments(['cmt_text' => str_replace(self::$_sPrefixLoad, self::$_sPrefixRetrieve, $aCmt['cmt_text'])], ['cmt_id' => $iCmtId]);
+        else if($this->_isShowAnswerRetrieve($aCmt) && ($mixedAiData = $this->_getAiData()) !== false) {
+            list($iModelId, $sAssistantId, $sThreadId) = $mixedAiData;
+
+            $aCmtRequest = $this->getCommentRow((int)str_replace(self::$_sPrefixRetrieve, '', $aCmt['cmt_text']));
+
+            if(($oAIModel = $this->_oAI->getModelObject($iModelId)) !== false) {
+                if(empty($sThreadId) && ($aResponseInit = $oAIModel->getResponseInit(BX_DOL_AI_ASSISTANT, $aCmtRequest['cmt_text'], ['assistant_id' => $sAssistantId])) !== false) {
+                    $sThreadId = $aResponseInit['params']['thread_id'];
+
+                    $this->_oAI->updateAssistantChatById($iObjId, [
+                        'ai_thread_id' => $sThreadId
+                    ]);
+                }
+
+                if(!empty($sThreadId) && ($sResponse = $oAIModel->getResponse(BX_DOL_AI_ASSISTANT, $aCmtRequest['cmt_text'], ['thread_id' => $sThreadId, 'assistant_id' => $sAssistantId])) !== false) {
+                    $this->_oQuery->updateComments(['cmt_text' => $sResponse], ['cmt_id' => $iCmtId]);
+
+                    $aCmt['cmt_text'] = $sResponse;
+                }
+            }
+        }
+
+        return parent::getComment($aCmt, $aBp, $aDp);
+    }
+
+    public function isAttachImageEnabled()
+    {
+        return false;
+    }
+
+    public function isAllowDelete()
+    {
+        return !$this->isParam(self::$_sParamAllowDelete) || (int)$this->getParam(self::$_sParamAllowDelete) != 0;
+    }
+
+    public function setAllowDelete($bAllow)
+    {
+        $this->setParam(self::$_sParamAllowDelete, $bAllow ? 1 : 0);
+    }
+
+    public function addAuto($aValues, $bUnsetForm = false)
+    {
+        $this->_bAuto = true;
+        $mixedResult = $this->add($aValues);
+        $this->_bAuto = false;
+
+        if($bUnsetForm)
+            $this->_unsetFormObject();
+
+        return $mixedResult;
+    }
+
+    public function onPostAfter($iCmtId, $aDp = [])
+    {
+        $mixedResult = parent::onPostAfter($iCmtId, $aDp);
+        if($this->_bAuto || $mixedResult === false) 
+            return $mixedResult;
+
+        $mixedResultAuto = $this->addAuto([
+            'cmt_author_id' => $this->_iProfileIdAi,
+            'cmt_parent_id' => 0,
+            'cmt_text' => self::$_sPrefixLoad . $iCmtId
+        ], true);
+
+        if($mixedResultAuto !== false)
+            $mixedResult['id'] .= ',' . $mixedResultAuto['id'];
+
+        return $mixedResult;
+    }
+
+    protected function _getActionsBox(&$aCmt, $aBp = [], $aDp = [])
+    {
+        if(!$this->isAllowDelete())
+            return '';
+
+        return $this->_oTemplate->parseLink('javascript:void(0)', _t('_sys_menu_item_title_cmts_item_delete'), [
+            'class' => 'bx-btn bx-btn-small',
+            'onclick' => $this->_sJsObjName . '.cmtRemove(this, ' . $aCmt['cmt_id'] . ')',
+        ]);
+    }
+
+    protected function _getCountersBox(&$aCmt, $aBp = [], $aDp = [])
+    {
+        return '';
+    }
+
+    protected function _getFormBox($sType, $aBp, $aDp)
+    {
+        return parent::_getFormBox($sType, $aBp, array_merge($aDp, ['min_post_form' => false]));
+    }
+    
+    protected function _getForm($sAction, $iId, $aDp = [])
+    {
+        $oForm = parent::_getForm($sAction, $iId, $aDp);
+
+        $oForm->aInputs['cmt_text']['caption'] = '';
+        $oForm->aInputs['cmt_text']['db']['pass'] = 'xss';
+
+        if(isset($aDp['mode']) && $aDp['mode'] == 'compact')
+            $oForm->aInputs['cmt_submit']['value'] = $this->_oTemplate->parseIcon('arrow-right');
+
+        return $oForm;
+    }
+
+    protected function _getTmplVarsText($aCmt)
+    {
+        $bLoad = $this->_isShowAnswerLoad($aCmt);
+        $bRetrieve = $this->_isShowAnswerRetrieve($aCmt);
+
+        $iId = (int)$aCmt['cmt_id'];
+        $sText = $aCmt['cmt_text'];
+        if(!$bLoad && !$bRetrieve)
+            $sText = nl2br($this->_prepareTextForOutput($sText, $iId));
+
+        if($bLoad) {
+            $sLoadingId = 'cmt' . $iId . '-loading';
+            $sText = $this->_oTemplate->parseLink('javascript:void(0)', _t('_sys_loading'), [
+                'id' => $sLoadingId, 
+                'class' => 'bx-btn bx-btn-small bx-agents-loading'
+            ]);
+
+            $sText .= $this->_oTemplate->_wrapInTagJsCode("bx_loading('" . $sLoadingId . "', true); var " . $this->_sJsObjName . "_interval = setInterval(function() {if(window['" . $this->_sJsObjName . "'] == undefined) return; clearInterval(" . $this->_sJsObjName . "_interval); " . $this->getJsObjectName() . "._getCmt('#cmt" . $iId . "', " . $iId . ");}, 200)");
+        }
+        
+        if($bRetrieve)
+            $sText = _t('_sys_agents_assistants_chats_err_no_response');
+
+        return [
+            'style_prefix' => $this->_sStylePrefix,
+            'text' => $sText
+        ];
+    }
+
+    protected function _getAiData()
+    {
+        $aChat = $this->_oAI->getAssistantChatById((int)$this->getId());
+        if(empty($aChat) || !is_array($aChat))
+            return false;
+
+        $aAssistant = $this->_oAI->getAssistantById($aChat['assistant_id']);
+        if(empty($aAssistant) || !is_array($aAssistant))
+            return false;
+
+        return [$aAssistant['model_id'], $aAssistant['ai_asst_id'], $aChat['ai_thread_id']];
+    }
+
+    protected function _isShowAnswerLoad($aCmt)
+    {
+        return $aCmt['cmt_author_id'] == $this->_iProfileIdAi && strpos($aCmt['cmt_text'], self::$_sPrefixLoad) !== false;
+    }
+    
+    protected function _isShowAnswerRetrieve($aCmt)
+    {
+        return $aCmt['cmt_author_id'] == $this->_iProfileIdAi && strpos($aCmt['cmt_text'], self::$_sPrefixRetrieve) !== false;
+    }
+}
+
+/** @} */

+ 181 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/classes/BxDolStudioFormsField.php

@@ -0,0 +1,181 @@
+<?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
+ * @{
+ */
+
+bx_import('BxDolStudioForm');
+
+class BxDolStudioFormsField extends BxDol
+{
+    protected $oDb;
+    protected $aTypes = array();
+    protected $aTypesRelated = array();
+
+    protected $sType = '';
+    protected $aCheckFunctions = array();
+    protected $sDbPass = '';
+    protected $aDbPassDependency = array();
+
+    protected $aParams = array();
+    protected $aField = array();
+    protected $iFieldNameMaxLen;
+    protected $aFormObject = array();
+    
+    public function __construct($aParams = array(), $aField = array())
+    {
+        parent::__construct();
+
+        $this->oDb = new BxDolStudioFormsQuery();
+
+        $this->aTypes = array(
+        	'block_header' => array('add' => 1), 
+        	'block_end' => array('add' => 1), 
+        	'text' => array('add' => 1), 
+        	'datepicker' => array('add' => 1), 
+        	'datetime' => array('add' => 1), 
+        	'number' => array('add' => 1), 
+                'price' => array('add' => 1), 
+            'time' => array('add' => 1), 
+        	'checkbox' => array('add' => 1), 
+        	'password' => array('add' => 1), 
+        	'slider' => array('add' => 1), 
+        	'doublerange' => array('add' => 1), 
+        	'hidden' => array('add' => 1), 
+        	'switcher' => array('add' => 1), 
+        	'reset' => array('add' => 1), 
+        	'submit' => array('add' => 1), 
+        	'textarea' => array('add' => 1), 
+        	'select' => array('add' => 1), 
+        	'select_multiple' => array('add' => 1), 
+        	'checkbox_set' => array('add' => 1), 
+        	'radio_set' => array('add' => 1), 
+        	'value' => array('add' => 1), 
+        	'file' => array('add' => 1), 
+        	'files' => array('add' => 1),
+            'nested_form' => array('add' => 1), 
+        	'captcha' => array('add' => 0),
+        	'location' => array('add' => 0), 
+        	'custom' => array('add' => 0)
+        );
+
+        $this->aTypesRelated = array(
+            'select_multiple' => array('types' => array('select_multiple', 'checkbox_set'), 'reload_on_change' => 0),
+            'checkbox_set' => array('types' => array('select_multiple', 'checkbox_set'), 'reload_on_change' => 0),
+
+            'select' => array('types' => array('select', 'radio_set'), 'reload_on_change' => 0),
+            'radio_set' => array('types' => array('select', 'radio_set'), 'reload_on_change' => 0),
+
+            'datepicker' => array('types' => array('datepicker', 'datetime', 'dateselect'), 'reload_on_change' => 1),
+            'datetime' => array('types' => array('datepicker', 'datetime', 'dateselect'), 'reload_on_change' => 1),
+            'dateselect' => array('types' => array('datepicker', 'datetime', 'dateselect'), 'reload_on_change' => 1),
+
+            'checkbox' => array('types' => array('checkbox', 'switcher'), 'reload_on_change' => 0),
+            'switcher' => array('types' => array('checkbox', 'switcher'), 'reload_on_change' => 0),
+
+            'number' => array('types' => array('number', 'slider'), 'reload_on_change' => 1),
+            'slider' => array('types' => array('number', 'slider'), 'reload_on_change' => 1),
+        );
+
+        $this->aParams = $aParams;
+        
+        $this->aFormObject = BxDolFormQuery::getFormObject ($this->aParams['object']); 
+        if ($this->isNested()){
+            $this->aTypes['nested_form']['add'] = 0;
+            $this->aTypes['file']['add'] = 0;
+            $this->aTypes['files']['add'] = 0;
+        }
+       
+        $this->aField = $aField;
+        $this->iFieldNameMaxLen = 32;
+    }
+
+    public function init()
+    {
+    	if(isset($this->aParams['object']) && isset($this->aParams['display'])) {
+            $aForm = array();
+            $this->oDb->getForms(array('type' => 'by_object_display', 'object' => $this->aParams['object'], 'display' => $this->aParams['display']), $aForm, false);
+
+            $this->aParams['table'] = $aForm['table'];
+        }
+    }
+
+    public function canAdd()
+    {
+        return isset($this->aParams['table']) && !empty($this->aParams['table']);
+    }
+
+    public function isNested()
+    {
+        return $this->aFormObject['parent_form'] != '' ? true : false;
+    }
+    
+    /**
+     * Checks whether the field exists in connected table.
+     * @param type $sName - field name
+     * @return boolean - check result
+     */
+    public function isField($sName)
+    {
+        if(!isset($this->aParams['table_alter']) || $this->aParams['table_alter'] !== true)
+            return false;
+
+        if(!isset($this->aParams['table']))
+            return false;
+
+        return $this->oDb->isFieldExists($this->aParams['table'], $sName);
+    }
+
+    public function alterAdd($sName)
+    {
+        if(!isset($this->aParams['table_alter']) || $this->aParams['table_alter'] !== true)
+            return true;
+
+        if(!isset($this->aParams['table'], $this->aParams['table_field_type']))
+            return false;
+
+        return $this->oDb->alterAdd($this->aParams['table'], $sName, $this->aParams['table_field_type']);
+    }
+
+    public function alterChange($sNameOld, $sNameNew)
+    {
+        if(!isset($this->aParams['table_alter']) || $this->aParams['table_alter'] !== true)
+            return true;
+
+        if(!isset($this->aParams['table'], $this->aParams['table_field_type']))
+            return false;
+
+        return $this->oDb->alterChange($this->aParams['table'], $sNameOld, $sNameNew, $this->aParams['table_field_type']);
+    }
+
+    public function alterRemove($sName)
+    {
+        if(!isset($this->aParams['table_alter']) || $this->aParams['table_alter'] !== true)
+            return true;
+
+        if(!isset($this->aParams['table']))
+            return false;
+
+        return $this->oDb->alterRemove($this->aParams['table'], $sName);
+    }
+
+    protected function getSystemName($sValue)
+    {
+        return BxDolStudioUtils::getSystemName($sValue);
+    }
+
+    protected function getClassName($sValue)
+    {
+        return BxDolStudioUtils::getClassName($sValue);
+    }
+
+    protected function addInArray($aInput, $sKey, $aValues, $bAddAfter = true)
+    {
+        return BxDolStudioUtils::addInArray($aInput, $sKey, $aValues, $bAddAfter);
+    }
+}
+
+/** @} */

+ 186 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/classes/BxDolStudioTemplate.php

@@ -0,0 +1,186 @@
+<?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_PAGE_COLUMN_DUAL', 3); ///< page, with 2 columns
+
+class BxDolStudioTemplate extends BxDolTemplate 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();
+
+        $this->_sRootPath = BX_DOL_DIR_STUDIO;
+        $this->_sRootUrl = BX_DOL_URL_STUDIO;
+        $this->_sPrefix = 'BxDolStudioTemplate';
+        $this->_sInjectionsTable = 'sys_injections_admin';
+        $this->_sInjectionsCache = 'sys_injections_admin.inc';
+
+        $aCode = self::retrieveCode();
+
+        $this->_sCodeKey = BX_DOL_STUDIO_TEMPLATE_CODE_KEY;
+        $aCodeStudio = self::retrieveCode($this->_sCodeKey, $this->_sMixKey, $this->_sRootPath);
+
+        $sCodeDefault = getParam('template');
+        if($aCodeStudio !== false && $aCodeStudio[0] != $sCodeDefault)
+            $aCode = $aCodeStudio;
+
+        list(
+            $this->_sCode, 
+            $this->_sName, 
+            $this->_sSubPath
+        ) = $aCode;
+
+        $this->_iMix = 0;
+        if(is_array($this->_sCode))
+            list($this->_sCode, $this->_iMix) = $this->_sCode;
+
+        $this->addLocation('studio', $this->_sRootPath, $this->_sRootUrl);
+        $this->addLocationJs('system_admin_js', $this->_sRootPath . 'js/' , $this->_sRootUrl . 'js/');
+    }
+
+    /**
+     * 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 BxDolStudioTemplate();
+            $GLOBALS['bxDolClasses'][__CLASS__]->init();
+        }
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    function init()
+    {
+        parent::init();
+
+        //--- Add default CSS in output
+        $this->addCssSystem(array(
+            'common.less',
+            'default.less',
+            'general.css',
+            'menu.css',
+        ));
+
+        bx_import('BxTemplStudioConfig');
+        $this->_oTemplateConfig = BxTemplStudioConfig::getInstance();
+
+        bx_import('BxTemplStudioFunctions');
+        $this->_oTemplateFunctions = BxTemplStudioFunctions::getInstance($this);
+    }
+
+    function _getAbsoluteLocation($sType, $sFolder, $sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+    	return parent::_getAbsoluteLocation($sType, $sFolder, $sName, BX_DOL_TEMPLATE_CHECK_IN_BASE);
+    }
+
+    function parseSystemKey($sKey, $mixedKeyWrapperHtml = null, $bProcessInjection = true)
+    {
+        $sRet = '';
+        switch( $sKey ) {
+            case 'version':
+                $sRet = bx_get_ver();
+                break;
+            case 'page_breadcrumb':
+                $sRet = $this->getPageBreadcrumb();
+                break;
+			case 'popup_loading':
+                $s = $this->parsePageByName('popup_loading.html', array());
+                $sRet = BxTemplFunctions::getInstance()->transBox('bx-popup-loading', $s, true);
+                break;
+            case 'dol_images':
+                $sRet = $this->_processJsImages();
+                break;
+            case 'dol_lang':
+                $sRet = $this->_processJsTranslations();
+                break;
+            case 'dol_options':
+                $sRet = $this->_processJsOptions();
+                break;
+            case 'menu_top':
+                $sRet = BxTemplStudioMenuTop::getInstance()->getCode();
+                break;
+            case 'copyright':
+                $sRet = _t( '_copyright',   date('Y') ) . getVersionComment();
+                break;
+            case 'class_name':
+                $sRet = 'bx-dir-' . strtolower(bx_lang_direction());
+                break;
+            default:
+                $sRet = parent::parseSystemKey($sKey, $mixedKeyWrapperHtml, false);
+        }
+
+        return $this->processInjection($this->getPageNameIndex(), $sKey, $sRet);
+    }
+
+    function setPageBreadcrumb($aItems)
+    {
+        $this->aPage['breadcrumb'] = $aItems;
+    }
+
+    function getPageBreadcrumb()
+    {
+        if(empty($this->aPage['breadcrumb']) || !is_array($this->aPage['breadcrumb']))
+           return "";
+
+        $aItems = array();
+        foreach($this->aPage['breadcrumb'] as $aItem) {
+            $bLink = isset($aItem['link']) && $aItem['link'] != '';
+
+            $aItems[] = array(
+                'bx_if:show_link' => array(
+                    'condition' => $bLink,
+                    'content' => array(
+                        'link' => $bLink ? $aItem['link'] : '',
+                        'title' => _t($aItem['title'])
+                    )
+                ),
+                'bx_if:show_text' => array(
+                    'condition' => !$bLink,
+                    'content' => array(
+                        'title' => _t($aItem['title'])
+                    )
+                )
+            );
+        }
+
+        return $this->parseHtmlByName('breadcrumb.html', array('bx_repeat:items' => $aItems));
+    }
+
+    function displayMsg ($s, $bTranslate = false, $iPage = BX_PAGE_DEFAULT, $iDesignBox = BX_DB_PADDING_DEF)
+    {
+        $sTitle = $bTranslate ? _t($s) : $s;
+
+        $sContent = MsgBox($sTitle);
+        $sContent = $this->parseHtmlByName('page_not_found.html', array (
+            'content' => $sContent
+        ));
+
+        $this->setPageNameIndex($iPage);
+        $this->setPageHeader($sTitle);
+        $this->setPageContent('page_main_code', $sContent);
+        $this->getPageCode();
+        exit;
+    }
+}
+
+/** @} */

+ 72 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/js/forms.js

@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaStudio UNA Studio
+ * @{
+ */
+
+/*
+ * Functions for "Form View Image Uploader" fields.
+ */
+function fviuDelete(sName, oLink) {
+	var $this = this;
+	var oDate = new Date();
+
+	/*
+	$.post(
+		this.sActionsUrl,
+		{
+			dsg_action: 'delete_logo',
+			_t: oDate.getTime()
+		},
+		function(oData) {
+			if(oData.code != 0 && oData.message.length > 0) {
+				bx_alert(oData.message);
+				return;
+			}
+
+			document.location.href = document.location.href; 
+		},
+		'json'
+	);
+	*/
+}
+
+/*
+ * Functions for "Form View Translatable" fields. 
+ */
+function fvtTogglePopup(sName, oLink) {
+    var oPopup = $(oLink).parents('.bx-form-input-translator:first').find('#bx-form-field-translator-popup-' + sName);
+    if(oPopup.filter(':visible').length > 0) {
+        oPopup.dolPopupHide();
+        return;
+    }
+
+    oPopup.dolPopup({
+    	moveToDocRoot: false,
+        pointer:{
+            el:$(oLink),
+            align: $('body').hasClass('bx-dir-rtl') ? 'left' : 'right'
+        }
+    });
+};
+
+function fvtSelectLanguage(sName, sLangName, oLink) {
+    var oTranslator = $(oLink).parents('.bx-form-input-translator:first');
+
+    //--- Update popup languages
+    var sId = '#bx-form-language-' + sName + '-' + sLangName;
+    $(oLink).parent().siblings('.active:visible').hide().siblings('.not-active:hidden').show().siblings(sId + '-pas:visible').hide().siblings(sId + '-act:hidden').show();
+
+    //--- Update current language
+    oTranslator.find('.bx-form-input-language-current').css('background-image', $(oLink).parent('.bx-form-input-language').css('background-image')).find('a').html($(oLink).html());
+
+    //--- Close selector
+    oTranslator.find('#bx-form-field-translator-popup-' + sName).dolPopupHide();
+
+    //--- Update inputs
+    $(oLink).parents('#bx-form-element-' + sName + ':first').find('.bx-form-input-translation:visible').hide().siblings('.bx-form-input-translation-' + sName + '-' + sLangName + ':hidden').show();
+};
+
+/** @} */

+ 38 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/_header.html

@@ -0,0 +1,38 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
+    <head>
+        <bx_injection:injection_head_begin />
+        
+        <meta http-equiv="Content-Type" content="text/html; charset=__page_charset__" />
+        <title>__page_header__</title>
+
+        __meta_info__
+	    __page_robots__
+
+        <bx_include_css_styles />
+        <bx_include_css_system />
+        <bx_include_css />
+
+        __dol_images__
+        __dol_lang__
+        __dol_options__
+
+        <bx_include_js_system />
+        <bx_include_js />
+
+        <script defer language="javascript">
+            var sUrlStudio = '<bx_url_studio />';
+        </script>
+        
+        <bx_include_auto:_header_js.html />
+        <bx_include_auto:_header_js_spinner.html />
+        <bx_include_auto:_header_js_custom.html />
+
+        __extra_js__
+
+        __system_injection_head__
+        <bx_injection:injection_head />
+    </head>
+    <body class="bx-std bx-std-bg bx-def-font bx-def-color-bg-page __class_name__<bx_injection:injection_body_style />" dir="__lang_direction__" <bx_injection:injection_body /> >
+        __system_injection_header__
+        <bx_injection:injection_header />

+ 247 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/css/builder_page.css

@@ -0,0 +1,247 @@
+/*--- Panel Top style ---*/
+div.bx-std-pt-pages {
+    position: relative;
+    float: left;
+}
+div.bx-std-pt-pages div.bx-form-element-wrapper {
+    width: 165px;
+}
+div.bx-std-pt-buttons {
+    float: right;
+}
+
+/*--- Main section style ---*/
+.bx-std .bx-layout-wrapper {
+    padding-top: 0;
+}
+div.adm-bp-cell {}
+div.adm-bp-cell-cnt {
+    padding: 1rem;
+}
+div.adm-bp-blocks {
+    position: relative;
+}
+div.adm-bp-block {
+    position: relative;
+    overflow: hidden;
+
+    box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.05);
+}
+div.adm-bp-block.adm-bpb-inactive,
+div.bx-lr-dump div.adm-bp-block {
+    opacity: 0.5;
+}
+div.adm-bp-block-empty {
+    height: 4rem;
+    border-style: dashed;
+    box-shadow: none;
+}
+div.adm-bp-block-dragging {}
+div.adm-bpb-content {
+    position: relative;
+    display: -webkit-flex; 
+    display: flex;
+    -webkit-justify-content: flex-start;
+    justify-content: flex-start;
+    -webkit-align-items: center;
+    align-items: center;
+    -webkit-flex-flow: row nowrap;
+    flex-flow: row nowrap;
+
+    padding: 0.625rem;
+}
+
+div.adm-bpb-icon,
+div.adm-bpb-icon-bg,
+div.adm-bpb-icon img,
+div.adm-bpb-icon i.sys-icon {
+    width: 2.5rem;
+    height: 2.5rem;
+    max-width: none;
+    margin-right: 0.75rem;
+}
+.bx-dir-rtl div.adm-bpb-icon,
+.bx-dir-rtl div.adm-bpb-icon-bg,
+.bx-dir-rtl div.adm-bpb-icon img {
+    margin-left: 0.75rem;
+    margin-right: 0;
+}
+div.adm-bpb-icon {
+    position: relative;
+}
+div.adm-bpb-icon-bg {
+    background-color: #323c46;
+    border-radius: 0.5rem;
+}
+div.adm-bpb-icon img {
+    border-radius: 0.5rem;
+}
+div.adm-bpb-icon i.sys-icon {
+    position: absolute;
+    top: 0px;
+    left: 0px;
+
+    text-align: center;
+    text-shadow: none;
+    line-height: 2.5rem;
+    font-size: 20px;
+    color:white;
+}
+div.adm-bpb-info {
+    -webkit-flex-grow: 2;
+    flex-grow: 2;
+
+    min-height: 2rem;
+}
+div.adm-bpb-title {
+    display: -webkit-flex; 
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+
+    padding-right: 1rem;
+
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    line-height: 1.25rem;    
+
+    letter-spacing: -0.4px;
+    font-weight: 600;
+
+    overflow: hidden;
+}
+div.adm-bpb-title .bx-std-api {
+    margin-left: 0.25rem;
+}
+.bx-dir-rtl div.adm-bpb-title .bx-std-api {
+    margin-left: 0;
+    margin-right: 0.25rem;
+}
+div.adm-bpb-description {
+    font-size: 0.75rem;
+}
+div.adm-bpb-drag-handle {
+    text-align: center;
+    line-height: 2rem;
+    cursor: move;
+    color: #666666;
+    font-size: 0.875rem;
+}
+
+/*--- Add Block popup style ---*/
+div#adm-bp-create-block-popup div.bx-popup-content {
+	padding-left: 0px;
+	padding-right: 0px; 
+}
+
+div.adm-bp-create-block-form {}
+
+div.adm-bp-create-block-form-cnt {}
+
+div.adm-bp-create-block-form fieldset.bx-form-section {
+    padding-bottom: 0px;
+}
+div.adm-bp-create-block-form div.adm-bp-block:first-child {
+    margin-top: 0px;
+}
+div.adm-bp-cbf-left {
+    position: absolute;
+    top: 0px;
+    left: 0px;
+    width: 14rem;
+    height: 20rem;
+    overflow-y: auto; 
+}
+div.adm-bp-cbf-left-cnt {}
+div.adm-bp-cbf-right {
+    position: relative;
+    height: 20rem;
+    margin-left: 14rem;
+    overflow-y: auto;
+}
+div.adm-bp-cbf-right-cnt {}
+div.adm-bp-cbf-counter {
+    position: relative;
+    float: right;
+    line-height: 38px;
+    color: #b5b5b5;
+}
+
+/*--- Edit Block popup style ---*/
+div.adm-bp-edit-block-form {
+	margin-top: 0px;
+}
+
+/*--- Uploader Ghost in Popup style ---*/
+div.bx-popup .bx-uploader-ghost .bx-ug-content input.bx-ug-url.bx-form-input-text {
+    width: 18.0rem;
+}
+div.bx-popup .bx-uploader-ghost .bx-ug-content input.bx-ug-dimension.bx-form-input-text {
+	width: 4.0rem;
+}
+
+/*--- Page Settings popups style ---*/
+div#adm-bp-add-popup div.bx-popup-content,
+div#adm-bp-edit-popup div.bx-popup-content {
+    padding-left: 0px;
+    padding-right: 0px; 
+}
+
+div.adm-bp-add-form {}
+div.adm-bp-add-form-cnt {}
+
+div.adm-bp-edit-form {}
+div.adm-bp-edit-form-cnt {}
+
+div.adm-bp-add-form fieldset.bx-form-section,
+div.adm-bp-edit-form fieldset.bx-form-section {
+    padding-bottom: 0px;
+}
+div.adm-bp-add-form div.bx-std-pmen-item-cnt, 
+div.adm-bp-edit-form div.bx-std-pmen-item-cnt {
+	padding-left: 20px;
+}
+div.adm-bp-add-form div.bx-form-input-wrapper-custom, 
+div.adm-bp-edit-form div.bx-form-input-wrapper-custom {
+	position: relative;
+}
+div.adm-bp-epf-left {
+    position: absolute;
+    top: 0px;
+    left: 0px;
+    width: 14rem;
+    height: 20rem;
+    overflow-y: auto; 
+}
+div.adm-bp-epf-left-cnt {}
+div.adm-bp-epf-left .bx-std-pmen-item {
+    height: 2.5rem;
+}
+div.adm-bp-epf-right {
+    position: relative;
+    height: 20rem;
+    margin-left: 14rem;
+    overflow-y: auto;
+}
+div.adm-bp-epf-right-cnt {}
+div.adm-bp-epf-controls .bx-def-margin-sec-left {
+    margin-left: 0.5rem;
+}
+div#adm-bp-settings-group-layout {
+    margin: 0px -10px;
+}
+div.adm-bp-layout {
+	position: relative;
+	float: left;
+	width: 66px;
+	height: 66px;
+	margin-left: 0.5rem;
+	margin-right: 0.5rem;
+	border: 2px solid transparent;
+}
+div.adm-bp-layout-active {
+	border-color: #9999cc;
+}
+div.adm-bp-layout-cnt {
+	padding: 1px;
+}

+ 164 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/css/common.less

@@ -0,0 +1,164 @@
+@import (reference) 'default.less';
+@import '../../../template/css/common.css';
+
+/**
+ * Overwrite default styles
+ */
+
+/* body */
+a {
+    color: rgba(30, 140, 240, 1); 
+}
+a:hover {
+    color: rgba(10, 120, 220, 1); 
+}
+.sys-icon:not(.sys-colored),
+.sys-icon-a {
+	color: rgba(30, 140, 240, 1); 
+}
+a:hover > .sys-icon,
+a:hover > .sys-icon-a {
+	color: rgba(10, 120, 220, 1); 
+}
+.sys-icon-a i {
+	.background-color (rgba(30, 140, 240, 1)) !important;
+	color: rgba(30, 140, 240, 1);
+}
+a:hover > .sys-icon-a i {
+	.background-color (rgba(10, 120, 220, 1)) !important;
+	color: rgba(10, 120, 220, 1);
+}
+
+/* design boxes */
+.bx-db-container {
+    box-shadow: none;
+}
+.bx-db-header {
+    padding: 1.25rem 1.5rem 0 1.5rem;
+}
+.bx-db-header .bx-db-menu-tab-btn .bx-btn {
+    display: inline-flex;
+}
+.bx-db-header .bx-db-title {
+    line-height: 2rem;
+    font-weight: 600;
+    font-size: 1.5rem;
+}
+.bx-db-content.bx-content-padding-block {
+    padding: 1.5rem;
+}
+.bx-db-content.bx-content-padding-block .bx-dbd-block-actions {
+    margin-left: -1.5rem;
+    margin-right: -1.5rem;
+    padding: 0 1.5rem;
+}
+.bx-db-content.bx-def-padding-top {
+    padding: 1rem;
+}
+div.bx-db-note {
+    position: relative;
+    float: left;
+    line-height: 40px;
+    font-size: 11px;
+}
+
+/* popup */
+.bx-popup-applied:not(.bx-popup-chain) .bx-popup,
+.bx-popup-applied.bx-popup-chain .bx-popup-chain-item {
+    margin: 0.5rem;
+
+    .radius(24px);
+
+    -webkit-box-shadow: 0px 3px 5px 1px rgba(0,0,0,0.1);
+    -moz-box-shadow: 0px 3px 5px 1px rgba(0,0,0,0.1);
+    box-shadow: 0px 3px 5px 1px rgba(0,0,0,0.1);
+}
+.dark .bx-popup-applied:not(.bx-popup-chain) .bx-popup,
+.dark .bx-popup-applied.bx-popup-chain .bx-popup-chain-item {
+    -webkit-box-shadow: 0px 3px 5px 1px rgba(255,255,255,0.1);
+    -moz-box-shadow: 0px 3px 5px 1px rgba(255,255,255,0.1);
+    box-shadow: 0px 3px 5px 1px rgba(255,255,255,0.1);
+}
+.bx-popup-applied.bx-popup-menu:not(.bx-popup-chain) .bx-popup {
+    .radius(16px);
+}
+.bx-popup-applied .bx-popup-gap .bx-popup {
+    margin-left: 0;
+    margin-right: 0;
+}
+.bx-popup-width {
+    width: 50rem;
+}
+.bx-popup-border {
+    border: 1px solid rgba(40, 60, 80, 0.1);
+    .radius(3px);
+}
+.bx-popup-color-bg {
+    background-color: rgba(255, 255, 255, 1);
+}
+.bx-popup-content-indent {
+    padding: 1.25rem;
+}
+.bx-popup-header.bx-def-padding {
+    padding: 1.25rem 1.25rem 0 1.25rem;
+}
+.bx-popup-header-color-bg {
+    background-color: transparent;
+}
+.bx-popup-header h2 {
+    line-height: 1.5rem;
+    text-align: left;
+    font-size: 1.5rem;
+    font-weight: 600;
+}
+
+/* button large */
+.bx-btn,
+.bx-btn.bx-btn-primary {
+    .box-sizing(border-box);
+
+    height: 2.5rem;
+    padding: 0 1rem;
+
+    .radius(8px);
+
+    -webkit-box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.08);
+    -moz-box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.08);
+    box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.08);
+
+    text-shadow: none;
+}
+
+.bx-btn.bx-btn-primary:hover,
+.bx-btn.bx-btn-primary:active {
+    .background-color(rgba(68, 134, 244, 0.8));
+    color: rgba(251, 252, 253, 1.0);
+}
+.bx-btn.bx-btn-primary .sys-icon:not(.sys-colored) {
+    color: rgba(255, 255, 255, 0.9);
+}
+.bx-btn.bx-btn-primary:hover .sys-icon:not(.sys-colored),
+.bx-btn.bx-btn-primary:active .sys-icon:not(.sys-colored) {
+    color: rgba(255, 255, 255, 1);
+}
+
+/* button small */
+.bx-btn.bx-btn-small {
+    .box-sizing(border-box);
+
+    height: 2.0rem;
+    line-height: 2.0rem;
+    padding: 0 1rem;
+
+    .radius(6px);
+
+    text-shadow: none;
+
+    font-family: sans-serif;
+    font-size: 0.875rem;
+    font-weight: 500;
+}
+
+/**
+ * General
+ */

+ 56 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/css/forms.css

@@ -0,0 +1,56 @@
+@import url(../../../template/css/forms.css);
+
+
+/**
+ * General
+ */
+select.bx-form-field-type option {
+    padding-left: 25px;
+    background-repeat: no-repeat;
+    background-position: 5px 0px;
+}
+
+
+/**
+ * Image uploader with with manager
+ */
+div.bx-form-input-preview {}
+div.bx-form-input-preview img {
+    max-width: 768px; 
+    max-height: 256px;
+}
+div.bx-form-input-preview-empty {}
+
+
+/**
+ * Translatable fields.
+ */
+div.bx-form-input-wrapper-textarea {
+    position: relative;
+}
+
+div.bx-form-input-translator {
+    position: absolute;
+    top: -22px;
+}
+.bx-dir-ltr div.bx-form-input-translator {
+    right: 0px;
+}
+.bx-dir-rtl div.bx-form-input-translator {
+    left: 0px;
+}
+
+div.bx-form-input-language,
+div.bx-form-input-language-current{
+    position: relative;
+    height: 17px;
+    line-height: 17px;
+}
+
+div.bx-form-input-language {
+    white-space: nowrap
+}
+
+div.bx-form-input-language-current {
+    font-weight: bold;
+}

+ 235 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/css/general.css

@@ -0,0 +1,235 @@
+html,
+body {
+    min-height: 100%;
+    min-height: 100vh;
+}
+iframe.bx-std-hidden {
+    display: none;
+}
+div.bx-std-cb {
+    clear: both;
+    font-size: 1px;
+}
+
+/**
+ * Page Content
+ */
+div.bx-std-page-bg {
+    position:fixed; 
+    width:100%; 
+    height:100%; 
+
+    background:url(../images/bx-std-spacer.gif) center center no-repeat;
+    /* background:url(../images/bx-std-bg-main-image.jpg) center center no-repeat; */
+}
+div.bx-std-page {
+    position: relative;
+    position: relative;
+    display: -webkit-flex;
+    display: flex;
+    -webkit-flex-direction: column;
+    flex-direction: column;
+    width: 100%;
+    min-height: 100vh;
+}
+div.bx-std-menu-top-wrp {
+    -webkit-flex-grow: 0;
+    flex-grow: 0;
+    -webkit-flex-shrink: 0;
+    flex-shrink: 0;
+}
+div.bx-std-content {
+    position: relative;
+    -webkit-flex-grow: 2;
+    flex-grow: 2;
+
+    width: 100%;
+}
+div.bx-std-content-wrp {
+    position: relative;
+    width: 100%;
+    height: 100vh;
+}
+div.bx-std-content-wrp.bx-std-one-col {
+    padding-top: 5rem;
+
+    overflow-y: auto;
+}
+div.bx-std-page-columns,
+div.bx-std-page-columns-cnt {
+    position: relative;
+    width: 100%;
+    height: 100%;
+}
+
+/**
+ * Dual Column page
+ */
+div.bx-std-page-caption {
+    position: relative;
+    height: 70px;
+
+    background-color: #334455;
+}
+div.bx-std-pcap-icon {}
+div.bx-std-pcap-text {
+	position: relative;
+	line-height: 70px;
+	text-align: center;
+	font-size: 2rem;
+}
+div.bx-std-pcap-menu-popup {
+	position: absolute;
+	z-index: 1;
+}
+div.bx-std-pcap-mp-right {
+	right: 10px;
+}
+
+div.bx-std-pcap-help .sys-rss-feed-wrapper {
+	padding-top: 0px;
+}
+div.bx-std-pcap-help .bx-loading-ajax {
+	margin-top: 0px;
+	margin-bottom: 0px;
+}
+
+div.bx-std-pcap-action {
+	position: relative;
+	float: left;
+	width: 36px;
+	height: 36px;
+}
+div.bx-std-pcap-action:first-child {
+	margin-left: 0px;
+}
+div#bx-std-pca-activate {
+	width: 82px;
+}
+div.bx-std-pcap-mp-right div.bx-std-pcap-action {
+    float: right;
+}
+
+/**
+ * Page Menu (Left Side)
+ */
+div.bx-std-pmen-item {
+    position: relative;
+    height: 3.5rem;
+    margin-top: 0.125rem;
+
+    border-radius: 0.75rem;
+
+    font-size: 1.0rem;
+    font-weight: 500;
+
+    overflow: hidden;
+}
+div.bx-std-pmen-item:first-child {
+    margin-top: 0;
+}
+
+div.bx-std-pmen-item-cnt {
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+    -webkit-flex-wrap: nowrap;
+    flex-wrap: nowrap;
+
+    padding: 0.5rem;
+
+    text-overflow: ellipsis;
+    overflow: hidden;
+}
+
+span.bx-std-pmen-item-counter {}
+span.bx-std-pmen-ic-num {}
+
+div.bx-std-pmen-item .bx-std-pmen-ic-icon {
+    display: none;
+    flex: 0 0 auto;
+
+    width: 2.5rem;
+    height: 2.5rem;
+    box-sizing: border-box;
+
+    margin-right: 1rem;
+    
+    border-radius: 0.5rem;
+    background-repeat: no-repeat;
+    background-position: center center;
+
+    text-align: center;
+}
+.bx-dir-rtl div.bx-std-pmen-item .bx-std-pmen-ic-icon {
+    margin-left: 1rem;
+    margin-right: 0;
+}
+
+div.bx-std-pmen-item .bx-std-pmen-ic-icon.block {
+    display: block;
+}
+
+div.bx-std-pmen-item img,
+div.bx-std-pmen-item svg,
+div.bx-std-pmen-item .sys-icon {
+    width: 100%;
+    height: 100%;
+
+    border-radius: 0.5rem;
+}
+
+div.bx-std-pmen-item a, 
+div.bx-std-pmen-item .sys-icon {
+    color: #595959;
+}
+div.bx-std-pmen-item a {
+    position: relative;
+    display: block;
+    width: 100%;
+    height: 100%;
+
+    text-decoration: none;
+    color: rgba(55, 65, 81, 1);
+}
+div.bx-std-pmen-item .sys-icon {
+    font-size: 1.0rem;
+    color: #ffffff;
+}
+
+/*--- Icons ---*/
+img.bx-item-icon,
+svg.bx-item-icon{
+    width: 32px;
+    height: 32px;
+}
+div.bx-item-icon-preview {}
+div.bx-item-icon-preview img,
+div.bx-item-icon-preview svg{
+	max-width:128px; 
+    max-height:128px;
+}
+div.adm-item-icon-preview-empty {}
+
+/**
+ * Footer
+ */
+div.bx-std-bx-footer {
+	margin-top: 60px;
+	border: 0px;
+	background-color: transparent;
+    -moz-shadow: none;
+    -webkit-shadow: none;
+	box-shadow: none;
+}
+
+
+/**
+ * API
+ */
+img.bx-std-api {
+    display: inline-block;
+    width: 1rem;
+    height: 1rem;
+}

+ 16 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/css/grid.css

@@ -0,0 +1,16 @@
+@import url(../../../template/css/grid.css);
+
+/**
+ * Overwrite default styles
+ */
+.bx-grid-table td,
+.bx-grid-table th {
+    padding-top: 0.5rem;
+    padding-bottom: 0.5rem;
+    font-size: 14px;
+}
+.bx-grid-table tr:not(.bx-grid-table-row-hr) td {
+    height: 55px;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+}

+ 446 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/css/menu_top.css

@@ -0,0 +1,446 @@
+div.bx-std-menu-top-wrp {
+    position: relative;
+    display: -webkit-flex;
+    display: flex;
+    -webkit-justify-content: space-between;
+    justify-content: space-between;
+    -webkit-align-items: center;
+    align-items: center;
+    -webkit-flex-wrap: nowrap;
+    flex-wrap: nowrap;
+
+    width: 100%;
+    height: 4rem;
+
+    box-shadow: 0px 1px 0px 0px rgba(0, 0, 0, 0.05);
+    backdrop-filter: blur(16px);
+}
+div.bx-std-menu-top-wrp .bx-std-menu-top.bx-std-menu-top-center:first-child {
+    padding-left: 0.75rem;
+}
+
+div.bx-std-menu-top {}
+div.bx-std-menu-top.bx-std-menu-top-left,
+div.bx-std-menu-top.bx-std-menu-top-center,
+div.bx-std-menu-top.bx-std-menu-top-right {
+    -webkit-flex: 0 1 auto;
+    flex: 0 1 auto;
+}
+div.bx-std-menu-top.bx-std-menu-top-left,
+div.bx-std-menu-top.bx-std-menu-top-right {
+    position: relative;
+    z-index: 2;
+    min-width: 10%;
+}
+div.bx-std-menu-top.bx-std-menu-top-center {    
+    z-index: 1;
+    -webkit-flex-grow: 1;
+    flex-grow: 1;
+    height: 3.5rem;
+}
+div.bx-std-menu-top:nth-child(1).bx-std-menu-top-center {
+    position: relative;
+    display: -webkit-flex;
+    display: flex;
+}
+div.bx-std-menu-top:nth-child(2).bx-std-menu-top-center .bx-std-menu-top-center-cnt {
+    position: absolute;
+    left: 0;
+    width: 100vw;
+}
+div.bx-std-menu-top-center ul {
+    position: relative;
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: stretch;
+    align-items: stretch;
+    -webkit-flex-wrap: nowrap;
+    flex-wrap: nowrap;
+    
+}
+div.bx-std-menu-top-left {
+    padding-left: 1rem;
+    line-height: 4rem;
+    text-align: left;
+    font-size: 1.5rem;
+}
+div.bx-std-menu-top-right {
+    padding-right: 1rem;
+    text-align: right;
+}
+
+/*--- Breadcrumb (Left) ---*/
+div.bx-std-menu-top .bx-menu-breadcrumb {
+    position: relative;
+    display: -webkit-flex;
+    display: flex;
+    -webkit-flex-wrap: nowrap;
+    flex-wrap: nowrap;
+    -webkit-align-items: center;
+    align-items: center;
+}
+
+/*--- Widgets (Center) ---*/
+div.bx-std-menu-top .bx-menu-top-center {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    gap: 0.25rem;
+
+    padding: 0;
+    margin: 0;
+
+    list-style: none;
+}
+
+div.bx-std-menu-top .bx-menu-breadcrumb li.bx-menu-bc-item,
+div.bx-std-menu-top .bx-menu-top-center li {
+    position: relative;
+    display: -webkit-flex;
+    display: flex;
+    -webkit-justify-content: center;
+    justify-content: center;
+    -webkit-align-items: center;
+    align-items: center;
+
+    width: 3.5rem;
+    height: 3.5rem;
+}
+
+div.bx-std-menu-top .bx-menu-breadcrumb li.bx-menu-bc-div img {
+    height: 2.0rem;
+}
+
+div.bx-std-menu-top .bx-menu-breadcrumb li.bx-menu-bc-item.bx-menu-bc-page {
+    width: auto;
+    padding-left: 0.5rem;
+    padding-right: 0.5rem;
+}
+
+/*
+ * Vertical divider between static and dynamic menu items.
+ *
+div.bx-std-menu-top .bx-menu-top-center li.bx-menu-item-static + li.bx-menu-item-dynamic {
+    border-left: 1px solid rgba(240, 240, 240, 0.2);
+}
+*/
+
+.bx-menu-breadcrumb li.bx-menu-bc-item > a,
+div.bx-std-menu-top .bx-menu-top-center li a {
+    display: block;
+    z-index: 2;
+    box-sizing: content-box;
+
+    width: 2.5rem;
+    height: 2.5rem;
+}
+.bx-menu-breadcrumb li.bx-menu-bc-item > a {
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+
+    width: auto;
+}
+div.bx-std-menu-top .bx-menu-top-center li.bx-menu-tab-active a {
+    border-color: rgba(59, 130, 246, 0.5);
+}
+div.bx-std-menu-top .bx-menu-top-center li.bx-menu-tab-active .bx-mi-bg {
+    display: block;
+}
+div.bx-std-menu-top .bx-menu-breadcrumb li.bx-menu-bc-item .bx-mi-icon,
+div.bx-std-menu-top .bx-menu-top-center li .bx-mi-icon {
+    overflow: hidden;
+}
+div.bx-std-menu-top .bx-menu-breadcrumb li.bx-menu-bc-item .bx-mi-icon {
+    border-radius: 1.5rem;
+}
+div.bx-std-menu-top .bx-menu-breadcrumb li.bx-menu-bc-item .bx-mi-title {    
+    text-align: center;
+    letter-spacing: -0.02em;
+
+    font-size: 1.5rem;
+    font-weight: 500;   
+
+    color: rgb(243, 244, 246);
+}
+div.bx-std-menu-top .bx-menu-breadcrumb li.bx-menu-bc-item img,
+div.bx-std-menu-top .bx-menu-breadcrumb li.bx-menu-bc-item:not(.bx-menu-tab-search) svg,
+div.bx-std-menu-top .bx-menu-breadcrumb li.bx-menu-bc-item .sys-icon,
+div.bx-std-menu-top .bx-menu-top-center li img,
+div.bx-std-menu-top .bx-menu-top-center li .sys-icon {
+    display: block;
+    box-sizing: content-box;
+
+    width: 2.5rem;
+    height: 2.5rem;
+
+    margin: 0;
+}
+
+div.bx-std-menu-top .bx-menu-top-center li img {
+    position: relative;
+    z-index: -1;
+}
+
+div.bx-std-menu-top .bx-menu-top-center li .sys-icon {
+    font-size: 1.5rem;
+    color: rgba(255, 255, 255, 0.7);
+}
+
+div.bx-std-menu-top .bx-menu-top-center li .bx-mi-title {
+    margin-top: 0.25rem;
+
+    font-size: 0.75rem;
+    font-weight: normal;
+    color: rgba(255, 255, 255, 0.7);
+    
+    text-overflow: ellipsis;
+    overflow: hidden;
+}
+div.bx-std-menu-top .bx-menu-top-center li.bx-menu-tab-divider {
+    width: 0.25rem;
+    height: 1.75rem;
+}
+
+/*--- Search box (Top Left\Center\Right) ---*/
+div.bx-std-menu-top ul li.bx-menu-tab-search {
+    overflow: visible;
+}
+div.bx-std-menu-top ul li.bx-menu-tab-search > div {
+    position: absolute;
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+    -webkit-flex-wrap: nowrap;
+    flex-wrap: nowrap;
+
+    width: 2.5rem;
+    height: 2.5rem;
+    right: 0;
+
+    border-radius: 0.5rem;
+}
+.bx-dir-rtl div.bx-std-menu-top ul li.bx-menu-tab-search > div {
+    left: 0;
+    right: auto;
+}
+div.bx-std-menu-top ul.bx-menu-breadcrumb li.bx-menu-tab-search > div {
+    left: 0.5rem;
+    right: auto;
+}
+div.bx-std-menu-top ul li.bx-menu-tab-search.bx-mt-active > div {
+    width: 13.75rem;
+}
+div.bx-std-menu-top ul li.bx-menu-tab-search .bx-form-input-text {
+    height: 2.5rem;
+    padding-left: 0.5rem;
+    padding-right: 0.5rem;
+
+    border-width: 0;
+    background-color: transparent;
+
+    font-style: normal;
+    font-weight: 500;
+}
+
+div.bx-std-menu-top ul li.bx-menu-tab-search .bx-form-input-text:focus {
+    border-color: transparent;
+    background-color: transparent;
+    box-shadow: none;
+    filter: none;
+    transition: all 0s ease 0s;
+}
+
+
+/*--- Toolbar (Right) ---*/
+div.bx-std-menu-top .bx-menu-toolbar {
+    position: relative;
+    display: -webkit-flex;
+    display: flex;
+    -webkit-flex-wrap: nowrap;
+    flex-wrap: nowrap;
+    -webkit-justify-content: flex-end;
+    justify-content: flex-end;
+}
+
+div.bx-std-menu-top .bx-menu-toolbar li {
+    position: relative;
+    float: none;
+    display: block;
+
+    height: 2.5rem;
+    min-width: 2.5rem;
+    
+    border-radius: 0.5rem;
+
+    line-height: 2.5rem;
+    overflow: hidden;
+}
+div.bx-std-menu-top .bx-menu-toolbar li:not(.bx-menu-tab-search) a {
+    position: relative;
+    display: -webkit-flex;
+    display: flex;
+    -webkit-justify-content: center;
+    justify-content: center;
+    -webkit-align-items: center;
+    align-items: center;
+
+    width: 100%;
+    height: 100%;
+
+    border-radius: 0.5rem;
+}
+
+div.bx-std-menu-top .bx-menu-toolbar li a:hover {
+    color: rgb(55, 65, 81);
+}
+
+div.bx-std-menu-top i.sys-icon {
+    -webkit-transition: all 0.2s ease-in-out;
+    transition: all 0.2s ease-in-out;
+}
+
+div.bx-std-menu-top-right li.bx-menu-tab-active i.sys-icon {
+    color: #ffe900;
+    text-shadow: 0 0 4px #ffe900;
+}
+
+/*--- Account, Actions, Help and Settings popups ---*/
+body > .bx-popup-wrapper.bx-std-mod-popup-settings-wrapper,
+#bx-std-pcap-menu-popup-help,
+#bx-std-pcap-menu-popup-assistant,
+#bx-std-pcap-menu-popup-actions,
+#bx-std-pcap-menu-popup-account,
+#bx-std-pmenu-popup-actions {
+    overflow: visible;
+}
+.bx-std-mod-popup-settings-wrapper .bx-popup-gap,
+#bx-std-pcap-menu-popup-help .bx-popup-gap,
+#bx-std-pcap-menu-popup-assistant .bx-popup-gap,
+#bx-std-pcap-menu-popup-actions .bx-popup-gap,
+#bx-std-pcap-menu-popup-account .bx-popup-gap,
+#bx-std-pcap-menu-popup-scheme .bx-popup-gap,
+#bx-std-pmenu-popup-actions .bx-popup-gap {
+    margin-left: 0;
+    margin-right: 0;
+}
+#bx-std-pcap-menu-popup-help .bx-popup,
+#bx-std-pcap-menu-popup-actions .bx-popup,
+.bx-std-mod-popup-settings-wrapper.bx-popup-applied:not(.bx-popup-chain) .bx-popup {
+    border-width: 0;
+}
+#bx-std-pcap-menu-popup-actions .bx-popup-content,
+#bx-std-pmenu-popup-actions .bx-popup-content {
+    padding: 1.0rem;
+}
+#bx-std-pcap-menu-popup-account .bx-popup-content,
+#bx-std-pcap-menu-popup-scheme .bx-popup-content {
+    padding: 1.0rem 0;
+}
+#bx-std-pcap-menu-popup-account .bx-popup li,
+#bx-std-pcap-menu-popup-scheme .bx-popup li {
+    position: relative;
+    height: 2.5rem;
+    margin: 0;
+    padding: 0 1rem;
+
+    white-space: nowrap;
+    line-height: 2.5rem;
+    font-size: 0.875rem;
+    font-weight: 400;
+    overflow: hidden;
+}
+#bx-std-pcap-menu-popup-account .bx-popup li a,
+#bx-std-pcap-menu-popup-scheme .bx-popup li a {
+    position: relative;
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+}
+#bx-std-pcap-menu-popup-account .bx-popup li svg,
+#bx-std-pcap-menu-popup-scheme .bx-popup li svg {
+    margin-right: 0.75rem;
+}
+.bx-dir-rtl #bx-std-pcap-menu-popup-account .bx-popup li svg,
+.bx-dir-rtl #bx-std-pcap-menu-popup-scheme .bx-popup li svg {
+    margin-left: 0.75rem;
+    margin-right: 0;
+}
+
+.bx-std-mod-popup-settings-wrapper,
+#bx-std-pcap-menu-popup-help .bx-popup,
+#bx-std-pcap-menu-popup-help .bx-popup a,
+#bx-std-pcap-menu-popup-help .bx-popup .sys-icon,
+#bx-std-pcap-menu-popup-actions .bx-popup {
+    color: rgba(0, 0, 0, 0.7);
+}
+#bx-std-pcap-menu-popup-account .bx-popup li svg g {
+    opacity: 0.7;
+}
+#bx-std-pcap-menu-popup-help .bx-popup a:hover {
+    color: rgba(0, 0, 0, 0.85);
+}
+#bx-std-pcap-menu-popup-account .bx-popup li.bx-menu-tab-active,
+#bx-std-pcap-menu-popup-scheme .bx-popup li.bx-menu-tab-active {
+    background-color: rgba(24, 144, 255, 0.2);
+    box-shadow: inset 3px 0px 0px rgba(24, 144, 255, 1.0);
+}
+#bx-std-pcap-menu-popup-account .bx-popup li:hover svg g,
+#bx-std-pcap-menu-popup-account .bx-popup li.bx-menu-tab-active svg g {
+    opacity: 0.85;
+}
+
+.bx-std-mod-popup-settings-wrapper .bx-form-input-select {
+    border-color: rgb(55, 65, 81, 1.0);
+    background-color: rgb(17, 24, 39, 1.0);
+}
+
+/*--- Assistant ---*/
+#bx-std-pcap-menu-popup-assistant .bx-popup .bx-popup-content {
+    padding-top: 0;
+}
+.bx-std-pcap-assistant div.cmts {
+    max-height: calc(0.5 * 100vh);
+    overflow-y: auto;
+}
+.bx-std-pcap-assistant div.cmts .cmt .bx-agents-loading {
+    padding: 0;
+    background-color: transparent;
+    border-color: transparent;
+    box-shadow: none;
+}
+.bx-std-pcap-assistant div.cmts > ul.cmts {
+    margin-top: 1rem;
+}
+.bx-std-pcap-assistant .cmt > .cmts {
+    display: none;
+}
+.bx-std-pcap-assistant .cmt .cmt-head .cmt-meta {
+    margin-right: 0;
+    color: rgb(156 163 175);
+}
+.bx-std-pcap-assistant .cmt .cmt-head ul li:first-child .bx-base-general-unit-meta-div {
+  display: none;
+}
+.bx-std-pcap-assistant .cmt-reply.cmt-reply-bottom {
+    margin-bottom: 0;
+}
+.bx-std-pcap-assistant .bx-form-section-content {
+    display: flex;
+}
+.bx-std-pcap-assistant .bx-form-section-content > .bx-form-element-wrapper:first-child {
+    flex: 1 1 auto;
+}
+.bx-std-pcap-assistant .bx-form-section-content > .bx-form-element-wrapper:last-child {
+    flex: 0 0 auto;
+    margin: 0 0 0 1rem;
+}
+.bx-dir-rtl .bx-std-pcap-assistant .bx-form-section-content > .bx-form-element-wrapper:last-child {
+    margin: 0 1rem 0 0;
+}
+.bx-std-pcap-assistant .bx-form-value .bx-form-input-textarea {
+    height: 4.0rem;
+}

+ 22 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/dbd_htools.html

@@ -0,0 +1,22 @@
+<div id="bx-dbd-htools">
+    <div class="bx-dbd-block-content bx-def-margin-bottom">
+        <bx_if:show_content>
+            <div class="bx-dbd-htools bx-clearfix">
+                <bx_repeat:items>
+                    <div class="bx-dbd-htools-item bx-def-margin-sec-top-auto"><img class="bx-stg-icon-gray-bg ltr:mr-4 rtl:ml-4" src="<bx_image_url:bx-std-spacer.gif />" style="background-image: url('__icon__')" />__title__:<b class="pl-2">__value__</b></div>
+                </bx_repeat:items>
+            </div>
+        </bx_if:show_content>
+    </div>
+    <div class="bx-dbd-block-actions space-x-4" style="display:none">
+        <a class="bx-btn" href="javascript:void(0)" onclick="javascript:__js_object__.serverAudit()"><bx_text:_sys_inst_server_audit /></a>
+        <a class="bx-btn" href="javascript:void(0)" onclick="javascript:__js_object__.permissions()"><bx_text:_sys_audit_permissions /></a>
+    </div>
+    <bx_if:show_loader>
+        <script language="javascript">
+            $(document).ready(function() {
+                __js_object__.checkHostParams();
+            });
+        </script>
+    </bx_if:show_loader>
+</div>

+ 27 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/dbd_versions.html

@@ -0,0 +1,27 @@
+<div id="bx-dbd-version">
+    <div class="bx-dbd-block-content bx-def-margin-bottom">
+        <div class="bx-dbd-version-image">
+            <img src="<bx_image_url:dbd_version.svg />"/>
+        </div>
+        <div class="bx-dbd-version-info">
+            <div class="bx-dbd-vi-title bx-def-margin-sec-topbottom text-2xl font-semibold">__domain__ __version__</div>
+            <div class="bx-dbd-vi-info space-y-4">
+                <div class="bx-dbd-vii-row">
+                    <img class="bx-stg-icon-gray-bg ltr:mr-4 rtl:ml-4" src="<bx_image_url:bx-std-spacer.gif />" style="background-image: url('<bx_icon_url:db-vi-installed.svg />')" /><bx_text:_adm_dbd_txt_installed /><b class="ltr:pl-2 rtl:pr-2">__installed__</b>
+                </div>
+                <bx_if:show_update_info>
+                    <div class="bx-dbd-vii-row"><img class="bx-stg-icon-gray-bg ltr:mr-4 rtl:ml-4" src="<bx_image_url:bx-std-spacer.gif />" style="background-image: url('<bx_icon_url:db-vi-updated.svg />')" /><bx_text:_adm_dbd_txt_updated /><b class="ltr:pl-2 rtl:pr-2">__updated__</b></div>
+                </bx_if:show_update_info>
+                <div class="bx-dbd-vii-row bx-dbd-version-available" style="display:none"><img class="bx-stg-icon-gray-bg ltr:mr-4 rtl:ml-4" src="<bx_image_url:bx-std-spacer.gif />" style="background-image: url('<bx_icon_url:db-vi-available.svg />')" /><bx_text:_adm_dbd_txt_available /><b class="ltr:pl-2 rtl:pr-2"></b></div>
+            </div>
+        </div>
+    </div>
+    <div class="bx-dbd-block-actions" style="display:none">
+        <a class="bx-btn bx-btn-primary" href="javascript:void(0)" onclick="javascript:__js_object__.performUpgrade()"><bx_text:_adm_dbd_btn_update /></a>
+    </div>
+    <script language="javascript">
+        $(document).ready(function() {
+            __js_object__.checkForUpgrade();
+        });
+    </script>
+</div>

+ 22 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/menu_top_toolbar.html

@@ -0,0 +1,22 @@
+<ul class="bx-menu-toolbar gap-x-4">
+    <bx_if:show_search>
+        <li class="bx-menu-tab-search<bx_if:show_active> bx-mt-active</bx_if:show_active> flex-auto">
+            <div class="bx-menu-tab-item">
+                <a class="text-gray-600 hover:text-gray-700" href="javascript:void(0)" onclick="javascript:$(this).parents('li:first').toggleClass('bx-mt-active')">
+                    <svg class="flex-none ltr:ml-1.5 rtl:mr-1.5" width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
+                        <path d="M5 12C5 12.9193 5.18106 13.8295 5.53284 14.6788C5.88463 15.5281 6.40024 16.2997 7.05025 16.9497C7.70026 17.5998 8.47194 18.1154 9.32122 18.4672C10.1705 18.8189 11.0807 19 12 19C12.9193 19 13.8295 18.8189 14.6788 18.4672C15.5281 18.1154 16.2997 17.5998 16.9497 16.9497C17.5998 16.2997 18.1154 15.5281 18.4672 14.6788C18.8189 13.8295 19 12.9193 19 12C19 11.0807 18.8189 10.1705 18.4672 9.32122C18.1154 8.47194 17.5998 7.70026 16.9497 7.05025C16.2997 6.40024 15.5281 5.88463 14.6788 5.53284C13.8295 5.18106 12.9193 5 12 5C11.0807 5 10.1705 5.18106 9.32122 5.53284C8.47194 5.88463 7.70026 6.40024 7.05025 7.05025C6.40024 7.70026 5.88463 8.47194 5.53284 9.32122C5.18106 10.1705 5 11.0807 5 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                        <path d="M23 23L17 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                    </svg>
+                </a>
+                <div class="bx-form-input-wrapper bx-form-input-wrapper-text">
+                    <input type="text" name="search" value="" placeholder="<bx_text_attribute:_adm_tmi_cpt_search />" class="bx-def-font-inputs bx-form-input-text" onkeydown="return oBxDolStudioMenuTop.searchWidget(event)" />
+                </div>
+            </div>
+        </li>
+    </bx_if:show_search>
+    <bx_repeat:menu_items>
+        <li class="bx-menu-tab-__name__  __class_add__ bx-menu-tab-item">
+            <a href="__link__" onclick="__onclick__" title="__title__"__attrs__><bx_if:icon><i class="sys-icon __icon__ bx-def-round-corners bx-def-font-contrasted"></i></bx_if:icon><bx_if:image><img src="__icon_url__" /></bx_if:image><bx_if:image_inline>__image__</bx_if:image_inline></a>
+        </li>
+    </bx_repeat:menu_items>
+</ul>

+ 37 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/page_breadcrumb.html

@@ -0,0 +1,37 @@
+<div class="bx-std-pcap-bx-std-pcap">
+    <ul class="bx-menu-hor bx-menu-breadcrumb">
+        <bx_repeat:menu_items>
+            <li class="bx-menu-bc-div px-2">
+                <img src="<bx_icon_url:bc-arrow.svg />" />
+            </li>
+            <li class="bx-menu-bc-item bx-menu-bc-__name__  __class_add__">
+                <a href="__link__" onclick="__onclick__" title="__title_attr__"__attrs__>
+                    <div class="bx-mi-icon flex-0 hover:ring-4 hover:ring-gray-800 hover:ring-offset-4 hover:ring-offset-black active:opacity-80 duration-300">
+                        <bx_if:icon><i class="sys-icon __icon__ bx-def-round-corners bx-def-font-contrasted"></i></bx_if:icon>
+                        <bx_if:image><img src="__icon_url__" /></bx_if:image>
+                        <bx_if:image_inline>__image__</bx_if:image_inline>
+                    </div>
+                    <bx_if:title><div class="bx-mi-title flex-1">__title__</div></bx_if:title>
+                </a>
+            </li>
+        </bx_repeat:menu_items>
+        <bx_if:show_search>
+            <li class="bx-menu-bc-div px-2">
+                <img src="<bx_icon_url:bc-arrow.svg />" />
+            </li>
+            <li class="bx-menu-bc-item bx-menu-tab-search bx-mt-active flex-auto">
+                <div class="bg-gray-200 hover:bg-gray-100 text-gray-600 border border-black/10 hover:text-gray-700 hover:ring-4 hover:ring-gray-200 hover:ring-offset-2 hover:ring-offset-white active:opacity-80 duration-300">
+                    <a class="text-gray-600 hover:text-gray-700" href="javascript:void(0)" onclick="javascript:$(this).parents('li:first').toggleClass('bx-mt-active')">
+                        <svg class="flex-none ltr:ml-1.5 rtl:mr-1.5" width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
+                            <path d="M5 12C5 12.9193 5.18106 13.8295 5.53284 14.6788C5.88463 15.5281 6.40024 16.2997 7.05025 16.9497C7.70026 17.5998 8.47194 18.1154 9.32122 18.4672C10.1705 18.8189 11.0807 19 12 19C12.9193 19 13.8295 18.8189 14.6788 18.4672C15.5281 18.1154 16.2997 17.5998 16.9497 16.9497C17.5998 16.2997 18.1154 15.5281 18.4672 14.6788C18.8189 13.8295 19 12.9193 19 12C19 11.0807 18.8189 10.1705 18.4672 9.32122C18.1154 8.47194 17.5998 7.70026 16.9497 7.05025C16.2997 6.40024 15.5281 5.88463 14.6788 5.53284C13.8295 5.18106 12.9193 5 12 5C11.0807 5 10.1705 5.18106 9.32122 5.53284C8.47194 5.88463 7.70026 6.40024 7.05025 7.05025C6.40024 7.70026 5.88463 8.47194 5.53284 9.32122C5.18106 10.1705 5 11.0807 5 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                            <path d="M23 23L17 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+                        </svg>
+                    </a>
+                    <div class="bx-form-input-wrapper bx-form-input-wrapper-text">
+                        <input type="text" name="search" value="" placeholder="<bx_text_attribute:_adm_tmi_cpt_search />" class="bx-def-font-inputs bx-form-input-text" onkeydown="return oBxDolStudioMenuTop.searchWidget(event)" />
+                    </div>
+                </div>
+            </li>
+        </bx_if:show_search>
+    </ul>
+</div>

+ 2064 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/scripts/BxBaseStudioBuilderPage.php

@@ -0,0 +1,2064 @@
+<?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 BxBaseStudioBuilderPage extends BxDolStudioBuilderPage
+{
+    protected $sParamsDivider = '#';
+    protected $sSelectKeyPrefix = 'id-';
+
+    protected $sActionPageCreate = 'page_create';
+    protected $sActionPageDelete = 'page_delete';
+    protected $sActionPageEdit = 'page_edit';
+    protected $sActionBlockCreate = 'block_create';
+    protected $sActionBlockEdit = 'block_edit';
+
+    protected $sStorage;
+    protected $sTranscoder;
+    protected $aUploaders; 
+    
+    protected $sTranscoderCover;
+    protected $aUploadersCover;
+
+    protected $sBaseUrl;
+    protected $sTypeUrl;
+    protected $sPageUrl;
+
+    protected $aHtmlIds = array(
+        'add_popup_id' => 'adm-bp-add-popup',
+        'edit_popup_id' => 'adm-bp-edit-popup',
+        'uri_field_id' => 'adm-bp-field-uri',
+    	'url_field_id' => 'adm-bp-field-url',
+        'settings_group_id' => 'adm-bp-settings-group-',
+        'settings_groups_id' => 'adm-bp-settings-groups',
+        'create_block_popup_id' => 'adm-bp-create-block-popup',
+        'edit_block_popup_id' => 'adm-bp-edit-block-popup',
+    	'edit_block_editor_id' => 'adm-bp-edit-block-editor',
+        'edit_block_builder_id' => 'adm-bp-edit-block-builder',
+        'block_id' => 'adm-bpb-',
+        'block_list_id' => 'adm-bpl-',
+        'block_lists_id' => 'adm-bp-block-lists',
+        'layout_id' => 'adm-bpl-',
+    );
+
+    protected $aPageSettings = array(
+        array('name' => 'options', 'title' => '_adm_bp_mi_page_options', 'active' => 1),
+        array('name' => 'cover', 'title' => '_adm_bp_mi_page_cover', 'active' => 0),
+        array('name' => 'layout', 'title' => '_adm_bp_mi_page_layout', 'active' => 0),
+        array('name' => 'visibility', 'title' => '_adm_bp_mi_page_visibility', 'active' => 0),
+        array('name' => 'cache', 'title' => '_adm_bp_mi_page_cache', 'active' => 0),
+        array('name' => 'seo', 'title' => '_adm_bp_mi_page_seo', 'active' => 0),
+        array('name' => 'injections', 'title' => '_adm_bp_mi_page_injections', 'active' => 0)
+    );
+
+    function __construct($sType = '', $sPage = '')
+    {
+        parent::__construct($sType, $sPage);
+
+        $this->sStorage = BX_DOL_STORAGE_OBJ_IMAGES;
+        $this->sTranscoder = 'sys_builder_page_preview';
+        $this->aUploaders = array('sys_builder_page_html5');
+
+        $this->sTranscoderCover = 'sys_cover_preview';
+        $this->aUploadersCover = array('sys_std_crop_cover');
+
+        $this->sBaseUrl = BX_DOL_URL_STUDIO . 'builder_page.php';
+        $this->sTypeUrl = $this->sBaseUrl . '?type=%s';
+        $this->sPageUrl = $this->sTypeUrl . '&page=%s';
+    }
+
+    function getPageCss()
+    {
+    	$oTemplate = BxDolStudioTemplate::getInstance();
+    	$aUploaders = array_merge($this->aUploaders, $this->aUploadersCover);
+        foreach($aUploaders as $sUploader) {
+            $oUploader = BxDolUploader::getObjectInstance($sUploader, $this->sStorage, '', $oTemplate);
+            if($oUploader)
+                $oUploader->addCssJs();
+        }
+
+        return array_merge(parent::getPageCss(), array(
+            BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'codemirror/|codemirror.css',
+            BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'grapesjs/|grapes.min.css',
+            'page_layouts.css', 
+            'builder_page.css'
+        ));
+    }
+
+    function getPageJs()
+    {
+        return array_merge(parent::getPageJs(), array(
+            'codemirror/codemirror.min.js',
+            'grapesjs/grapes.min.js',
+            'grapesjs/grapesjs-blocks-basic.js',
+            'grapesjs/grapesjs-style-bg.js',
+            'grapesjs/grapesjs-preset-webpage.min.js',
+            'jquery-ui/jquery-ui.min.js',
+            'jquery.ui.touch-punch.min.js',
+            'jquery.easing.js',
+            'jquery.form.min.js',
+            'functions.js',
+            'builder_page.js'
+        ));
+    }
+
+    function getPageJsObject()
+    {
+        return 'oBxDolStudioBuilderPage';
+    }
+
+    function getPageMenu($aMenu = [], $aMarkers = [])
+    {
+        $aMenuItems = [
+            BX_DOL_STUDIO_MODULE_SYSTEM => [
+                'name' => BX_DOL_STUDIO_MODULE_SYSTEM,
+                'icon' => 'mi-bp-system.svg',
+                'icon_bg' => true,
+                'title' => '_adm_bp_cpt_type_' . BX_DOL_STUDIO_MODULE_SYSTEM,
+            ],
+            BX_DOL_STUDIO_MODULE_CUSTOM => [
+                'name' => BX_DOL_STUDIO_MODULE_CUSTOM,
+                'icon' => 'mi-bp-custom.svg',
+                'icon_bg' => true,
+                'title' => '_adm_bp_cpt_type_' . BX_DOL_STUDIO_MODULE_CUSTOM,
+            ]
+        ];
+
+        $aModulesDb = BxDolModuleQuery::getInstance()->getModulesBy(['type' => 'type', 'value' => [BX_DOL_MODULE_TYPE_MODULE, BX_DOL_MODULE_TYPE_TEMPLATE]]);
+        foreach($aModulesDb as $aModuleDb) {
+            $sName = $aModuleDb['name'];
+            if($sName == BX_DOL_STUDIO_MODULE_SYSTEM)
+                continue;
+
+            $aMenuItems[] = [
+                'name' => $sName,
+                'icon' => BxDolStudioUtils::getModuleIcon($aModuleDb, 'menu', false),
+                'icon_bg' => false,
+                'title' => BxDolStudioUtils::getModuleTitle($sName)
+            ]; 
+        }
+
+        $aMenu = [];
+        foreach($aMenuItems as $aMenuItem)
+            $aMenu[] = array_merge($aMenuItem, [
+                'link' =>  sprintf($this->sTypeUrl, $aMenuItem['name']),
+                'selected' => $aMenuItem['name'] == $this->sType
+            ]);
+
+        return parent::getPageMenu($aMenu);
+    }
+
+    function getPageCode($sPage = '', $bWrap = true)
+    {
+        $sResult = parent::getPageCode($sPage, $bWrap);
+        if($sResult === false)
+            return false;
+
+        $oPage = BxTemplPage::getObjectInstance($this->sPage);
+
+        $oTemplate = BxDolStudioTemplate::getInstance();
+        $sJsObject = $this->getPageJsObject();
+        $sTxtEmpty = _t('_sys_txt_empty');
+
+        $sContent = "";
+        if(($bPage = $this->sPage != '') === true) {
+            /**
+             * Reset (move to hidden sell) blocks which cannot be seen, 
+             * because of unsuitable cell number to currently selected layout.
+             */
+            $this->oDb->resetBlocksByPage($this->aPageRebuild['object'], $this->aPageRebuild['layout_cells_number']);
+
+            $aTmplVars = array(
+                'page_id' => 'bx-page-' . $this->aPageRebuild['uri'],
+                'bx_if:show_layout_row_dump' => array(
+                    'condition' => true,
+                    'content' => array()
+                )
+            );
+            for($i = 0; $i <= $this->aPageRebuild['layout_cells_number']; $i++) {
+                $aBlocks = array();
+                $this->oDb->getBlocks(array('type' => 'by_object_cell', 'object' => $this->aPageRebuild['object'], 'cell' => $i), $aBlocks, false);
+
+                $aTmplVarsCell = array('id' => $i, 'bx_repeat:blocks' => array());
+                foreach($aBlocks as $aBlock) {
+                    $sTitle = strip_tags(!empty($aBlock['title_system']) ? _t($aBlock['title_system']) : $oPage->getBlockTitle($aBlock));
+                    list($sIcon, $sIconUrl) = $this->getBlockIcon($aBlock);
+
+                    $aTmplVarsCell['bx_repeat:blocks'][] = array(
+                        'html_id' => $this->aHtmlIds['block_id'] . $aBlock['id'],
+                        'bx_if:is_inactive' => array(
+                            'condition' => (int)$aBlock['active'] == 0,
+                            'content' => array()
+                        ),
+                        'bx_if:show_link' => array(
+                            'condition' => true,
+                            'content' => array(
+                                'onclick' => $sJsObject . ".performAction('block_edit', {id: " . $aBlock['id'] . "})",
+                                'title' => !empty($sTitle) ? $sTitle : $sTxtEmpty,
+                            )
+                        ),
+                        'bx_if:show_text' => array(
+                            'condition' => false,
+                            'content' => array()
+                        ),
+                        'bx_if:show_api' => [
+                            'condition' => (int)$aBlock['active_api'] != 0,
+                            'content' => []
+                        ],
+                        'bx_if:image' => array (
+                            'condition' => (bool)$sIconUrl,
+                            'content' => array('icon_url' => $sIconUrl),
+                        ),
+                        'bx_if:icon' => array (
+                            'condition' => (bool)$sIcon,
+                            'content' => array('icon' => $sIcon),
+                        ),
+                        'module' => $this->getModuleTitle($aBlock['module']),
+                        'visible_for' => _t('_adm_bp_txt_visible_for', BxDolStudioUtils::getVisibilityTitle($aBlock['visible_for_levels'])),
+                        'bx_if:show_checkbox' => array(
+                            'condition' => false,
+                            'content' => array()
+                        ),
+                        'bx_if:show_drag_handle' => array(
+                            'condition' => true,
+                            'content' => array()
+                        )
+                    );
+                }
+
+                $sCell = $oTemplate->parseHtmlByName('bp_cell.html', $aTmplVarsCell);
+                if($i == 0)
+                    $aTmplVars['bx_if:show_layout_row_dump']['content']['cell_' . $i] = $sCell;
+                else
+                    $aTmplVars['cell_' . $i] = $sCell;
+            }
+
+            $sContent = $oTemplate->parseHtmlByName($this->aPageRebuild['layout_template'], $aTmplVars);
+        }
+
+        $aLanguages = BxDolStudioLanguagesUtils::getInstance()->getLanguages();
+
+        $aTmplVars = array(
+            'js_object' => $sJsObject,
+            'action_url' => $this->sBaseUrl,
+            'page_url' => sprintf($this->sPageUrl, '{0}', '{1}'),
+            'type' => $this->sType,
+            'page' => $this->sPage,
+            'html_ids' => json_encode($this->aHtmlIds),
+            'languahes' => json_encode($aLanguages),
+            'content' => $this->getBlockCode(array(
+                'items' => $sContent
+            ))
+        );
+
+        $oTemplate->addJsTranslation(array(
+            '_adm_bp_wrn_page_delete',
+            '_adm_bp_wrn_page_block_delete'
+        ));
+        return $sResult . $oTemplate->parseHtmlByName('builder_page.html', $aTmplVars);
+    }
+
+    function getBlockPanelTop($aBlock)
+    {
+        return parent::getBlockPanelTop(
+            array('panel_top' => BxDolStudioTemplate::getInstance()->parseHtmlByName('bp_block_panel_top.html', $this->_getTmplVarsBlockPanelTop()))
+        );
+    }
+
+    protected function actionPageCreate()
+    {
+        $sJsObject = $this->getPageJsObject();
+        $oTemplate = BxDolStudioTemplate::getInstance();
+
+        $sModule = BX_DOL_STUDIO_MODULE_CUSTOM;
+
+        $aForm = array(
+            'form_attrs' => array(
+                'id' => 'adm-bp-page-create',
+                'action' => sprintf($this->sPageUrl, $this->sType, $this->sPage) . '&bp_action=' . $this->sActionPageCreate,
+                'method' => 'post'
+            ),
+            'params' => array (
+                'db' => array(
+                    'table' => 'sys_objects_page',
+                    'key' => 'id',
+                    'uri' => '',
+                    'uri_title' => '',
+                    'submit_name' => 'do_submit'
+                ),
+            ),
+            'inputs' => array (
+                'module' => array(
+                    'type' => 'hidden',
+                    'name' => 'module',
+                    'value' => $sModule,
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+                'deletable' => array(
+                    'type' => 'hidden',
+                    'name' => 'deletable',
+                    'value' => 1,
+                    'db' => array (
+                        'pass' => 'Int',
+                    ),
+                ),
+                'settings' => array(
+                    'type' => 'custom',
+                    'name' => 'settings',
+                    'content' => $oTemplate->parseHtmlByName('bp_edit_page_form.html', $this->_getTmplVarsPageSettings()),
+                ),
+            )
+        );
+
+        $oForm = new BxTemplStudioFormView($aForm);
+        $oForm->initChecker();
+
+        if($oForm->isSubmittedAndValid()) {
+            unset($oForm->aInputs['settings']);
+            foreach($this->aPageSettings as $aSetting)
+                $oForm->aInputs = array_merge($oForm->aInputs, $this->{'getSettings' . $this->getClassName($aSetting['name']) . 'Fields'}());
+
+            $sLanguage = BxDolStudioLanguagesUtils::getInstance()->getCurrentLangName(false);
+
+            $sObject = BxDolForm::getSubmittedValue('title-' . $sLanguage, $aForm['form_attrs']['method']);
+            $sObject = uriGenerate($sObject, 'sys_objects_page', 'object', ['empty' => 'object']);
+
+            $sUri = $oForm->getCleanValue('uri');
+            
+            $aPage = $this->oDb->getPages(['type' => 'by_uri', 'value' => $sUri]);
+            if(!empty($aPage) && is_array($aPage)) 
+            	return ['msg' => _t('_adm_bp_err_page_uri')];
+
+            $iVisibleFor = BxDolStudioUtils::getVisibilityValue($oForm->getCleanValue('visible_for'), $oForm->getCleanValue('visible_for_levels'));
+            BxDolForm::setSubmittedValue('visible_for_levels', $iVisibleFor, $aForm['form_attrs']['method']);
+            unset($oForm->aInputs['visible_for']);
+
+            $aValsToAdd = [
+                'author' => bx_get_logged_profile_id(), 
+                'object' => $sObject, 
+                'url' => $this->sPageBaseUrl . $sUri, 
+                'added' => time()
+            ];
+
+            $this->onSavePage($oForm, $aValsToAdd);
+
+            $iId = (int)$oForm->insert($aValsToAdd);
+            if($iId != 0)
+                return ['eval' => $sJsObject . '.onCreatePage(\'' . $sModule . '\', \'' . $sObject . '\')'];
+            else
+                return ['msg' => _t('_adm_bp_err_page_create')];
+        }
+
+        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($this->aHtmlIds['add_popup_id'], _t('_adm_bp_txt_create_popup'), $oTemplate->parseHtmlByName('bp_add_page.html', array(
+            'js_object' => $sJsObject,
+            'form_id' => $aForm['form_attrs']['id'],
+            'form' => $oForm->getCode(true)
+        )));
+
+        return array('popup' => $sContent);
+    }
+
+    protected function actionPageEdit()
+    {
+        $sJsObject = $this->getPageJsObject();
+        $oTemplate = BxDolStudioTemplate::getInstance();
+
+        $aForm = array(
+            'form_attrs' => array(
+                'id' => 'adm-bp-page-edit',
+                'action' => sprintf($this->sPageUrl, $this->sType, $this->sPage) . '&bp_action=' . $this->sActionPageEdit,
+                'method' => 'post'
+            ),
+            'params' => array (
+                'db' => array(
+                    'table' => 'sys_objects_page',
+                    'key' => 'id',
+                    'uri' => '',
+                    'uri_title' => '',
+                    'submit_name' => 'do_submit'
+                ),
+            ),
+            'inputs' => array (
+                'settings' => array(
+                    'type' => 'custom',
+                    'name' => 'settings',
+                    'content' => $oTemplate->parseHtmlByName('bp_edit_page_form.html', $this->_getTmplVarsPageSettings($this->aPageRebuild, false)),
+                ),
+            )
+        );
+
+        $oForm = new BxTemplStudioFormView($aForm);
+        $oForm->initChecker();
+
+        if($oForm->isSubmittedAndValid()) {
+            unset($oForm->aInputs['settings']);
+            foreach($this->aPageSettings as $aSetting)
+                $oForm->aInputs = array_merge($oForm->aInputs, $this->{'getSettings' . $this->getClassName($aSetting['name']) . 'Fields'}($this->aPageRebuild, false));
+
+            $mixedVisibleFor = $oForm->getCleanValue('visible_for');
+            $mixedVisibleForLevels = $oForm->getCleanValue('visible_for_levels');
+            if($mixedVisibleFor !== false && $mixedVisibleForLevels !== false) {
+                $iVisibleFor = BxDolStudioUtils::getVisibilityValue($mixedVisibleFor, $mixedVisibleForLevels);
+                BxDolForm::setSubmittedValue('visible_for_levels', $iVisibleFor, $aForm['form_attrs']['method']);
+                unset($oForm->aInputs['visible_for']);
+            }
+
+            $this->onSavePage($oForm, $this->aPageRebuild);
+
+            if($oForm->update($this->aPageRebuild['id'])) {
+                $iLevelId = $oForm->getCleanValue('layout_id');
+                if(!empty($iLevelId) && $iLevelId != $this->aPageRebuild['layout_id']) {
+                    $aLayoutOld = array();
+                    $this->oDb->getLayouts(array('type' => 'by_id', 'value' => $this->aPageRebuild['layout_id']), $aLayoutOld, false);
+
+                    $aLayoutNew = array();
+                    $this->oDb->getLayouts(array('type' => 'by_id', 'value' => $iLevelId), $aLayoutNew, false);
+
+                    if($aLayoutOld['cells_number'] > $aLayoutNew['cells_number'] && $this->oDb->resetBlocksByPage($this->sPage, $aLayoutNew['cells_number']) === false)
+                        return array('msg' => _t('_adm_bp_err_save'));
+
+                    return array('eval' => $sJsObject . '.onSaveSettingsLayout()');
+                }
+
+                return array();
+            } 
+            else
+                return array('msg' => _t('_adm_bp_err_save'));
+        }
+
+        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($this->aHtmlIds['edit_popup_id'], _t('_adm_bp_txt_settings_popup'), $oTemplate->parseHtmlByName('bp_edit_page.html', array(
+            'js_object' => $sJsObject,
+            'form_id' => $aForm['form_attrs']['id'],
+            'form' => $oForm->getCode(true)
+        )));
+
+        return array('popup' => $sContent);
+    }
+
+    protected function actionPageDelete()
+    {
+        if(empty($this->sPage) || empty($this->aPageRebuild) || !is_array($this->aPageRebuild))
+            return array('msg' => _t('_adm_bp_err_page_delete'));
+
+        $oLangauge = BxDolStudioLanguagesUtils::getInstance();
+        $oStorage = BxDolStorage::getObjectInstance(BX_DOL_STORAGE_OBJ_IMAGES);
+
+        $aBlocks = array();
+        $this->oDb->getBlocks(array('type' => 'by_object', 'value' => $this->sPage), $aBlocks, false);
+        if(is_array($aBlocks) && !empty($aBlocks)) {
+            foreach($aBlocks as $aBlock)
+                $this->onBlockDelete($aBlock);
+
+            $this->oDb->deleteBlocks(array('type' => 'by_object', 'value' => $this->sPage));
+        }
+
+        if($this->oDb->deletePages(array('type' => 'by_object', 'value' => $this->sPage))) {
+            $oLangauge->deleteLanguageString($this->aPageRebuild['title_system']);
+            $oLangauge->deleteLanguageString($this->aPageRebuild['title']);
+            return array('eval' => 'window.parent.location.href = "' . sprintf($this->sTypeUrl, $this->sType) . '";');
+        }
+
+        return array('msg' => _t('_adm_bp_err_page_delete'));
+    }
+
+    protected function actionBlockList()
+    {
+        $sModule = BX_DOL_STUDIO_BP_TYPE_DEFAULT;
+        if(bx_get('bp_module') !== false)
+            $sModule = bx_process_input(bx_get('bp_module'));
+
+        return array(
+            'content' => $this->getBlockList($sModule)
+        );
+    }
+
+    protected function actionBlockCreate()
+    {
+        $sJsObject = $this->getPageJsObject();
+        $oTemplate = BxDolStudioTemplate::getInstance();
+
+        $sSelected = BX_DOL_STUDIO_BP_SKELETONS;
+
+        $aForm = array(
+            'form_attrs' => array(
+                'id' => 'adm-bp-block-create',
+                'action' => sprintf($this->sPageUrl, $this->sType, $this->sPage) .  '&bp_action=' . $this->sActionBlockCreate,
+                'method' => 'post'
+            ),
+            'params' => array (
+                'db' => array(
+                    'table' => 'sys_pages_blocks',
+                    'key' => 'id',
+                    'uri' => '',
+                    'uri_title' => '',
+                    'submit_name' => 'do_submit'
+                ),
+            ),
+            'inputs' => array (
+                'blocks' => array(
+                    'type' => 'custom',
+                    'name' => 'blocks',
+                    'content' => '',
+                    'db' => array (
+                        'pass' => 'Int',
+                    ),
+                ),
+            )
+        );
+
+        $aMenu = [
+            BX_DOL_STUDIO_BP_SKELETONS => [
+                'name' => BX_DOL_STUDIO_BP_SKELETONS,
+                'icon' => 'mi-qrcode.svg',
+                'icon_bg' => true,
+                'title' => '_sys_block_types_skeletons',
+                'selected' => $sSelected == BX_DOL_STUDIO_BP_SKELETONS,
+            ],
+            BX_DOL_STUDIO_MODULE_SYSTEM => [
+                'name' => BX_DOL_STUDIO_MODULE_SYSTEM,
+                'icon' => 'mi-cog.svg',
+                'icon_bg' => true,
+                'title' => '_sys_block_types_system',
+                'selected' => $sSelected == BX_DOL_STUDIO_MODULE_SYSTEM,
+            ],
+            BX_DOL_STUDIO_MODULE_CUSTOM => [
+                'name' => BX_DOL_STUDIO_MODULE_CUSTOM,
+                'icon' => 'mi-wrench.svg',
+                'icon_bg' => true,
+                'title' => '_sys_block_types_custom',
+                'selected' => $sSelected == BX_DOL_STUDIO_MODULE_CUSTOM,
+            ]
+        ];
+
+        $aModules = BxDolModuleQuery::getInstance()->getModulesBy(['type' => 'type', 'value' => [BX_DOL_MODULE_TYPE_MODULE, BX_DOL_MODULE_TYPE_TEMPLATE]]);
+        $aModulesWithBlocks = $this->oDb->getModulesWithCopyableBlocks();
+        foreach($aModules as $aModule) {
+            $sName = $aModule['name'];
+            if(!in_array($sName, $aModulesWithBlocks))
+                continue;
+
+            if(!empty($aMenu[$sName]))
+                $aMenu[$sName] = array_merge($aMenu[$sName], $aModule);
+            else
+                $aMenu[$sName] = $aModule;
+
+            if(empty($aMenu[$sName]['icon']))
+                $aMenu[$sName]['icon'] = BxDolStudioUtils::getModuleIcon($aModule, 'menu', false); 
+        }
+
+        foreach($aMenu as $sKey => $aItem)
+            $aMenu[$sKey]['onclick'] =  $sJsObject . '.onChangeModule(\'' . $aItem['name'] . '\', this);';
+
+        $oMenu = new BxTemplStudioMenu(['template' => 'menu_side.html', 'menu_items' => $aMenu]);
+
+        $aForm['inputs']['blocks']['content'] = $oTemplate->parseHtmlByName('bp_add_block_form.html', [
+            'menu' => $oMenu->getCode(),
+            'html_block_lists_id' => $this->aHtmlIds['block_lists_id'],
+            'blocks' => $this->getBlockList($sSelected)
+        ]);
+
+        $oForm = new BxTemplStudioFormView($aForm);
+        $oForm->initChecker();
+
+        if($oForm->isSubmittedAndValid()) {
+            $aIds = $oForm->getCleanValue('blocks');
+
+            $aBlocks = array();
+            $this->oDb->getBlocks(array('type' => 'by_ids', 'value' => $aIds), $aBlocks, false);
+
+            $oLanguage = BxDolStudioLanguagesUtils::getInstance();
+            $oStorege = BxDolStorage::getObjectInstance(BX_DOL_STORAGE_OBJ_IMAGES);
+
+            $bResult = true;
+            foreach($aBlocks as $aBlock) {
+            	$sModule = $aBlock['module'];
+                $sTitleKey = $this->getSystemName($aBlock['title'] . '_' . time());
+                $aTitleValues = $oLanguage->getLanguageString($aBlock['title']);
+
+                unset($aBlock['id']);
+                $aBlock['object'] = $this->sPage;
+                $aBlock['cell_id'] = 1;
+                $aBlock['module'] = $this->getBlockModule($aBlock);
+                $aBlock['title'] = $sTitleKey;
+                $aBlock['copyable'] = $sModule == BX_DOL_STUDIO_BP_SKELETONS ? 1 : 0;
+                $aBlock['deletable'] = 1;
+
+                //--- Process Lang copy
+                $sContentKey = $sContentValue = "";
+                if($aBlock['type'] == BX_DOL_STUDIO_BP_BLOCK_LANG && $aBlock['content'] != '') {
+                    $sContentKey = $this->getSystemName($aBlock['content'] . '_' . time());
+                    $sContentValue = _t($aBlock['content']);
+
+                    $aBlock['content'] = $sContentKey;
+                    $oLanguage->addLanguageString($sContentKey, $sContentValue);
+                }
+
+                //--- Process Image copy
+                $iImageId = $sImageAlign = "";
+                if($aBlock['type'] == BX_DOL_STUDIO_BP_BLOCK_IMAGE && $aBlock['content'] != '') {
+                    list($iImageId, $sImageAlign) = explode($this->sParamsDivider, $aBlock['content']);
+
+                    $aBlock['content'] = "";
+                    if(is_numeric($iImageId) && (int)$iImageId != 0 && ($iImageId = $oStorege->storeFileFromStorage(array('id' => $iImageId))) !== false)
+                        $aBlock['content'] = implode($this->sParamsDivider, array($iImageId, $sImageAlign));
+                }
+
+                // add indexing data
+                $aBlock = array_merge($aBlock, $this->getIndexingDataForBlock($aBlock['type'], $aBlock['content']));
+
+                if(!$this->oDb->insertBlock($aBlock)) {
+                    if($sContentKey != "")
+                        $oLanguage->deleteLanguageString($sContentKey);
+
+                    if($iImageId != "")
+                        $oStorege->deleteFile((int)$iImageId, 0);
+
+                    $bResult = false;
+                    break;
+                }
+
+                //--- Process Title copy
+                foreach($aTitleValues as $iLangId => $aTitleValue)
+                	$oLanguage->addLanguageString($sTitleKey, $aTitleValue['string'], $iLangId);
+            }
+
+            if($bResult)
+                return array('eval' => $sJsObject . '.onCreateBlock(oData)');
+            else
+                return array('msg' => _t('_adm_bp_err_block_added'));
+        }
+
+        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($this->aHtmlIds['create_block_popup_id'], _t('_adm_bp_txt_new_block_popup'), $oTemplate->parseHtmlByName('bp_add_block.html', array(
+        	'action' => 'create',
+            'form_id' => $aForm['form_attrs']['id'],
+            'form' => $oForm->getCode(true)
+        )));
+
+        return array('popup' => $sContent);
+    }
+
+    protected function actionBlockEdit()
+    {
+        $sJsObject = $this->getPageJsObject();
+        $oTemplate = BxDolStudioTemplate::getInstance();
+
+        $iId = (int)bx_get('id');
+        if(!$iId)
+            return array();
+
+        $aBlock = array();
+        $this->oDb->getBlocks(array('type' => 'by_id', 'value' => $iId), $aBlock, false);
+        if(empty($aBlock) || !is_array($aBlock))
+            return array('msg' => _t('_adm_bp_err_block_not_found'));
+
+        bx_import('BxDolStudioUtils');
+        $aForm = array(
+            'form_attrs' => array(
+                'id' => 'adm-bp-block-edit',
+                'action' => sprintf($this->sPageUrl, $this->sType, $this->sPage) . '&bp_action=' . $this->sActionBlockEdit,
+                'method' => 'post'
+            ),
+            'params' => array (
+                'db' => array(
+                    'table' => 'sys_pages_blocks',
+                    'key' => 'id',
+                    'uri' => '',
+                    'uri_title' => '',
+                    'submit_name' => 'do_submit'
+                ),
+            ),
+            'inputs' => array (
+                'id' => array(
+                    'type' => 'hidden',
+                    'name' => 'id',
+                    'value' => $iId,
+                    'db' => array (
+                        'pass' => 'Int',
+                    ),
+                ),
+                'title' => array(
+                    'type' => 'text_translatable',
+                    'name' => 'title',
+                    'caption' => _t('_adm_bp_txt_block_title'),
+                    'info' => _t('_adm_bp_dsc_block_title'),
+                    'value' => $aBlock['title'],
+                    'required' => '1',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                    'checker' => array (
+                        'func' => 'LengthTranslatable',
+                        'params' => array(3, 100, 'title'),
+                        'error' => _t('_adm_bp_err_block_title'),
+                    ),
+                ),
+                'designbox_id' => array(
+                    'type' => 'select',
+                    'name' => 'designbox_id',
+                    'caption' => _t('_adm_bp_txt_block_designbox'),
+                    'info' => '',
+                    'value' => $this->sSelectKeyPrefix . $aBlock['designbox_id'],
+                    'values' => array(
+                        array('key' => '', 'value' => _t('_adm_bp_txt_block_designbox_empty')),
+                    ),
+                    'required' => '1',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                    'checker' => array (
+                        'func' => 'avail',
+                        'params' => array(),
+                        'error' => _t('_adm_bp_err_block_designbox'),
+                    ),
+                ),
+                'async' => array(
+                    'type' => 'select',
+                    'name' => 'async',
+                    'caption' => _t('_adm_bp_txt_block_async'),
+                    'info' => '',
+                    'value' => isset($aBlock['async']) ? $aBlock['async'] : 0,
+                    'required' => '0',
+                    'db' => array (
+                        'pass' => 'Int',
+                    ),
+                ),
+                'submenu' => array(
+                    'type' => 'select',
+                    'name' => 'submenu',
+                    'caption' => _t('_adm_bp_txt_block_submenu'),
+                    'info' => '',
+                    'value' => isset($aBlock['submenu']) ? $aBlock['submenu'] : '',
+                    'required' => '0',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+                'tabs' => array(
+                    'type' => 'switcher',
+                    'name' => 'tabs',
+                    'caption' => _t('_adm_bp_txt_block_tabs'),
+                    'info' => '',
+                    'value' => '1',
+                    'checked' => $aBlock['tabs'] == '1',
+                    'db' => array (
+                        'pass' => 'Int',
+                    )
+                ),
+                'hidden_on' => array(
+                    'type' => 'select_multiple',
+                    'name' => 'hidden_on',
+                    'caption' => _t('_adm_bp_txt_block_hidden_on'),
+                    'info' => '',
+                    'value' => (int)$aBlock['hidden_on'],
+                    'values' => array(
+                        BX_DB_HIDDEN_PHONE => _t('_adm_bp_txt_block_hidden_on_phone'),
+                        BX_DB_HIDDEN_TABLET => _t('_adm_bp_txt_block_hidden_on_tablet'),
+                        BX_DB_HIDDEN_DESKTOP => _t('_adm_bp_txt_block_hidden_on_desktop'),
+                        BX_DB_HIDDEN_MOBILE => _t('_adm_bp_txt_block_hidden_on_mobile')
+                    ),
+                    'db' => array (
+                        'pass' => 'Set',
+                    )
+                ),
+                'visible_for' => array(
+                    'type' => 'select',
+                    'name' => 'visible_for',
+                    'caption' => _t('_adm_bp_txt_block_visible_for'),
+                    'info' => '',
+                    'value' => $aBlock['visible_for_levels'] == BX_DOL_INT_MAX ? BX_DOL_STUDIO_VISIBLE_ALL : BX_DOL_STUDIO_VISIBLE_SELECTED,
+                    'values' => array(
+                        array('key' => BX_DOL_STUDIO_VISIBLE_ALL, 'value' => _t('_adm_bp_txt_block_visible_for_all')),
+                        array('key' => BX_DOL_STUDIO_VISIBLE_SELECTED, 'value' => _t('_adm_bp_txt_block_visible_for_selected')),
+                    ),
+                    'required' => '0',
+                    'attrs' => array(
+                        'onchange' => $sJsObject . '.onChangeVisibleFor(this)'
+                    ),
+                    'db' => array (
+                        'pass' => 'Xss',
+                    )
+                ),
+                'visible_for_levels' => array(
+                    'type' => 'checkbox_set',
+                    'name' => 'visible_for_levels',
+                    'caption' => _t('_adm_bp_txt_block_visible_for_levels'),
+                    'info' => _t('_adm_bp_dsc_block_visible_for_levels'),
+                    'value' => array(),
+                    'values' => array(),
+                    'tr_attrs' => array(
+                        'style' => $aBlock['visible_for_levels'] == BX_DOL_INT_MAX ? 'display:none' : ''
+                    ),
+                    'db' => array (
+                        'pass' => 'Int',
+                    )
+                ),
+                'content_empty' => array(
+                    'type' => 'text_translatable',
+                    'name' => 'content_empty',
+                    'caption' => _t('_adm_bp_txt_block_content_empty'),
+                    'info' => _t('_adm_bp_dsc_block_content_empty'),
+                    'value' => $aBlock['content_empty'],
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+                'help' => array(
+                    'type' => 'textarea_translatable',
+                    'name' => 'help',
+                    'caption' => _t('_adm_bp_txt_block_help'),
+                    'info' => '',
+                    'value' => $aBlock['help'],
+                    'required' => '0',
+                    'html' => 2,
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+                'class' => array(
+                    'type' => 'text',
+                    'name' => 'class',
+                    'caption' => _t('_adm_bp_txt_block_class'),
+                    'info' => '',
+                    'value' => $aBlock['class'],
+                    'required' => '0',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+                'cache_lifetime' => array(
+                    'type' => 'text',
+                    'name' => 'cache_lifetime',
+                    'caption' => _t('_adm_bp_txt_block_cache_lifetime'),
+                    'info' => '',
+                    'value' => isset($aBlock['cache_lifetime']) ? $aBlock['cache_lifetime'] : 0,
+                    'required' => '0',
+                    'db' => array (
+                        'pass' => 'Int',
+                    ),
+                ),
+                'active' => array(
+                    'type' => 'switcher',
+                    'name' => 'active',
+                    'caption' => _t('_adm_bp_txt_block_active'),
+                    'info' => '',
+                    'value' => '1',
+                    'checked' => $aBlock['active'] == '1',
+                    'db' => array (
+                        'pass' => 'Int',
+                    )
+                ),
+                'active_api' => array(
+                    'type' => 'switcher',
+                    'name' => 'active_api',
+                    'caption' => _t('_adm_bp_txt_block_active_api'),
+                    'info' => '',
+                    'value' => '1',
+                    'checked' => $aBlock['active_api'] == '1',
+                    'db' => array (
+                        'pass' => 'Int',
+                    )
+                ),
+                'controls' => array(
+                    'name' => 'controls',
+                    'type' => 'input_set',
+                    array(
+                        'type' => 'submit',
+                        'name' => 'do_submit',
+                        'value' => _t('_adm_bp_btn_block_save')
+                    ),
+                    array (
+                        'type' => 'reset',
+                        'name' => 'close',
+                        'value' => _t('_adm_bp_btn_block_cancel'),
+                        'attrs' => array(
+                            'onclick' => $sJsObject . '.onEditBlockCancel(this)',
+                            'class' => 'bx-def-margin-sec-left',
+                        ),
+                    )
+                )
+            )
+        );
+
+        $aDBoxes = array();
+        $this->oDb->getDesignBoxes(array('type' => 'ordered'), $aDBoxes, false);
+        foreach($aDBoxes as $aDBox)
+            $aForm['inputs']['designbox_id']['values'][] = array('key' => $this->sSelectKeyPrefix . $aDBox['id'], 'value' => _t($aDBox['title']));
+
+        $aContentPlaceholders = $this->oDb->getContentPlaceholders();
+        $aForm['inputs']['async']['values'][] = array('key' => 0, 'value' => _t('_sys_no_async'));
+        foreach ($aContentPlaceholders as $iKey => $sTitle)
+            $aForm['inputs']['async']['values'][] = array('key' => $iKey, 'value' => _t($sTitle));
+
+        $aSubmenus = $this->oDb->getBlockSubmenus();
+        $aForm['inputs']['submenu']['values'][] = array('key' => '', 'value' => _t('_sys_menu_item_title_inherited'));
+        $aForm['inputs']['submenu']['values'][] = array('key' => 'disabled', 'value' => _t('_sys_menu_item_title_disabled'));
+        foreach ($aSubmenus as $sObject => $sTitle)
+            $aForm['inputs']['submenu']['values'][] = array('key' => $sObject, 'value' => _t($sTitle));
+
+        BxDolStudioUtils::getVisibilityValues($aBlock['visible_for_levels'], $aForm['inputs']['visible_for_levels']['values'], $aForm['inputs']['visible_for_levels']['value']);
+
+        $aForm['inputs'] = $this->addInArray($aForm['inputs'], 'visible_for_levels', $this->getBlockContent($aBlock));
+
+        if((int)$aBlock['deletable'] != 0)
+            $aForm['inputs']['controls'][] = array (
+                'type' => 'reset',
+                'name' => 'close',
+                'value' => _t('_adm_bp_btn_block_delete'),
+                'attrs' => array(
+                    'onclick' => $sJsObject . '.deleteBlock(' . $aBlock['id'] . ')',
+                    'class' => 'bx-def-margin-sec-left',
+                ),
+            );
+
+        $oForm = new BxTemplStudioFormView($aForm);
+        $oForm->initChecker();
+
+        if($oForm->isSubmittedAndValid()) {
+            $this->onSaveBlock($oForm, $aBlock);
+
+            // add indexing data
+            $aBlockAddon = $this->getIndexingDataForBlock($aBlock['type'], $oForm->getCleanValue('content'));
+
+            if($oForm->update($iId, $aBlockAddon) !== false)
+                return array('eval' => $sJsObject . '.onEditBlock(oData)');
+            else
+                return array('msg' => _t('_adm_bp_err_block_edit'));
+        }
+
+        $sContent = BxTemplStudioFunctions::getInstance()->popupBox($this->aHtmlIds['edit_block_popup_id'], _t('_adm_bp_txt_edit_block_popup', _t($aBlock['title_system'])), $oTemplate->parseHtmlByName('bp_add_block.html', array(
+        	'action' => 'edit',
+            'form_id' => $aForm['form_attrs']['id'],
+            'form' => $oForm->getCode(true)
+        )));
+
+        return array('popup' => array('html' => $sContent, 'options' => array('onBeforeShow' => $sJsObject . '.onEditBlockBeforeShow($el)')));
+    }
+
+    protected function actionBlockDelete()
+    {
+        $sJsObject = $this->getPageJsObject();
+        $iId = (int)bx_get('id');
+
+        $aBlock = array();
+        $this->oDb->getBlocks(array('type' => 'by_id', 'value' => $iId), $aBlock, false);
+        if(empty($aBlock) || !is_array($aBlock))
+            return array('msg' => _t('_adm_bp_err_block_not_found'));
+
+        if((int)$aBlock['deletable'] == 0)
+            return array('msg' => _t('_adm_bp_err_block_not_deletable'));
+
+        if(!$this->oDb->deleteBlocks(array('type' => 'by_id', 'value' => $iId)))
+            return array('msg' => _t('_adm_bp_err_block_delete'));
+
+        $this->onBlockDelete($aBlock);
+        return array('eval' => $sJsObject . '.onDeleteBlock(' . $iId . ', oData)');
+    }
+
+    protected function actionUriGet()
+    {
+        $oLanguage = BxDolStudioLanguagesUtils::getInstance();
+        $aLanguages = $oLanguage->getLanguages();
+
+        $sLanguageDef = $oLanguage->getDefaultLangName();
+        $sLanguageCur = $oLanguage->getCurrentLangName(false);
+
+        $aValues = array();
+        foreach($aLanguages as $sName => $sTitle)
+            if(($mixedValue = bx_get($sName)) !== false)
+                $aValues[$sName] = bx_process_input($mixedValue);
+
+        $sUri = "";
+        if(($mixedValue = bx_get('uri')) !== false)
+        	$sUri = bx_process_input($mixedValue);
+        else if(array_key_exists('en', $aValues) && $aValues['en'] != '')
+            $sUri = $aValues['en'];
+        else if(array_key_exists($sLanguageDef, $aValues) && $aValues[$sLanguageDef] != '')
+            $sUri = $aValues[$sLanguageDef];
+        else if(array_key_exists($sLanguageCur, $aValues) && $aValues[$sLanguageCur] != '')
+            $sUri = $aValues[$sLanguageCur];
+        else if(count($aValues) > 0)
+            foreach($aValues as $sValue)
+                if(!empty($sValue)) {
+                    $sUri = $sValue;
+                    break;
+                }
+
+        $sUri = $sUri != "" ? uriGenerate($sUri, 'sys_objects_page', 'uri') : "";
+        $sUrl = BxDolPermalinks::getInstance()->permalink($this->sPageBaseUrl . $sUri);
+
+        return array('eval' => $this->getPageJsObject() . '.onGetUri(oData)', 'uri' => $sUri, 'url' => $sUrl);
+    }
+
+    protected function getSettingsOptions($aPage = array(), $bCreate = true, $bInputsOnly = false)
+    {
+        $aForm = array(
+            'form_attrs' => array(
+                'id' => 'adm-bp-settings-options',
+            ),
+            'params' => array (
+                'remove_form' => '1',
+                'csrf' => array(
+                    'disable' => true
+                )
+            ),
+            'inputs' => array (
+                'title_system' => array(
+                    'type' => 'text_translatable',
+                    'name' => 'title_system',
+                    'caption' => _t('_adm_bp_txt_page_title_system'),
+                    'info' => _t('_adm_bp_dsc_page_title_system'),
+                    'value' => isset($aPage['title_system']) ? $aPage['title_system'] : '',
+                    'required' => '1',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                    'checker' => array (
+                        'func' => 'length',
+                        'params' => array(3,100),
+                        'error' => _t('_adm_bp_err_page_title_system'),
+                    ),
+                ),
+                'title' => array(
+                    'type' => 'text_translatable',
+                    'name' => 'title',
+                    'caption' => _t('_adm_bp_txt_page_title'),
+                    'info' => _t('_adm_bp_dsc_page_title'),
+                    'value' => isset($aPage['title']) ? $aPage['title'] : '',
+                    'required' => '1',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                    'checker' => array (
+                        'func' => 'length',
+                        'params' => array(3,100),
+                        'error' => _t('_adm_bp_err_page_title'),
+                    ),
+                ),
+                'url' => array(
+					'type' => 'text',
+					'name' => 'url',
+					'caption' => _t('_adm_bp_txt_page_url'),
+					'info' => _t('_adm_bp_dsc_page_url'),
+					'value' => isset($aPage['url']) ? BxDolPermalinks::getInstance()->permalink($aPage['url']) : '',
+					'required' => '0',
+					'attrs' => array(
+                		'id' => $this->aHtmlIds['url_field_id'],
+						'disabled' => 'disabled'
+					),
+				),
+				'type_id' => array(
+					'type' => 'select',
+					'name' => 'type_id',
+					'caption' => _t('_adm_bp_txt_page_type_id'),
+					'info' => _t('_adm_bp_dsc_page_type_id'),
+					'value' => isset($aPage['type_id']) ? $aPage['type_id'] : '',
+					'required' => '1',
+					'db' => array (
+                        'pass' => 'Int',
+                    ),
+				),
+                'submenu' => array(
+                    'type' => 'select',
+                    'name' => 'submenu',
+                    'caption' => _t('_adm_bp_txt_page_submenu'),
+                    'info' => _t('_adm_bp_dsc_page_submenu'),
+                    'value' => isset($aPage['submenu']) ? $aPage['submenu'] : '',
+                    'required' => '0',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+                'sticky_columns' => array(
+                    'type' => 'switcher',
+                    'name' => 'sticky_columns',
+                    'caption' => _t('_adm_bp_txt_page_sticky_columns'),
+                    'info' => '',
+                    'value' => '1',
+                    'checked' => isset($aPage['sticky_columns']) && $aPage['sticky_columns'] == 1,
+                    'db' => array (
+                        'pass' => 'Int',
+                    )
+                )
+            )
+        );
+        
+        $aTypes = array();
+        $this->oDb->getTypes(array('type' => 'all'), $aTypes, false);
+        foreach($aTypes as $aType)
+        	$aForm['inputs']['type_id']['values'][] = array('key' => $aType['id'], 'value' => _t($aType['title']));
+
+        $aSubmenus = $this->oDb->getSubmenus();
+        $aForm['inputs']['submenu']['values'][] = array('key' => '', 'value' => _t('_sys_menu_item_title_inherited'));
+        $aForm['inputs']['submenu']['values'][] = array('key' => 'disabled', 'value' => _t('_sys_menu_item_title_disabled'));
+        foreach ($aSubmenus as $sObject => $sTitle)
+            $aForm['inputs']['submenu']['values'][] = array('key' => $sObject, 'value' => _t($sTitle));
+
+        if($bCreate) {
+        	$sJsObject = $this->getPageJsObject();
+
+        	$aForm['inputs']['title']['attrs']['onblur'] = $sJsObject . '.getUri(this);';
+
+        	$aForm['inputs'] = bx_array_insert_before(array(
+        		'uri' => array(
+					'type' => 'text',
+					'name' => 'uri',
+					'caption' => _t('_adm_bp_txt_page_uri'),
+					'info' => _t('_adm_bp_dsc_page_uri'),
+					'value' => '',
+					'required' => '1',
+        			'attrs' => array(
+						'id' => $this->aHtmlIds['uri_field_id'],
+						'onblur' => $sJsObject . '.getUri(this);'
+					),
+					'db' => array (
+						'pass' => 'Xss',
+					),
+					'checker' => array (
+						'func' => 'length',
+						'params' => array(3,100),
+					    'error' => _t('_adm_bp_err_page_uri'),
+					),
+				)
+        	), $aForm['inputs'], 'url');
+        }
+
+        if($bInputsOnly)
+            return $aForm['inputs'];
+
+        $oForm = new BxTemplStudioFormView($aForm);
+        return $oForm->getCode();
+    }
+
+    protected function getSettingsOptionsFields($aPage = array(), $bCreate = true)
+    {
+    	return $this->getSettingsOptions($aPage, $bCreate, true);
+    }
+
+    protected function getSettingsCover($aPage = array(), $bCreate = true, $bInputsOnly = false)
+    {
+        $aForm = array(
+            'form_attrs' => array(
+                'id' => 'adm-bp-settings-cover',
+            ),
+            'params' => array (
+                'remove_form' => '1',
+                'csrf' => array(
+                    'disable' => true
+                )
+            ),
+            'inputs' => array (
+                'cover' => array(
+                    'type' => 'select',
+                    'name' => 'cover',
+                    'caption' => _t('_adm_bp_txt_page_cover'),
+                    'info' => '',
+                    'value' => isset($aPage['cover']) ? (int)$aPage['cover'] : '1',
+                    'values' => array(
+                        array('key' => 0, 'value' => _t('_adm_bp_txt_page_cover_0')),
+                        array('key' => 1, 'value' => _t('_adm_bp_txt_page_cover_1')),
+                        array('key' => 2, 'value' => _t('_adm_bp_txt_page_cover_2')),
+                        array('key' => 3, 'value' => _t('_adm_bp_txt_page_cover_3'))
+                    ),
+                    'db' => array (
+                        'pass' => 'Int',
+                    )
+                ),
+                'cover_image' => array(
+                    'type' => 'files',
+                    'name' => 'cover_image',
+                    'storage_object' => $this->sStorage,
+                    'images_transcoder' => $this->sTranscoderCover,
+                    'uploaders' => $this->aUploadersCover,
+                    'multiple' => false,
+                    'content_id' => isset($aPage['id']) ? $aPage['id'] : 0,
+                    'ghost_template' => BxTemplStudioFunctions::getInstance()->getDefaultGhostTemplate('cover_image'),
+                    'caption' => _t('_adm_bp_txt_page_cover_image'),
+                    'db' => array (
+                        'pass' => 'Int',
+                    )
+                ),
+                'cover_title' => array(
+                    'type' => 'text_translatable',
+                    'name' => 'cover_title',
+                    'caption' => _t('_adm_bp_txt_page_cover_title'),
+                    'info' => '',
+                    'value' => isset($aPage['cover_title']) ? $aPage['cover_title'] : '',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    )
+                ),
+            )
+        );
+
+        if($bInputsOnly)
+            return $aForm['inputs'];
+
+        $oForm = new BxTemplStudioFormView($aForm);
+        return $oForm->getCode();
+    }
+
+    protected function getSettingsCoverFields($aPage = array(), $bCreate = true)
+    {
+    	return $this->getSettingsCover($aPage, $bCreate, true);
+    }
+
+    protected function getSettingsLayout($aPage = array(), $bCreate = true, $bInputsOnly = false)
+    {
+        $sJsObject = $this->getPageJsObject();
+        $oTemplate = BxDolStudioTemplate::getInstance();
+
+        $iLayout = isset($aPage['layout_id']) ? (int)$aPage['layout_id'] : 5;
+
+        $aLayouts = array();
+        $this->oDb->getLayouts(array('type' => 'all'), $aLayouts, false);
+
+        $aTmplParams = array(
+            'bx_repeat:layouts' => array(),
+            'form' => ''
+        );
+
+        foreach($aLayouts as $aLayout)
+            $aTmplParams['bx_repeat:layouts'][] = array(
+                'id' => $aLayout['id'],
+                'html_id' => $this->aHtmlIds['layout_id'] . $aLayout['id'],
+                'js_object' => $sJsObject,
+                'bx_if:active' => array(
+                    'condition' => (int)$aLayout['id'] == $iLayout,
+                    'content' => array()
+                ),
+                'icon' => $oTemplate->getImageUrl($aLayout['icon']),
+                'title' => _t($aLayout['title']),
+            );
+
+        $aForm = array(
+            'form_attrs' => array(
+                'id' => 'adm-bp-settings-layout',
+            ),
+            'params' => array (
+                'remove_form' => '1',
+                'csrf' => array(
+                    'disable' => true
+                )
+            ),
+            'inputs' => array (
+                'layout_id' => array(
+                    'type' => 'hidden',
+                    'name' => 'layout_id',
+                    'value' => $iLayout,
+                    'db' => array (
+                        'pass' => 'Int',
+                    ),
+                )
+            )
+        );
+
+        if($bInputsOnly)
+            return $aForm['inputs'];
+
+        $oForm = new BxTemplStudioFormView($aForm);
+        $aTmplParams['form'] = $oForm->getCode();
+
+        return $oTemplate->parseHtmlByName('bp_layouts.html', $aTmplParams);
+    }
+
+    protected function getSettingsLayoutFields($aPage = array(), $bCreate = true)
+    {
+    	return $this->getSettingsLayout($aPage, $bCreate, true);
+    }
+
+    protected function getSettingsVisibility($aPage = array(), $bCreate = true, $bInputsOnly = false)
+    {
+    	$iVisibleForLevels = isset($aPage['visible_for_levels']) ? (int)$aPage['visible_for_levels'] : BX_DOL_INT_MAX;
+
+        if(isset($aPage['visible_for_levels_editable']) && (int)$aPage['visible_for_levels_editable'] == 0)
+            $aInputs = array(
+                'visible_for_levels' => array(
+                    'type' => 'custom',
+                    'name' => 'visible_for_levels',
+                    'content' => MsgBox(_t('_adm_bp_err_page_visible_for_levels'))
+                )
+            );
+        else {
+        	bx_import('BxDolStudioUtils');
+            $aInputs = array(
+                'visible_for' => array(
+                    'type' => 'select',
+                    'name' => 'visible_for',
+                    'caption' => _t('_adm_bp_txt_block_visible_for'),
+                    'info' => '',
+                    'value' => $iVisibleForLevels == BX_DOL_INT_MAX ? BX_DOL_STUDIO_VISIBLE_ALL : BX_DOL_STUDIO_VISIBLE_SELECTED,
+                    'values' => array(
+                        array('key' => BX_DOL_STUDIO_VISIBLE_ALL, 'value' => _t('_adm_bp_txt_block_visible_for_all')),
+                        array('key' => BX_DOL_STUDIO_VISIBLE_SELECTED, 'value' => _t('_adm_bp_txt_block_visible_for_selected')),
+                    ),
+                    'required' => '0',
+                    'attrs' => array(
+                        'onchange' => $this->getPageJsObject() . '.onChangeVisibleFor(this)'
+                    ),
+                    'db' => array (
+                        'pass' => 'Xss',
+                    )
+                ),
+                'visible_for_levels' => array(
+                    'type' => 'checkbox_set',
+                    'name' => 'visible_for_levels',
+                    'caption' => _t('_adm_bp_txt_page_visible_for_levels'),
+                    'info' => _t('_adm_bp_dsc_page_visible_for_levels'),
+                    'value' => array(),
+                    'values' => array(),
+                    'tr_attrs' => array(
+                        'style' => $iVisibleForLevels == BX_DOL_INT_MAX ? 'display:none' : ''
+                    ),
+                    'db' => array (
+                        'pass' => 'Int',
+                    )
+                )
+            );
+        }
+
+        $aForm = array(
+            'form_attrs' => array(
+                'id' => 'adm-bp-settings-visibility',
+            ),
+            'params' => array (
+                'remove_form' => '1',
+                'csrf' => array(
+                    'disable' => true
+                )
+            ),
+            'inputs' => $aInputs
+        );
+
+        BxDolStudioUtils::getVisibilityValues($iVisibleForLevels, $aForm['inputs']['visible_for_levels']['values'], $aForm['inputs']['visible_for_levels']['value']);
+
+        if($bInputsOnly)
+            return $aForm['inputs'];       	
+
+        $oForm = new BxTemplStudioFormView($aForm);
+        return $oForm->getCode();
+    }
+
+    protected function getSettingsVisibilityFields($aPage = array(), $bCreate = true)
+    {
+        return $this->getSettingsVisibility($aPage, $bCreate, true);
+    }
+
+    protected function getSettingsCache($aPage = array(), $bCreate = true, $bInputsOnly = false)
+    {
+        $aForm = array(
+            'form_attrs' => array(
+                'id' => 'adm-bp-settings-cache',
+            ),
+            'params' => array (
+                'remove_form' => '1',
+                'csrf' => array(
+                    'disable' => true
+                )
+            ),
+            'inputs' => array (
+                'cache_lifetime' => array(
+                    'type' => 'text',
+                    'name' => 'cache_lifetime',
+                    'caption' => _t('_adm_bp_txt_page_cache_lifetime'),
+                    'info' => _t('_adm_bp_dsc_page_cache_lifetime'),
+                    'value' => isset($aPage['cache_lifetime']) ? $aPage['cache_lifetime'] : 0,
+                    'required' => '',
+                    'attrs' => array(
+                        'disabled' => isset($aPage['cache_editable']) && (int)$aPage['cache_editable'] == 0 ? 'disabled' : ''
+                    ),
+                    'db' => array (
+                        'pass' => 'Int',
+                    )
+                ),
+            )
+        );
+
+        if($bInputsOnly)
+            return $aForm['inputs'];
+
+        $oForm = new BxTemplStudioFormView($aForm);
+        return $oForm->getCode();
+    }
+
+    protected function getSettingsCacheFields($aPage = array(), $bCreate = true)
+    {
+    	return $this->getSettingsCache($aPage, $bCreate, true);
+    }
+
+    protected function getSettingsSeo($aPage = array(), $bCreate = true, $bInputsOnly = false)
+    {
+        $aForm = array(
+            'form_attrs' => array(
+                'id' => 'adm-bp-settings-seo',
+            ),
+            'params' => array (
+                'remove_form' => '1',
+                'csrf' => array(
+                    'disable' => true
+                )
+            ),
+            'inputs' => array (
+                'meta_title' => array(
+                    'type' => 'text',
+                    'name' => 'meta_title',
+                    'caption' => _t('_adm_bp_txt_page_meta_title'),
+                    'value' => isset($aPage['meta_title']) ? $aPage['meta_title'] : '',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+                'meta_description' => array(
+                    'type' => 'textarea',
+                    'name' => 'meta_description',
+                    'caption' => _t('_adm_bp_txt_page_meta_description'),
+                    'info' => _t('_adm_bp_dsc_page_meta_description'),
+                    'value' => isset($aPage['meta_description']) ? $aPage['meta_description'] : '',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+                'meta_keywords' => array(
+                    'type' => 'textarea',
+                    'name' => 'meta_keywords',
+                    'caption' => _t('_adm_bp_txt_page_meta_keywords'),
+                    'info' => _t('_adm_bp_dsc_page_meta_keywords'),
+                    'value' => isset($aPage['meta_keywords']) ? $aPage['meta_keywords'] : '',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+                'meta_robots' => array(
+                    'type' => 'text',
+                    'name' => 'meta_robots',
+                    'caption' => _t('_adm_bp_txt_page_meta_robots'),
+                    'info' => _t('_adm_bp_dsc_page_meta_robots'),
+                    'value' => isset($aPage['meta_robots']) ? $aPage['meta_robots'] : '',
+                    'required' => '0',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    )
+                )
+            )
+        );
+
+        if($bInputsOnly)
+            return $aForm['inputs'];
+
+        $oForm = new BxTemplStudioFormView($aForm);
+        return $oForm->getCode();
+    }
+
+    protected function getSettingsSeoFields($aPage = array(), $bCreate = true)
+    {
+    	return $this->getSettingsSeo($aPage, $bCreate, true);
+    }
+
+    protected function getSettingsInjections($aPage = array(), $bCreate = true, $bInputsOnly = false)
+    {
+        $aForm = array(
+            'form_attrs' => array(
+                'id' => 'adm-bp-injections-seo',
+            ),
+            'params' => array (
+                'remove_form' => '1',
+                'csrf' => array(
+                    'disable' => true
+                )
+            ),
+            'inputs' => array (
+                'inj_head' => array(
+                    'type' => 'textarea',
+                    'name' => 'inj_head',
+                    'caption' => _t('_adm_bp_txt_page_inj_head'),
+                    'info' => _t('_adm_bp_dsc_page_inj_head'),
+                    'value' => isset($aPage['inj_head']) ? $aPage['inj_head'] : '',
+                    'code' => 1,
+                    'db' => array (
+                        'pass' => 'XssHtml',
+                    ),
+                ),
+                'inj_footer' => array(
+                    'type' => 'textarea',
+                    'name' => 'inj_footer',
+                    'caption' => _t('_adm_bp_txt_page_inj_footer'),
+                    'info' => _t('_adm_bp_dsc_page_inj_footer'),
+                    'value' => isset($aPage['inj_footer']) ? $aPage['inj_footer'] : '',
+                    'code' => 1,
+                    'db' => array (
+                        'pass' => 'XssHtml',
+                    ),
+                )
+            )
+        );
+
+        if($bInputsOnly)
+            return $aForm['inputs'];
+
+        $oForm = new BxTemplStudioFormView($aForm);
+        return $oForm->getCode();
+    }
+
+    protected function getSettingsInjectionsFields($aPage = array(), $bCreate = true)
+    {
+    	return $this->getSettingsInjections($aPage, $bCreate, true);
+    }
+
+    protected function getBlockIcon($aBlock)
+    {
+        $sIcon = $sIconUrl = "";
+
+        $sResult = '';
+        switch($aBlock['type']) {
+            case BX_DOL_STUDIO_BP_BLOCK_RAW:
+            	$sResult = 'far file-alt';
+            	break;
+
+            case BX_DOL_STUDIO_BP_BLOCK_HTML:
+            	$sResult = 'code';
+            	break;
+            
+            case BX_DOL_STUDIO_BP_BLOCK_CREATIVE:
+            	$sResult = 'object-group';
+            	break;
+            
+            case BX_DOL_STUDIO_BP_BLOCK_BENTO_GRID:
+                $sResult = 'fa-grip-horizontal';
+                break;
+
+            case BX_DOL_STUDIO_BP_BLOCK_RSS:
+            	$sResult = 'rss';
+            	break;
+
+            case BX_DOL_STUDIO_BP_BLOCK_IMAGE:
+            	$sResult = 'far image';
+            	break;
+
+            case BX_DOL_STUDIO_BP_BLOCK_LANG:
+            	$sResult = 'globe';
+            	break;
+
+            case BX_DOL_STUDIO_BP_BLOCK_MENU:
+            	$sResult = 'list-alt';
+            	break;
+
+            case BX_DOL_STUDIO_BP_BLOCK_WIKI:
+            	$sResult = 'file-word';
+            	break;
+
+            case BX_DOL_STUDIO_BP_BLOCK_CUSTOM:
+                $sResult = 'file';
+            	break;
+
+            case BX_DOL_STUDIO_BP_BLOCK_SERVICE:
+                $sResult = $this->getModuleIcon($aBlock['module'], 'page');
+                break;
+        }
+
+        if(strpos($sResult, '.') === false)
+            $sIcon = $sResult;
+        else
+            $sIconUrl = $sResult;
+
+        return array($sIcon, $sIconUrl);
+    }
+
+    protected function getBlockModule($aBlock)
+    {
+        return  $aBlock['module'] != BX_DOL_STUDIO_BP_SKELETONS ? $aBlock['module'] : BX_DOL_STUDIO_MODULE_CUSTOM;
+    }
+
+    protected function getBlockContent($aBlock)
+    {
+        $aFields = array();
+
+        switch($aBlock['type']) {
+            case BX_DOL_STUDIO_BP_BLOCK_CUSTOM:
+            case BX_DOL_STUDIO_BP_BLOCK_RAW:
+            case BX_DOL_STUDIO_BP_BLOCK_BENTO_GRID:
+                $aFields = [
+                    'content' => [
+                        'type' => 'textarea',
+                        'name' => 'content',
+                        'caption' => _t('_adm_bp_txt_block_content_raw'),
+                        'info' => _t('_adm_bp_dsc_block_content_raw'),
+                        'value' => $aBlock['content'],
+                        'required' => '0',
+                        'code' => 1,
+                        'attrs' => [
+                            'class' => 'bx-form-input-textarea-codemirror'
+                        ],
+                        'db' => [
+                            'pass' => 'XssHtml',
+                        ],
+                    ],
+                ];
+                break;
+
+            case BX_DOL_STUDIO_BP_BLOCK_HTML:
+                $aFields = [
+                    'content' => [
+                        'type' => 'textarea',
+                        'name' => 'content',
+                        'caption' => _t('_adm_bp_txt_block_content_raw'),
+                        'info' => _t('_adm_bp_dsc_block_content_raw'),
+                        'value' => $aBlock['content'],
+                        'required' => '0',
+                        'html' => 2,
+                        'attrs' => [
+                            'id' => $this->aHtmlIds['edit_block_editor_id']
+                        ],
+                        'db' => [
+                            'pass' => 'XssHtml',
+                        ],
+                    ],
+                    'attachments' => [
+                    	'type' => 'files',
+                        'name' => 'attachments',
+                        'storage_object' => $this->sStorage,
+                        'images_transcoder' => $this->sTranscoder,
+                        'uploaders' => $this->aUploaders,
+                        'multiple' => true,
+                        'content_id' => $aBlock['id'],
+                        'ghost_template' => BxDolStudioTemplate::getInstance()->parseHtmlByName('bp_fgt_attachments.html', [
+                            'js_object' => $this->getPageJsObject(),
+                            'name' => 'attachments',
+                            'editor_id' => $this->aHtmlIds['edit_block_editor_id'],
+                    	]),
+                        'caption' => _t('_adm_bp_txt_block_content_attachments_html')
+                    ]
+                ];
+                break;
+            
+            case BX_DOL_STUDIO_BP_BLOCK_CREATIVE:
+                $oTemplate = BxDolStudioTemplate::getInstance();
+
+                $sContent = $oTemplate->parseHtmlByName('bp_block_skeleton_creative.html', [
+                    'html_id' => $this->aHtmlIds['edit_block_builder_id'],
+                    'content_html' => $aBlock['content'],
+                    'content_data' => json_encode([
+                        'pages' => [
+                            ['component' => $aBlock['content']]
+                        ]
+                    ])
+                ]);
+
+                $aFields = [
+                    'content' => [
+                        'type' => 'custom',
+                        'name' => 'content',
+                        'caption' => _t('_adm_bp_txt_block_content_raw'),
+                        'content' => $sContent,
+                        'required' => '0',
+                        'db' => [
+                            'pass' => 'XssHtml',
+                        ],
+                    ],
+                ];
+                break;
+
+            case BX_DOL_STUDIO_BP_BLOCK_LANG:
+                $aFields = [
+                    'content' => [
+                        'type' => 'textarea_translatable',
+                        'name' => 'content',
+                        'caption' => _t('_adm_bp_txt_block_content_lang'),
+                        'info' => _t('_adm_bp_dsc_block_content_lang'),
+                        'value' => $aBlock['content'],
+                        'required' => '0',
+                        'html' => 2,
+                        'db' => [
+                            'pass' => 'XssHtml',
+                        ],
+                    ],
+                ];
+                break;
+
+            case BX_DOL_STUDIO_BP_BLOCK_IMAGE:
+                $iImageId = $sImageAlign = '';
+                if($aBlock['content'] != '')
+                    list($iImageId, $sImageAlign) = explode($this->sParamsDivider, $aBlock['content']);
+
+                $aFields = [
+                    'content' => [
+                        'type' => 'hidden',
+                        'name' => 'content',
+                        'value' => '',
+                        'db' => [
+                            'pass' => 'Xss',
+                        ],
+                    ],
+                    'image_file' => [
+                        'type' => 'files',
+                        'name' => 'image_file',
+                        'storage_object' => BX_DOL_STORAGE_OBJ_IMAGES,
+                        'images_transcoder' => 'sys_image_resize',
+                        'uploaders' => ['sys_html5'],
+                        'multiple' => false,
+                        'content_id' => $aBlock['id'],
+                        'ghost_template' => BxTemplStudioFunctions::getInstance()->getDefaultGhostTemplate('image_file'),
+                        'caption' => _t('_adm_bp_txt_block_content_image_file'),
+                    ],
+                    'image_align' => [
+                        'type' => 'select',
+                        'name' => 'image_align',
+                        'caption' => _t('_adm_bp_txt_block_content_image_align'),
+                        'info' => '',
+                        'value' => $sImageAlign,
+                        'values' => [
+                            ['key' => '', 'value' => _t('_adm_bp_txt_block_content_image_align_empty')],
+                            ['key' => 'left', 'value' => _t('_adm_bp_txt_block_content_image_align_left')],
+                            ['key' => 'center', 'value' => _t('_adm_bp_txt_block_content_image_align_center')],
+                            ['key' => 'right', 'value' => _t('_adm_bp_txt_block_content_image_align_right')],
+                        ],
+                        'required' => '0',
+                        'db' => [
+                            'pass' => 'Xss',
+                        ],
+                    ],
+                ];
+                break;
+
+            case BX_DOL_STUDIO_BP_BLOCK_RSS:
+                $sRssUrl = $sRssLength = '';
+                if($aBlock['content'] != '')
+                    list($sRssUrl, $sRssLength) = explode($this->sParamsDivider, $aBlock['content']);
+
+                $aFields = [
+                    'content' => [
+                        'type' => 'hidden',
+                        'name' => 'content',
+                        'value' => '',
+                        'db' => [
+                            'pass' => 'Xss',
+                        ],
+                    ],
+                    'rss_url' => [
+                        'type' => 'text',
+                        'name' => 'rss_url',
+                        'caption' => _t('_adm_bp_txt_block_content_rss_url'),
+                        'info' => _t('_adm_bp_dsc_block_content_rss_url'),
+                        'value' => $sRssUrl,
+                        'required' => '0',
+                        'db' => [
+                            'pass' => 'Xss',
+                        ],
+                    ],
+                    'rss_length' => [
+                        'type' => 'text',
+                        'name' => 'rss_length',
+                        'caption' => _t('_adm_bp_txt_block_content_rss_length'),
+                        'info' => _t('_adm_bp_dsc_block_content_rss_length'),
+                        'value' => $sRssLength,
+                        'required' => '0',
+                        'db' => [
+                            'pass' => 'Int',
+                        ],
+                    ],
+                ];
+                break;
+
+            case BX_DOL_STUDIO_BP_BLOCK_MENU:
+                $aFields = [
+                    'content' => [
+                        'type' => 'select',
+                        'name' => 'content',
+                        'caption' => _t('_adm_bp_txt_block_content_menu'),
+                        'info' => '',
+                        'value' => $aBlock['content'],
+                        'values' => [],
+                        'required' => '0',
+                        'db' => [
+                            'pass' => 'Xss',
+                        ],
+                    ]
+                ];
+
+                $sGroup = '';
+                $aMenus = $this->oDb->getMenus(true);
+                foreach($aMenus as $sKey => $aMenu) {
+                    if($sGroup != $aMenu['module']) {
+                        if(!empty($sGroup))
+                            $aFields['content']['values'][$sGroup . '_end'] = ['type' => 'group_end'];
+
+                        $sGroup = $aMenu['module'];
+                        $aFields['content']['values'][$sGroup . '_beg'] = ['type' => 'group_header', 'value' => BxDolStudioUtils::getModuleTitle($sGroup)];
+                    }
+
+                    $aFields['content']['values'][$sKey] = _t($aMenu['title']);
+                }
+
+                $aFields['content']['values'] = array_merge(['' => _t('_adm_bp_txt_block_content_menu_empty')], $aFields['content']['values']);
+                break;
+
+            case BX_DOL_STUDIO_BP_BLOCK_SERVICE:
+                $aService = ['module' => '', 'method' => ''];
+                if($aBlock['content'] != '')
+                    $aService = unserialize($aBlock['content']);
+
+                $aFields = [
+                    'service_module' => [
+                        'type' => 'value',
+                        'name' => 'service_module',
+                        'caption' => _t('_adm_bp_txt_block_content_service_module'),
+                        'value' => $this->getModuleTitle($aService['module'])
+                    ],
+                    'service_method' => [
+                        'type' => 'value',
+                        'name' => 'service_method',
+                        'caption' => _t('_adm_bp_txt_block_content_service_method'),
+                        'value' => $aService['method']
+                    ]
+                ];
+                break;
+        }
+
+        return $aFields;
+    }
+
+    protected function getBlockList($sModule)
+    {
+        if(empty($sModule))
+            return '';
+
+        $sJsObject = $this->getPageJsObject();
+        $oTemplate = BxDolStudioTemplate::getInstance();
+
+        $oForm = new BxTemplStudioFormView(array());
+
+        $aInputCheckbox = [
+            'type' => 'checkbox',
+            'name' => 'blocks[]',
+            'attrs' => [
+                'onChange' => 'javascript:' . $sJsObject . '.onSelectBlock(this);'
+            ],
+            'value' => ''
+        ];
+
+        $aTmplParams = [
+            'html_block_list_id' => $this->aHtmlIds['block_list_id'] . $sModule,
+            'bx_repeat:blocks' => []
+        ];
+
+        $aBlocks = $this->getBlocks($sModule);
+        foreach($aBlocks as $aBlock) {
+        	list($sIcon, $sIconUrl) = $this->getBlockIcon($aBlock);
+
+            $aInputCheckbox['value'] = $aBlock['id'];
+
+            $aTmplParams['bx_repeat:blocks'][] = [
+                'js_object' => $sJsObject,
+                'html_id' => $this->aHtmlIds['block_id'] . $aBlock['id'],
+                'bx_if:is_inactive' => array(
+                    'condition' => false,
+                    'content' => [true]
+                ),
+                'bx_if:show_link' => [
+                    'condition' => false,
+                    'content' => [true]
+                ],
+                'bx_if:show_text' => [
+                    'condition' => true,
+                    'content' => [
+                        'title' => _t(!empty($aBlock['title_system']) ? $aBlock['title_system'] : $aBlock['title']),
+                    ]
+                ],
+                'bx_if:show_api' => [
+                    'condition' => false,
+                    'content' => [true]
+                ],
+                'bx_if:image' => [
+                    'condition' => (bool)$sIconUrl,
+                    'content' => array('icon_url' => $sIconUrl),
+                ],
+                'bx_if:icon' => [
+                    'condition' => (bool)$sIcon,
+                    'content' => ['icon' => $sIcon],
+                ],
+                'module' => $this->getModuleTitle($aBlock['module']),
+                'visible_for' => _t('_adm_bp_txt_visible_for', BxDolStudioUtils::getVisibilityTitle($aBlock['visible_for_levels'])),
+                'bx_if:show_checkbox' => [
+                    'condition' => true,
+                    'content' => [
+                        'content' => $oForm->genRow($aInputCheckbox)
+                    ]
+                ],
+                'bx_if:show_drag_handle' => [
+                    'condition' => false,
+                    'content' => []
+                ]
+            ];
+        }
+
+        return $oTemplate->parseHtmlByName('bp_blocks_list.html', $aTmplParams);
+    }
+
+    protected function onBlockDelete($aBlock)
+    {
+    	if($aBlock['module'] != BX_DOL_STUDIO_MODULE_CUSTOM)
+    		return;
+
+        BxDolStudioLanguagesUtils::getInstance()->deleteLanguageString($aBlock['title']);
+
+        //--- Process Lang block
+        if($aBlock['type'] == BX_DOL_STUDIO_BP_BLOCK_LANG && $aBlock['content'] != '')
+            BxDolStudioLanguagesUtils::getInstance()->deleteLanguageString($aBlock['content']);
+
+        //--- Process Image block
+        if($aBlock['type'] == BX_DOL_STUDIO_BP_BLOCK_IMAGE && $aBlock['content'] != '') {
+            $iImageId = $sImageAlign = '';
+            list($iImageId, $sImageAlign) = explode($this->sParamsDivider, $aBlock['content']);
+
+            if(is_numeric($iImageId) && (int)$iImageId != 0)
+                BxDolStorage::getObjectInstance(BX_DOL_STORAGE_OBJ_IMAGES)->deleteFile((int)$iImageId, 0);
+        }
+    }
+
+    protected function getBlocks($sModule)
+    {
+        $aBlocks = array();
+        $this->oDb->getBlocks(array('type' => 'by_module_to_copy', 'value' => $sModule), $aBlocks, false);
+
+        return $aBlocks;
+    }
+
+    protected function _getTmplVarsBlockPanelTop()
+    {
+        $sJsObject = $this->getPageJsObject();
+
+        $oForm = new BxTemplStudioFormView(array());
+
+        $aPages = $this->oDb->getPages(array('type' => 'by_module', 'value' => $this->sType));
+
+        $aCounter = array();
+        $this->oDb->getBlocks(array('type' => 'counter_by_pages'), $aCounter, false);
+        
+        $aInputPagesValues = [];
+        foreach($aPages as $aPage) {
+            $sTitle = _t($aPage['title_system']);
+            if(empty($sTitle))
+                $sTitle = _t($aPage['title']);
+
+            $aInputPagesValues[] = array(
+                'key' => $aPage['object'], 
+                'value' => $sTitle . " (" . (isset($aCounter[$aPage['object']]) ? $aCounter[$aPage['object']] : "0") . ")"
+            );
+        }
+
+        usort($aInputPagesValues, function($aV1, $aV2) {
+            return strcmp($aV1['value'], $aV2['value']);
+        });
+
+        $aInputPages = array(
+            'type' => 'select',
+            'name' => 'page',
+            'attrs' => array(
+                'onChange' => 'javascript:' . $this->getPageJsObject() . '.onChangePage(this)'
+            ),
+            'value' => $this->sPage,
+            'values' => array_merge([[
+                'key' => '', 
+                'value' => _t('_adm_bp_txt_select_page')]
+            ], $aInputPagesValues)
+        );       
+
+        $aTmplVarsActions = array();
+        if(($this->sPage != '' && !empty($this->aPageRebuild)) !== false)
+            $aTmplVarsActions = $this->_getTmplVarsBlockPanelTopActions();
+
+        return array(
+            'js_object' => $this->getPageJsObject(),
+            'selector' => $oForm->genRow($aInputPages),
+            'action_page_create' => $this->sActionPageCreate,
+            'bx_if:show_actions' => array(
+                'condition' => $this->sPage != '',
+                'content' => $aTmplVarsActions
+            )
+        );
+    }
+
+    protected function _getTmplVarsBlockPanelTopActions()
+    {
+        $sJsObject = $this->getPageJsObject();
+
+        $oPermalinks = BxDolPermalinks::getInstance();
+
+        return [
+            'js_object' => $sJsObject,
+            'bx_if:can_view' => [
+                'condition' => !empty($this->aPageRebuild['url']),
+                'content' => [
+                    'url_view' => bx_absolute_url($oPermalinks->permalink($this->aPageRebuild['url'])),
+                ]
+            ],
+            'action_page_edit' => $this->sActionPageEdit,
+            'bx_if:can_delete' => [
+                'condition' => (int)$this->aPageRebuild['deletable'] == 1,
+                'content' => [
+                    'js_object' => $sJsObject
+                ]
+            ],
+            'action_block_create' => $this->sActionBlockCreate,
+        ];
+    }
+
+    protected function _getTmplVarsPageSettings($aPage = array(), $bCreate = true)
+    {
+    	$sJsObject = $this->getPageJsObject();
+
+        $aTmplParams = array(
+            'js_object' => $sJsObject,
+            'menu' => array(),
+            'html_settings_groups_id' => $this->aHtmlIds['settings_groups_id'],
+            'bx_repeat:settings_groups' => array(),
+        	'submit' => _t($bCreate ? '_adm_bp_btn_page_create' : '_adm_bp_btn_page_apply')
+        );
+        foreach($this->aPageSettings as $aSetting) {
+            //--- get menu items
+            $aTmplParams['menu'][$aSetting['name']] = array(
+                'name' => $aSetting['name'],
+                'icon' => '',
+                'onclick' => $sJsObject . '.onChangeSettingGroup(\'' . $aSetting['name'] . '\', this);',
+                'title' => $aSetting['title'],
+                'selected' => isset($aSetting['active']) && (int)$aSetting['active'] == 1
+            );
+
+            //--- get settings
+            $aTmplParams['bx_repeat:settings_groups'][] = array(
+                'html_settings_group_id' => $this->aHtmlIds['settings_group_id'] .  $aSetting['name'],
+                'bx_if:hidden' => array(
+                    'condition' => $aSetting['active'] != 1,
+                    'content' => array()
+                ),
+                'content' => $this->{'getSettings' . $this->getClassName($aSetting['name'])}($aPage, $bCreate)
+            );
+        }
+
+        $oMenu = new BxTemplStudioMenu(array('template' => 'menu_side.html', 'menu_items' => $aTmplParams['menu']));
+        $aTmplParams['menu'] = $oMenu->getCode();
+
+        return $aTmplParams;
+    }
+
+    protected function getIndexingDataForBlock($sType, $sContent)
+    {
+        $aBlock = array();   
+        switch($sType) {
+            case BX_DOL_STUDIO_BP_BLOCK_HTML:
+            case BX_DOL_STUDIO_BP_BLOCK_RAW:
+                $aBlock['text'] = trim(strip_tags($sContent));
+                $aBlock['text_updated'] = time();
+                break;
+        }
+        return $aBlock;
+    }
+}
+
+/** @} */

+ 2287 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/scripts/BxBaseStudioFormsField.php

@@ -0,0 +1,2287 @@
+<?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 BxBaseStudioFormsField extends BxDolStudioFormsField
+{
+    protected $aForm;
+    protected $sTypeTitlePrefix = '_adm_form_txt_field_type_';
+
+    function __construct($aParams = array(), $aField = array())
+    {
+        parent::__construct($aParams, $aField);
+    }
+
+	public function init()
+	{
+		parent::init();
+
+		$sJsObject = $this->getJsObject();
+
+        bx_import('BxTemplStudioFormView');
+
+        $this->aForm = array(
+            'form_attrs' => array(
+                'id' => '',
+                'action' => '',
+                'method' => BX_DOL_STUDIO_METHOD_DEFAULT
+            ),
+            'params' => array (
+                'db' => array(
+                    'table' => 'sys_form_inputs',
+                    'key' => 'id',
+                    'uri' => '',
+                    'uri_title' => '',
+                    'submit_name' => 'do_submit'
+                ),
+            ),
+            'inputs' => array (
+                'type' => array(
+                    'type' => 'custom',
+                    'name' => 'type',
+                    'content' => '',
+                    'attrs' => array(
+                        'id' => 'bx-form-field-type',
+                        'class' => 'bx-form-field-type',
+                        'onchange' => $sJsObject . '.onSelectType()'
+                    ),
+                ),
+                'reset' => array (
+                    'type' => 'reset',
+                    'name' => 'close',
+                    'value' => _t('_adm_form_btn_field_cancel'),
+                    'attrs' => array(
+                        'onclick' => "$('.bx-popup-applied:visible').dolPopupHide()",
+                        'class' => 'bx-def-margin-sec-left',
+                    ),
+                )
+            )
+        );
+
+        $aTypes = array();
+        foreach($this->aTypes as $sType => $aParams)
+        	if(isset($aParams['add']) && (int)$aParams['add'] == 1)
+            	$aTypes[$sType] = _t($this->sTypeTitlePrefix . $sType);
+
+        asort($aTypes);
+
+        $aMenu = array();
+        foreach($aTypes as $sName => $sTitle)
+            $aMenu[] = array(
+                'name' => $sName,
+                'icon' => 'ui-' . $sName . '.png',
+                'onclick' => $sJsObject . ".onSelectType('" . $sName . "', this)",
+                'title' => $sTitle
+            );
+
+        $oMenu = new BxTemplStudioMenu(array('template' => 'menu_vertical.html', 'menu_items' => $aMenu));
+        $this->aForm['inputs']['type']['content'] = $oMenu->getCode();
+	}
+
+    public function getJsObject()
+    {
+        return BX_DOL_STUDIO_FORMS_FIELDS_JS_OBJECT;
+    }
+
+    public function getCheckFunctions()
+    {
+        return $this->aCheckFunctions;
+    }
+
+    public function getCode($sAction, $sObject)
+    {
+        $sFunction = 'getCode' . $this->getClassName($sAction);
+        if(method_exists($this, $sFunction))
+            return $this->$sFunction($sAction, $sObject);
+
+        return false;
+    }
+
+    protected function getCodeAdd($sAction, $sObject)
+    {
+        $aForm = $this->getFormAdd($sAction, $sObject);
+        $oForm = new BxTemplStudioFormView($aForm);
+
+        if($oForm->isSubmitted())
+            $this->onCheckField('add', $oForm);
+
+        $oForm->initChecker();
+        if($oForm->isSubmittedAndValid()) {
+            //--- Process field name.
+            $sInputName = $oForm->getCleanValue('name');
+            if(empty($sInputName)) {
+                $sInputObject = $oForm->getCleanValue('object');
+
+                $sLanguage = BxDolStudioLanguagesUtils::getInstance()->getCurrentLangName(false);
+                $sInputCaption = BxDolForm::getSubmittedValue('caption-' . $sLanguage, $aForm['form_attrs']['method']);
+                if(empty($sInputCaption))
+                    $sInputCaption = BxDolForm::getSubmittedValue('caption_system-' . $sLanguage, $aForm['form_attrs']['method']);
+
+                $sInputName = $this->getFieldName($sInputObject, $sInputCaption);
+                BxDolForm::setSubmittedValue('name', $sInputName, $oForm->aFormAttrs['method']);
+            }
+
+            $sFieldName = strmaxtextlen($sInputName, $this->iFieldNameMaxLen, '');
+            if(strcmp($sInputName, $sFieldName) !== 0) {
+                if($this->oDb->isInput($sInputObject, $sFieldName))
+                    return array('msg' => _t('_adm_form_err_field_add_already_exists'));
+
+            	BxDolForm::setSubmittedValue('name', $sFieldName, $oForm->aFormAttrs['method']);                
+            }
+
+            if($this->isField($sFieldName))
+                return array('msg' => _t('_adm_form_err_field_add_already_exists'));
+
+            $this->onSubmitField($oForm);
+            if(($iId = $oForm->insert()) === false)
+                return false;
+
+            if ($oForm->getCleanValue('rateable') != '')
+                $this->checkRateableFiledValue($sInputName, $oForm->getCleanValue('module'), $oForm->getCleanValue('object'), $oForm->getCleanValue('value'));
+            
+            $this->alterAdd($sFieldName);
+            return true;
+        } 
+        else
+            return BxTemplStudioFunctions::getInstance()->popupBox('adm-form-field-add-' . $this->aParams['display'] . '-popup', _t('_adm_form_txt_field_add_popup'), BxDolStudioTemplate::getInstance()->parseHtmlByName('form_add_field.html', array(
+                'form_id' => $aForm['form_attrs']['id'],
+                'form' => $oForm->getCode(true),
+                'object' => $sObject,
+                'action' => $sAction
+            )));
+    }
+
+    protected function getCodeEdit($sAction, $sObject)
+    {
+        $aForm = $this->getFormEdit($sAction, $sObject);
+        $oForm = new BxTemplStudioFormView($aForm);
+
+        $bAlter = false;
+        if($oForm->isSubmitted())
+            $bAlter = $this->onCheckField('edit', $oForm);
+
+        $oForm->initChecker();
+        if($oForm->isSubmittedAndValid()) {
+            $sInputName = $oForm->getCleanValue('name');
+
+            $sFieldName = strmaxtextlen($sInputName, $this->iFieldNameMaxLen, '');
+            if(strcmp($sInputName, $sFieldName) !== 0)
+            	BxDolForm::setSubmittedValue('name', $sFieldName, $oForm->aFormAttrs['method']);
+
+            $this->onSubmitField($oForm);
+            if($oForm->update((int)$this->aField['id']) === false)
+                return false;
+            
+            if ($oForm->getCleanValue('rateable') != '')
+                $this->checkRateableFiledValue($sInputName, $oForm->getCleanValue('module'), $oForm->getCleanValue('object'), $oForm->getCleanValue('value'));
+
+            $sFieldNameOld = $this->aField['name'];
+            if($bAlter || strcmp($sFieldNameOld, $sFieldName) !== 0)
+                $this->alterChange($sFieldNameOld, $sFieldName);
+
+            return true;
+        } 
+        else {
+            $sCaption = _t($this->aField['caption_system']);
+            if(empty($sCaption))
+                $sCaption = _t($this->aField['caption']);
+
+            return BxTemplStudioFunctions::getInstance()->popupBox('adm-form-field-edit-' . $this->aField['type'] . '-popup', _t('_adm_form_txt_field_edit_popup', $sCaption), BxDolStudioTemplate::getInstance()->parseHtmlByName('form_add_field.html', array(
+                'form_id' => $aForm['form_attrs']['id'],
+                'form' => $oForm->getCode(true),
+                'object' => $sObject,
+                'action' => $sAction
+            )));
+        }
+    }
+
+    protected function getFormAdd($sAction, $sObject)
+    {
+        $aForm = $this->aForm;
+        $aForm['form_attrs']['id'] = 'adm-form-field-add-' . $this->aParams['display'];
+        $aForm['form_attrs']['action'] = BX_DOL_URL_ROOT . 'grid.php?o=' . $sObject . '&a=' . $sAction . '&object=' . $this->aParams['object'] . '&display=' . $this->aParams['display'];
+
+        if(isset($aForm['inputs']['object']))
+            $aForm['inputs']['object']['value'] = $this->aParams['object'];
+
+        return $aForm;
+    }
+
+    protected function getFormEdit($sAction, $sObject)
+    {
+        $aForm = $this->aForm;
+        $aForm['form_attrs']['id'] = 'adm-form-field-edit-' . $this->aParams['display'];
+        $aForm['form_attrs']['action'] = BX_DOL_URL_ROOT . 'grid.php?o=' . $sObject . '&a=' . $sAction . '&object=' . $this->aParams['object'] . '&display=' . $this->aParams['display'] . '&di_id=' . (int)$this->aField['di_id'];
+
+        foreach($aForm['inputs'] as $sKey => $aInput)
+            if(!in_array($sKey, array('type_display', 'controls')))
+                switch($sKey) {
+                    case 'value':
+                        $aForm['inputs'][$sKey]['value'] = $this->aField[$sKey];
+
+                        //--- Date and DateTime
+                        if(in_array($aForm['inputs']['type']['value'], array('datepicker', 'datetime', 'date_time'))) 
+                            $aForm['inputs'][$sKey]['db']['pass'] = $this->aField['db_pass'];
+
+                        //--- Select, Multi Select, Radio Set and Checkbox Set
+                        if(in_array($aForm['inputs']['type']['value'], array('select', 'select_multiple', 'radio_set', 'checkbox_set'))) {
+                            $sList = trim($this->getFieldValues($this->aField), BX_DATA_LISTS_KEY_PREFIX . ' ');
+
+                            $bUseForSets = false;
+                            if(in_array($aForm['inputs']['type']['value'], array('select_multiple', 'checkbox_set'))) {
+                                $aList = array();
+                                $this->oDb->getLists(array('type' => 'by_key', 'value' => $sList), $aList, false);
+
+                                $bUseForSets = (int)$aList['use_for_sets'] == 1;
+                            }
+
+                            $aPreValues = BxDolForm::getDataItems($sList, $bUseForSets);
+                            foreach($aPreValues as $mixedValue => $sTitle)
+                                 $aForm['inputs'][$sKey]['values'][] = array('key' => $mixedValue, 'value' => $sTitle);
+                        }
+
+                        //--- Files
+                        if(in_array($aForm['inputs']['type']['value'], array('files'))) {
+                        	if(!empty($aForm['inputs'][$sKey]['value']))
+                        		$aForm['inputs'][$sKey]['value'] = unserialize($aForm['inputs'][$sKey]['value']);
+
+                        	$aValues = unserialize($this->getFieldValues($this->aField));
+                        	if(!empty($aValues) && is_array($aValues)) {
+                        		$aForm['inputs'][$sKey]['values'] = array();
+
+	                        	foreach($aValues as $mixedValue => $sTitle)
+									$aForm['inputs'][$sKey]['values'][$mixedValue] = _t($sTitle);
+                        	}
+                        }
+                        break;
+
+                    case 'values':
+                        if(!empty($this->aField[$sKey]))
+                            $aForm['inputs'][$sKey]['value'] = $this->getFieldValues($this->aField);
+
+                        if(in_array($aForm['inputs']['type']['value'], array('select', 'select_multiple', 'radio_set', 'checkbox_set'))) {
+                            $aForm['inputs'][$sKey]['type'] = 'value';
+                            foreach($aForm['inputs'][$sKey]['values'] as $aValue)
+                                if($aValue['key'] == $aForm['inputs'][$sKey]['value']) {
+                                    $sLink = BX_DOL_URL_STUDIO . 'builder_forms.php?page=pre_values&list=' . trim($aValue['key'], BX_DATA_LISTS_KEY_PREFIX . ' ');
+                                    $aForm['inputs'][$sKey]['value'] = BxDolStudioTemplate::getInstance()->parseLink($sLink, $aValue['value'], array(
+                                        'title' =>  _t('_adm_form_txt_field_values_manage') 
+                                    ));
+                                    break;
+                                }
+                            unset($aForm['inputs'][$sKey]['values'], $aForm['inputs'][$sKey]['db']);
+                        }
+                        break;
+
+                    case 'checked':
+                        $aForm['inputs'][$sKey]['checked'] = (int)$this->aField[$sKey];
+                        break;
+
+                    case 'icon_preview':
+                        $aForm['inputs'][$sKey]['content'] = $this->getFieldIconPreview($this->aField['id'], $this->aField['icon']);
+                        break;
+
+                    case 'collapsed':
+                        $aForm['inputs'][$sKey]['checked'] = (int)$this->aField[$sKey];
+                        break;
+
+                    case 'required':
+                        $aForm['inputs'][$sKey]['checked'] = (int)$this->aField[$sKey];
+                        if($aForm['inputs'][$sKey]['checked'] == 1 && isset($aForm['inputs']['checker_func'])) {
+                            unset($aForm['inputs']['checker_func']['tr_attrs']['style'], $aForm['inputs']['checker_error']['tr_attrs']['style']);
+                            unset($aForm['inputs']['checker_func']['attrs']['disabled'], $aForm['inputs']['checker_error']['attrs']['disabled']);
+                        }
+                        break;
+
+                    case 'unique':
+                        $aForm['inputs'][$sKey]['checked'] = (int)$this->aField[$sKey];
+                        break;
+
+                    case 'privacy':
+                        $aForm['inputs'][$sKey]['checked'] = (int)$this->aField[$sKey];
+                        break;
+                        
+                    case 'checker_func':
+                        $sCfValue = strtolower($this->aField[$sKey]);
+                        $aForm['inputs'][$sKey]['value'] = $sCfValue;
+
+                        $bHidden = isset($aForm['inputs'][$sKey]['attrs']['disabled']) && !empty($sCfValue);
+                        if($bHidden) {
+                            unset($aForm['inputs'][$sKey]['attrs']['disabled']);
+                            unset($aForm['inputs']['checker_error']['attrs']['disabled']);
+                        }
+                        
+                        switch($sCfValue) {
+                            case 'length':
+                                if(!$bHidden)
+                                    unset($aForm['inputs']['checker_params_length_min']['tr_attrs']['style'], $aForm['inputs']['checker_params_length_max']['tr_attrs']['style']);
+                                unset($aForm['inputs']['checker_params_length_min']['attrs']['disabled'], $aForm['inputs']['checker_params_length_max']['attrs']['disabled']);
+                                break;
+                            case 'date_range':
+                                if(!$bHidden)
+                                    unset($aForm['inputs']['checker_params_length_min']['tr_attrs']['style'], $aForm['inputs']['checker_params_length_max']['tr_attrs']['style'], $aForm['inputs']['checker_params_required']['tr_attrs']['style']);
+                                unset($aForm['inputs']['checker_params_length_min']['attrs']['disabled'], $aForm['inputs']['checker_params_length_max']['attrs']['disabled'], $aForm['inputs']['checker_params_required']['attrs']['disabled']);
+                                break;
+                            case 'preg':
+                                if(!$bHidden)
+                                    unset($aForm['inputs']['checker_params_preg']['tr_attrs']['style']);
+                                unset($aForm['inputs']['checker_params_preg']['attrs']['disabled']);
+                                break;
+                        }
+
+                        if(!empty($sCfValue) && !in_array($sCfValue, $this->aCheckFunctions)) {
+                            $aForm['inputs'][$sKey]['values'][] = array(
+                                'key' => $sCfValue, 
+                                'value' => _t('_adm_form_txt_field_checker_custom')
+                            );
+                            if(!isset($aForm['inputs'][$sKey]['attrs']))
+                                $aForm['inputs'][$sKey]['attrs'] = [];
+                            $aForm['inputs'][$sKey]['attrs']['disabled'] = 'disabled';
+                        }
+                        break;
+
+                    case 'checker_params_length_min':
+                        $aParams = unserialize($this->aField['checker_params']);
+                        $aForm['inputs'][$sKey]['value'] = isset($aParams['min']) ? (int)$aParams['min'] : 0;
+                        break;
+
+                    case 'checker_params_length_max':
+                        $aParams = unserialize($this->aField['checker_params']);
+                        $aForm['inputs'][$sKey]['value'] = isset($aParams['max']) ? (int)$aParams['max'] : 0;
+                        break;
+                    
+                    case 'checker_params_required':
+                        $aParams = unserialize($this->aField['checker_params']);
+                        $aForm['inputs'][$sKey]['checked'] = isset($aParams['required']) ? (int)$aParams['required'] : 0;
+                        break;
+
+                    case 'checker_params_preg':
+                        $aParams = unserialize($this->aField['checker_params']);
+                        $aForm['inputs'][$sKey]['value'] = isset($aParams['preg']) ? $aParams['preg'] : '';
+                        break;
+
+                    case 'attrs_min':
+                        $aParams = unserialize($this->aField['attrs']);
+                        $aForm['inputs'][$sKey]['value'] = isset($aParams['min']) ? (int)$aParams['min'] : 0;
+                        break;
+
+                    case 'attrs_max':
+                        $aParams = unserialize($this->aField['attrs']);
+                        $aForm['inputs'][$sKey]['value'] = isset($aParams['max']) ? (int)$aParams['max'] : 100;
+                        break;
+
+                    case 'attrs_step':
+                        $aParams = unserialize($this->aField['attrs']);
+                        $aForm['inputs'][$sKey]['value'] = isset($aParams['step']) ? (int)$aParams['step'] : 1;
+                        break;
+
+                    case 'attrs_src':
+                        $aParams = unserialize($this->aField['attrs']);
+                        $aForm['inputs'][$sKey]['value'] = isset($aParams['src']) ? $aParams['src'] : '';
+                        break;
+
+                    case 'editable':
+                        $aForm['inputs'][$sKey]['checked'] = (int)$this->aField[$sKey];
+                        break;
+
+                    case 'deletable':
+                        $aForm['inputs'][$sKey]['checked'] = (int)$this->aField[$sKey];
+                        break;
+
+                    default:
+                        $aForm['inputs'][$sKey]['value'] = $this->aField[$sKey];
+                }
+
+        if(array_key_exists($aForm['inputs']['type']['value'], $this->aTypesRelated)) {
+           $aForm['inputs']['type'] = $this->getFieldTypesSelector('type', $aForm['inputs']['type']['value'], true);
+           unset($aForm['inputs']['type_display']);
+        }
+
+        $aForm['inputs']['controls'][0]['value'] = _t('_adm_form_btn_field_save');
+
+        return $aForm;
+    }
+    
+    //--- If field is rateable, check presence in 'sys_form_fields_ids' table, insertation, if not.
+    protected function checkRateableFiledValue($sInputName, $sModuleName, $sFormObject, $sNestedForm)
+    {
+        $oModule = BxDolModule::getInstance($sModuleName);
+        $CNF = $oModule->_oConfig->CNF;
+        
+        $aData = bx_srv($sModuleName, 'get_all');
+        
+        foreach($aData as $aContentInfo){
+            $iContentId = $aContentInfo[$CNF['FIELD_ID']];
+            if ($sNestedForm == ''){
+                $mixedId = BxDolFormQuery::getFormField($sFormObject, $sInputName, $iContentId);
+                if (!$mixedId){
+                    BxDolFormQuery::addFormField($sFormObject, $sInputName, $iContentId, $aContentInfo[$CNF['FIELD_AUTHOR']], $sModuleName);
+                }
+            }
+            else{
+                $aNestedForm = BxDolFormQuery::getFormObject ($sNestedForm); 
+                $aNestedValues = $oModule->_oDb->getNestedBy(array('type' => 'content_id', 'id' => $iContentId, 'key_name' => $aNestedForm['key']), $aNestedForm['table']);
+                foreach($aNestedValues as $aNestedValue){
+                    $mixedId = BxDolFormQuery::getFormField($sFormObject, $sInputName, $iContentId, $aNestedValue[$aNestedForm['key']]);
+                    if (!$mixedId){
+                        BxDolFormQuery::addFormField($sFormObject, $sInputName, $iContentId, $aContentInfo[$CNF['FIELD_AUTHOR']], $sModuleName, $aNestedValue[$aNestedForm['key']]);
+                    }
+                }
+            }
+        }
+    }
+
+    protected function getFieldTypes($sRelatedTo = '')
+    {
+        $oTemplate = BxDolStudioTemplate::getInstance();
+
+        $aTypesList = $sRelatedTo != '' && isset($this->aTypesRelated[$sRelatedTo]) ? $this->aTypesRelated[$sRelatedTo]['types'] : array_keys($this->aTypes);
+
+        $aTypes = array();
+        foreach($aTypesList as $sType)
+            $aTypes[$sType] = _t($this->sTypeTitlePrefix . $sType);
+
+        asort($aTypes);
+
+        $aResult = array();
+        foreach($aTypes as $sName => $sTitle)
+            $aResult[] = array(
+                'key' => $sName,
+                'value' => $sTitle,
+                'style' => 'background-image:url(' . $oTemplate->getIconUrl('ui-' . $sName . '.png') . ')'
+            );
+
+        return $aResult;
+    }
+
+    protected function getFieldTypesSelector($sName, $sValue, $bRelated = false)
+    {
+        $bRelated = $bRelated && $sValue != '' && isset($this->aTypesRelated[$sValue]);
+
+        $aField = array(
+            'type' => 'select',
+            'name' => $sName,
+            'caption' => _t('_adm_form_txt_field_type_display'),
+            'info' => '',
+            'value' => $sValue,
+            'values' => $this->getFieldTypes($bRelated ? $sValue : ''),
+            'required' => '',
+            'attrs' => array(
+                'id' => 'bx-form-field-type',
+                'class' => 'bx-form-field-type',
+                'disabled' => 'disabled'
+            )
+        );
+
+        if($bRelated) {
+            $aField['db'] = array('pass' => 'Xss');
+            if((int)$this->aTypesRelated[$sValue]['reload_on_change'] == 1)
+                $aField['attrs']['onchange'] = $this->getJsObject() . '.onChangeType(' . $this->aField['di_id'] . ')';
+            unset($aField['attrs']['disabled']);
+        }
+
+        return $aField;
+    }
+
+    protected function getFieldName($sObject, $mixedCaption)
+    {
+        if($mixedCaption === false)
+            return $this->genFieldName($sObject);
+
+        $sName = get_mb_replace('/([^\d^\w]+)/u', '', $mixedCaption);
+        if(empty($sName))
+            return $this->genFieldName($sObject);
+
+        $sName = get_mb_replace('/([^\d^\w^\s]+)/u', '', $mixedCaption);
+        $sName = $this->getSystemName(trim($sName));
+        if($this->oDb->isInput($sObject, $sName))
+            $sName = $this->genFieldName($sObject, $sName);
+
+        return $sName;
+    }
+
+    protected function getFieldValues($aField)
+    {
+        $mixedResult = null;
+
+        switch($aField['type']) {
+            case 'select':
+            case 'select_multiple':
+            case 'checkbox_set':
+            case 'radio_set':
+                $mixedResult = $this->aField['values'];
+                break;
+
+            case 'input_set':
+                $mixedResult = $this->aField['values'];
+                break;
+
+			case 'files':
+                $mixedResult = $this->aField['values'];
+                break;
+        }
+
+        return $mixedResult;
+    }
+
+    protected function getFieldIconPreview($iId, $sIcon)
+    {
+        $oTemplate = BxDolStudioTemplate::getInstance();
+
+        $aIcons = BxTemplFunctions::getInstanceWithTemplate($oTemplate)->getIcon($sIcon);
+
+        $sIconHtml = $aIcons[2] . $aIcons[3] . $aIcons[4];
+        $bIconHtml = !empty($sIconHtml);
+
+        return $oTemplate->parseHtmlByName('item_icon_preview.html', [
+            'id' => $iId,
+            'bx_if:show_icon_empty' => [
+                'condition' => !$bIconHtml,
+                'content' => []
+            ],
+            'bx_if:show_icon_image' => [
+                'condition' => false,
+                'content' => []
+            ],
+            'bx_if:show_icon_html' => [
+                'condition' => $bIconHtml,
+                'content' => [
+                    'icon' => $sIconHtml
+                ]
+            ]
+        ]);
+    }
+
+    protected function getCheckerFields($bMandatory = false)
+    {
+        $aResult = array(
+            'checker_func' => array(
+                'type' => 'select',
+                'name' => 'checker_func',
+                'caption' => _t('_adm_form_txt_field_checker_func'),
+                'info' => '',
+                'value' => '',
+                'values' => array(
+                    array('key' => '', 'value' => _t('_adm_form_txt_field_checker_empty'))
+                ),
+                'required' => $bMandatory ? '1' : '0',
+                'attrs' => array(
+                    'id' => 'bx-form-field-type',
+                    'onchange' => $this->getJsObject() . ".onSelectChecker(this)",
+                    'disabled' => 'disabled'
+                ),
+                'tr_attrs' => array(
+                    'style' => 'display:none'
+                ),
+                'db' => array (
+                    'pass' => 'Xss',
+                )
+            ),
+            'checker_params' => array(
+                'type' => 'hidden',
+                'name' => 'checker_params',
+                'value' => '',
+                'db' => array (
+                    'pass' => 'Xss',
+                )
+            ),
+            'checker_params_length_min' => array(
+                'type' => 'text',
+                'name' => 'checker_params_length_min',
+                'caption' => _t('_adm_form_txt_field_checker_params_length_min'),
+                'info' => '',
+                'value' => '',
+                'required' => $bMandatory ? '1' : '0',
+                'attrs' => array(
+                    'disabled' => 'disabled'
+                ),
+                'tr_attrs' => array(
+                    'style' => 'display:none'
+                ),
+                'db' => array (
+                    'pass' => 'Int',
+                )
+            ),
+            'checker_params_length_max' => array(
+                'type' => 'text',
+                'name' => 'checker_params_length_max',
+                'caption' => _t('_adm_form_txt_field_checker_params_length_max'),
+                'info' => '',
+                'value' => '',
+                'required' => $bMandatory ? '1' : '0',
+                'attrs' => array(
+                    'disabled' => 'disabled'
+                ),
+                'tr_attrs' => array(
+                    'style' => 'display:none'
+                ),
+                'db' => array (
+                    'pass' => 'Int',
+                )
+            ),
+            'checker_params_required' => array(
+                'type' => 'checkbox',
+                'name' => 'checker_params_required',
+                'caption' => _t('_adm_form_txt_field_checker_params_required'),
+                'info' => '',
+                'value' => '1',
+                'required' => $bMandatory ? '1' : '0',
+                'attrs' => array(
+                    'disabled' => 'disabled'
+                ),
+                'tr_attrs' => array(
+                    'style' => 'display:none'
+                ),
+                'db' => array (
+                    'pass' => 'Int',
+                )
+            ),
+            'checker_params_preg' => array(
+                'type' => 'text',
+                'name' => 'checker_params_preg',
+                'caption' => _t('_adm_form_txt_field_checker_params_preg'),
+                'info' => _t('_adm_form_dsc_field_checker_params_preg'),
+                'value' => '',
+                'required' => $bMandatory ? '1' : '0',
+                'attrs' => array(
+                    'disabled' => 'disabled'
+                ),
+                'tr_attrs' => array(
+                    'style' => 'display:none'
+                ),
+                'db' => array (
+                    'pass' => 'Xss',
+                )
+            ),
+            'checker_error' => array(
+                'type' => 'text_translatable',
+                'name' => 'checker_error',
+                'caption' => _t('_adm_form_txt_field_checker_error'),
+                'info' => '',
+                'value' => '_sys_form_txt_field_checker_error',
+                'required' => $bMandatory ? '1' : '0',
+                'attrs' => array(
+                    'disabled' => 'disabled'
+                ),
+                'tr_attrs' => array(
+                    'style' => 'display:none'
+                ),
+                'db' => array (
+                    'pass' => 'Xss',
+                )
+            ),
+        );
+
+        foreach($this->aCheckFunctions as $sCheckFunction)
+            $aResult['checker_func']['values'][] = array('key' => $sCheckFunction, 'value' => _t('_adm_form_txt_field_checker_' . $sCheckFunction));
+
+        if($bMandatory) 
+            $aResult = array_merge_recursive($aResult, array(
+                'checker_func' => array(
+                    'checker' => array (
+                        'func' => 'avail',
+                        'params' => array(),
+                        'error' => _t('_adm_form_err_field_checker_func'),
+                    )
+                ),
+                'checker_params_length_min' => array(
+                    'checker' => array (
+                        'func' => 'preg',
+                        'params' => array('/^[0-9]+$/'),
+                        'error' => _t('_adm_form_err_field_checker_params_length_min'),
+                    )
+                ),
+                'checker_params_length_max' => array(
+                    'checker' => array (
+                        'func' => 'preg',
+                        'params' => array('/^[0-9]+$/'),
+                        'error' => _t('_adm_form_err_field_checker_params_length_max'),
+                    )
+                ),
+                'checker_params_preg' => array(
+                    'checker' => array (
+                        'func' => 'avail',
+                        'params' => array(),
+                        'error' => _t('_adm_form_err_field_checker_params_preg'),
+                    ),
+                    
+                ),
+                'checker_error' => array(
+                    'checker' => array (
+                        'func' => 'availTranslatable',
+                        'params' => array('checker_error'),
+                        'error' => _t('_adm_form_err_field_checker_error'),
+                    )
+                )
+            ));
+
+        return $aResult;
+    }
+
+    protected function updateCheckerFields($sType, &$oForm)
+    {
+            if((int)$oForm->getCleanValue('required') == 0)
+                switch($sType) {
+                    case 'add':
+                        $this->unsetCheckerFields($oForm);
+                        break;
+
+                    case 'edit':
+                        $this->clearCheckerFields($oForm);
+                        break;
+                }
+            else {
+                unset($oForm->aInputs['checker_func']['tr_attrs']['style'], $oForm->aInputs['checker_error']['tr_attrs']['style']);
+                unset($oForm->aInputs['checker_func']['attrs']['disabled'], $oForm->aInputs['checker_error']['attrs']['disabled']);
+                switch($oForm->getCleanValue('checker_func')) {
+                    case 'length':
+                        unset($oForm->aInputs['checker_params_length_min']['tr_attrs']['style'], $oForm->aInputs['checker_params_length_max']['tr_attrs']['style']);
+                        unset($oForm->aInputs['checker_params_length_min']['attrs']['disabled'], $oForm->aInputs['checker_params_length_max']['attrs']['disabled']);
+                        $this->unsetCheckerFields($oForm, 'required');
+                        $this->unsetCheckerFields($oForm, 'preg');
+                        break;
+
+                    case 'date_range':
+                        unset($oForm->aInputs['checker_params_length_min']['tr_attrs']['style'], $oForm->aInputs['checker_params_length_max']['tr_attrs']['style'], $oForm->aInputs['checker_params_required']['tr_attrs']['style']);
+                        unset($oForm->aInputs['checker_params_length_min']['attrs']['disabled'], $oForm->aInputs['checker_params_length_max']['attrs']['disabled'], $oForm->aInputs['checker_params_required']['attrs']['disabled']);
+                        $this->unsetCheckerFields($oForm, 'preg');
+                        break;
+
+                    case 'preg':
+                        unset($oForm->aInputs['checker_params_preg']['tr_attrs']['style']);
+                        unset($oForm->aInputs['checker_params_preg']['attrs']['disabled']);
+                        $this->unsetCheckerFields($oForm, 'date_range');
+                        break;
+
+                    default:
+                        $this->unsetCheckerFields($oForm, 'params');
+                }
+            }
+    }
+
+    protected function clearCheckerFields(&$oForm, $sCheckerFunc = 'all')
+    {
+        switch($sCheckerFunc) {
+            case 'length':
+                $oForm->aInputs['checker_params_length_min']['value'] = '';
+                $oForm->aInputs['checker_params_length_max']['value'] = '';
+                break;
+                
+            case 'date_range':
+                $oForm->aInputs['checker_params_length_min']['value'] = '';
+                $oForm->aInputs['checker_params_length_max']['value'] = '';
+                $oForm->aInputs['checker_params_required']['value'] = '';
+                break;
+
+            case 'required':
+                $oForm->aInputs['checker_params_required']['value'] = '';
+                break;
+
+            case 'preg':
+                $oForm->aInputs['checker_params_preg']['value'] = '';
+                break;
+
+            case 'params':
+                $oForm->aInputs['checker_params_length_min']['value'] = '';
+                $oForm->aInputs['checker_params_length_max']['value'] = '';
+                $oForm->aInputs['checker_params_required']['value'] = '';
+                $oForm->aInputs['checker_params_preg']['value'] = '';
+                break;
+
+            case 'all':
+                $oForm->aInputs['checker_func']['value'] = '';
+                $oForm->aInputs['checker_params']['value'] = '';
+                $oForm->aInputs['checker_params_length_min']['value'] = '';
+                $oForm->aInputs['checker_params_length_max']['value'] = '';
+                $oForm->aInputs['checker_params_required']['value'] = '';
+                $oForm->aInputs['checker_params_preg']['value'] = '';
+                $oForm->aInputs['checker_error']['value'] = '';
+        }
+    }
+
+    protected function unsetCheckerFields(&$oForm, $sCheckerFunc = 'all')
+    {
+        switch($sCheckerFunc) {
+            case 'length':
+                unset(
+                    $oForm->aInputs['checker_params_length_min'],
+                    $oForm->aInputs['checker_params_length_max']
+                );
+                break;
+                
+            case 'date_range':
+                unset(
+                    $oForm->aInputs['checker_params_length_min'],
+                    $oForm->aInputs['checker_params_length_max'],
+                    $oForm->aInputs['checker_params_required']
+                );
+                break;
+
+            case 'required':
+                unset(
+                    $oForm->aInputs['checker_params_required']
+                );
+                break;
+
+            case 'preg':
+                unset($oForm->aInputs['checker_params_preg']);
+                break;
+
+            case 'params':
+                unset(
+                    $oForm->aInputs['checker_params_length_min'],
+                    $oForm->aInputs['checker_params_length_max'],
+                    $oForm->aInputs['checker_params_required'],
+                    $oForm->aInputs['checker_params_preg']
+                );
+                break;
+
+            case 'all':
+                unset(
+                    $oForm->aInputs['checker_func'],
+                    $oForm->aInputs['checker_params'],
+                    $oForm->aInputs['checker_params_length_min'],
+                    $oForm->aInputs['checker_params_length_max'],
+                    $oForm->aInputs['checker_params_required'],
+                    $oForm->aInputs['checker_params_preg'],
+                    $oForm->aInputs['checker_error']
+                );
+        }
+    }
+
+    protected function genFieldName($sObject, $sPrefix = 'field')
+    {
+        $aFields = array();
+        $this->oDb->getInputs(array('type' => 'by_object_name_filter', 'object' => $sObject, 'name_filter' => $sPrefix . '%'), $aFields, false);
+
+        for($iIndex = 1; true; $iIndex++)
+            if(!in_array($sPrefix . $iIndex, $aFields))
+                break;
+
+        return $sPrefix . $iIndex;
+    }
+
+    protected function onCheckField($sType, &$oForm)
+    {
+        $bAlter = false;
+
+        //--- Process field 'required' and related 'checker' fields.
+        if(isset($oForm->aInputs['required']))
+            $this->updateCheckerFields($sType, $oForm);
+
+        //--- Process field 'db_pass' and related dependencies.
+        if(isset($oForm->aInputs['db_pass']) && $oForm->aInputs['db_pass']['type'] == 'select') {
+            $sDbPass = $oForm->getCleanValue('db_pass');
+
+            $this->aForm['inputs']['value']['db']['pass'] = $sDbPass;
+
+            if(!empty($this->aDbPassDependency[$sDbPass])) {
+                $this->aParams['table_field_type'] = $this->aDbPassDependency[$sDbPass]['alter'];
+                $bAlter = true;
+            }
+        }
+
+        return $bAlter;
+    }
+
+    protected function onSubmitField(&$oForm)
+    {
+        //--- Process field value.
+        if(isset($oForm->aInputs['value']['db']))
+            $oForm->aInputs['value']['db']['pass'] = $oForm->getCleanValue('db_pass');
+
+        //--- Process field values.
+        if(isset($oForm->aInputs['values']['db'])) 
+            $this->onSubmitFieldValues($oForm);
+
+        //--- Process field 'html' flag.
+        if(isset($oForm->aInputs['html'])) {
+            $iHtml = (int)$oForm->getCleanValue('html');
+            BxDolForm::setSubmittedValue('db_pass', $iHtml == 0 ? 'XssMultiline' : 'XssHtml', $oForm->aFormAttrs['method']);
+        }
+
+        //--- Process field checker.
+        $sCheckerFunc = $oForm->getCleanValue('checker_func');
+
+        $aCheckerParams = array();
+        if(!empty($sCheckerFunc)) {
+            if(isset($oForm->aInputs['checker_params_length_min'], $oForm->aInputs['checker_params_length_max'])) {
+                $aCheckerParams['min'] = $oForm->getCleanValue('checker_params_length_min');
+                $aCheckerParams['max'] = $oForm->getCleanValue('checker_params_length_max');
+            }
+
+            if(isset($oForm->aInputs['checker_params_preg']))
+                $aCheckerParams['preg'] = $oForm->getCleanValue('checker_params_preg');
+
+            if(isset($oForm->aInputs['checker_params_required']))
+                $aCheckerParams['required'] = $oForm->getCleanValue('checker_params_required');
+
+            switch($sCheckerFunc) {
+                case 'location':
+                    $aCheckerParams['name'] = $oForm->getCleanValue('name');
+                    break;
+            }
+        }
+
+        unset($oForm->aInputs['checker_params_length_min'], $oForm->aInputs['checker_params_length_max'], $oForm->aInputs['checker_params_preg'], $oForm->aInputs['checker_params_required']);
+        BxDolForm::setSubmittedValue('checker_params', !empty($aCheckerParams) ? serialize($aCheckerParams) : '', $oForm->aFormAttrs['method']);
+
+        //--- Process field attrs.
+        $aAttrs = array();
+        if(isset($oForm->aInputs['attrs_min'], $oForm->aInputs['attrs_max'], $oForm->aInputs['attrs_step'])) {
+            $aAttrs['min'] = $oForm->getCleanValue('attrs_min');
+            $aAttrs['max'] = $oForm->getCleanValue('attrs_max');
+            $aAttrs['step'] = $oForm->getCleanValue('attrs_step');
+        } else if(isset($oForm->aInputs['attrs_src']))
+            $aAttrs['src'] = $oForm->getCleanValue('attrs_src');
+
+        unset($oForm->aInputs['attrs_min'], $oForm->aInputs['attrs_max'], $oForm->aInputs['attrs_step'], $oForm->aInputs['attrs_src']);
+        BxDolForm::setSubmittedValue('attrs', serialize($aAttrs), $oForm->aFormAttrs['method']);
+    }
+
+    protected function onSubmitFieldValues(&$oForm)
+    {
+    	$sValues = $oForm->getCleanValue('values');
+		if(is_string($sValues) && strpos($sValues, BX_DATA_LISTS_KEY_PREFIX) === false)
+        	BxDolForm::setSubmittedValue('values', serialize(explode("\n", $sValues)), $oForm->aFormAttrs['method']);
+    }
+}
+
+class BxBaseStudioFormsFieldBlockHeader extends BxBaseStudioFormsField
+{
+    protected $sType = 'block_header';
+
+    public function init()
+	{
+		parent::init();
+
+		$this->aForm = array(
+            'form_attrs' => array(
+                'id' => '',
+                'action' => '',
+                'method' => 'post'
+            ),
+            'params' => array (
+                'db' => array(
+                    'table' => 'sys_form_inputs',
+                    'key' => 'id',
+                    'uri' => '',
+                    'uri_title' => '',
+                    'submit_name' => 'do_submit'
+                ),
+            ),
+            'inputs' => array (
+                'module' => array(
+                    'type' => 'hidden',
+                    'name' => 'module',
+                    'caption' => _t('_adm_form_txt_field_module'),
+                    'value' => 'custom',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+                'object' => array(
+                    'type' => 'hidden',
+                    'name' => 'object',
+                    'caption' => _t('_adm_form_txt_field_object'),
+                    'value' => '',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+                'name' => array(
+                    'type' => 'hidden',
+                    'name' => 'name',
+                	'caption' => _t('_adm_form_txt_field_name'),
+                    'value' => '',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    )
+                ),
+                'type' => array(
+                    'type' => 'hidden',
+                    'name' => 'type',
+                    'value' => $this->sType,
+                    'db' => array (
+                        'pass' => 'Xss',
+                    )
+                ),
+                'db_pass' => array(
+                    'type' => 'hidden',
+                    'name' => 'db_pass',
+                    'value' => $this->sDbPass,
+                    'db' => array (
+                        'pass' => 'Xss',
+                    )
+                ),
+                'type_display' => $this->getFieldTypesSelector('type_display', $this->sType),
+                'caption_system' => array(
+                    'type' => 'text_translatable',
+                    'name' => 'caption_system',
+                    'caption' => _t('_adm_form_txt_field_caption_system'),
+                    'info' => _t('_adm_form_dsc_field_caption_system'),
+                    'value' => '_sys_form_txt_field',
+                    'required' => '0',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+                'caption' => array(
+                    'type' => 'text_translatable',
+                    'name' => 'caption',
+                    'caption' => _t('_adm_form_txt_field_caption'),
+                    'info' => _t('_adm_form_dsc_field_caption_block_header'),
+                    'value' => '_sys_form_txt_field',
+                    'required' => '1',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                    'checker' => array (
+                        'func' => 'LengthTranslatable',
+                        'params' => array(3, 100, 'caption'),
+                        'error' => _t('_adm_form_err_field_caption'),
+                    ),
+                ),
+                'collapsed' => array(
+                    'type' => 'checkbox',
+                    'name' => 'collapsed',
+                    'caption' => _t('_adm_form_txt_field_collapsed'),
+                    'info' => _t('_adm_form_dsc_field_collapsed'),
+                    'value' => '1',
+                    'required' => '0',
+                    'db' => array (
+                        'pass' => 'Int',
+                    )
+                ),
+                'privacy' => array(
+                    'type' => 'switcher',
+                    'name' => 'privacy',
+                    'caption' => _t('_adm_form_txt_field_privacy'),
+                    'info' => _t('_adm_form_dsc_field_privacy'),
+                    'value' => '1',
+                    'required' => '0',
+                    'attrs' => array(
+                        'id' => 'bx-form-field-privacy'
+                    ),
+                    'db' => array (
+                        'pass' => 'Int',
+                    )
+                ),
+                'rateable' => array(
+                    'type' => 'select',
+                    'name' => 'rateable',
+                    'caption' => _t('_adm_form_txt_field_rateable'),
+                    'info' => _t('_adm_form_dsc_field_rateable'),
+                    'values' => array(
+                        array('key' => '', 'value' => _t('_adm_form_txt_field_rateable_value_non')),
+                        array('key' => 'sys_form_fields_votes', 'value' => _t('_adm_form_txt_field_rateable_value_votes')),
+                        array('key' => 'sys_form_fields_reaction', 'value' => _t('_adm_form_txt_field_rateable_value_reactions'))
+                	),
+                    'required' => '0',
+                    'attrs' => array(
+                        'id' => 'bx-form-field-rateable'
+                    ),
+                    'db' => array (
+                        'pass' => 'Xss',
+                    )
+                ),
+                'icon' => array(
+                    'type' => 'textarea',
+                    'name' => 'icon',
+                    'caption' => _t('_adm_form_txt_field_icon'),
+                    'info' => _t('_adm_form_dsc_field_icon'),
+                    'value' => '',
+                    'code' => 1,
+                    'required' => '0',
+                    'attrs' => array('class' => 'bx-form-input-textarea-small'),
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+                'icon_preview' => array(
+                    'type' => 'custom',
+                    'name' => 'icon_preview',
+                    'caption' => _t('_adm_form_txt_field_icon_preview'),
+                    'content' => ''
+                ),
+                'controls' => array(
+                    'name' => 'controls',
+                    'type' => 'input_set',
+                    array(
+                        'type' => 'submit',
+                        'name' => 'do_submit',
+                        'value' => _t('_adm_form_btn_field_add'),
+                    ),
+                    array (
+                        'type' => 'reset',
+                        'name' => 'close',
+                        'value' => _t('_adm_form_btn_field_cancel'),
+                        'attrs' => array(
+                            'onclick' => "$('.bx-popup-applied:visible').dolPopupHide()",
+                            'class' => 'bx-def-margin-sec-left',
+                        ),
+                    )
+                )
+            )
+        );
+        
+        if ($this->isNested()){
+            unset($this->aForm['inputs']['rateable']);
+        }
+    }
+}
+
+class BxBaseStudioFormsFieldBlockEnd extends BxBaseStudioFormsField
+{
+    protected $sType = 'block_end';
+
+    public function init()
+    {
+        parent::init();
+
+        $this->aForm = array(
+            'form_attrs' => array(
+                'id' => '',
+                'action' => '',
+                'method' => 'post'
+            ),
+            'params' => array (
+                'db' => array(
+                    'table' => 'sys_form_inputs',
+                    'key' => 'id',
+                    'uri' => '',
+                    'uri_title' => '',
+                    'submit_name' => 'do_submit'
+                ),
+            ),
+            'inputs' => array (
+                'module' => array(
+                    'type' => 'hidden',
+                    'name' => 'module',
+                    'caption' => _t('_adm_form_txt_field_module'),
+                    'value' => 'custom',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+                'object' => array(
+                    'type' => 'hidden',
+                    'name' => 'object',
+                    'caption' => _t('_adm_form_txt_field_object'),
+                    'value' => '',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+                'name' => array(
+                    'type' => 'hidden',
+                    'name' => 'name',
+                    'caption' => _t('_adm_form_txt_field_name'),
+                    'value' => '',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    )
+                ),
+                'type' => array(
+                    'type' => 'hidden',
+                    'name' => 'type',
+                    'value' => $this->sType,
+                    'db' => array (
+                        'pass' => 'Xss',
+                    )
+                ),
+                'db_pass' => array(
+                    'type' => 'hidden',
+                    'name' => 'db_pass',
+                    'value' => $this->sDbPass,
+                    'db' => array (
+                        'pass' => 'Xss',
+                    )
+                ),
+                'type_display' => $this->getFieldTypesSelector('type_display', $this->sType),
+                'caption_system' => array(
+                    'type' => 'text_translatable',
+                    'name' => 'caption_system',
+                    'caption' => _t('_adm_form_txt_field_caption_system'),
+                    'info' => _t('_adm_form_dsc_field_caption_system'),
+                    'value' => '_sys_form_txt_field',
+                    'required' => '1',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                    'checker' => array (
+                        'func' => 'LengthTranslatable',
+                        'params' => array(3,100, 'caption_system'),
+                        'error' => _t('_adm_form_err_field_caption_system'),
+                    ),
+                ),
+                'controls' => array(
+                    'name' => 'controls',
+                    'type' => 'input_set',
+                    array(
+                        'type' => 'submit',
+                        'name' => 'do_submit',
+                        'value' => _t('_adm_form_btn_field_add'),
+                    ),
+                    array (
+                        'type' => 'reset',
+                        'name' => 'close',
+                        'value' => _t('_adm_form_btn_field_cancel'),
+                        'attrs' => array(
+                            'onclick' => "$('.bx-popup-applied:visible').dolPopupHide()",
+                            'class' => 'bx-def-margin-sec-left',
+                        ),
+                    )
+                )
+            )
+        );
+    }
+}
+
+class BxBaseStudioFormsFieldValue extends BxBaseStudioFormsFieldBlockHeader
+{
+    protected $sType = 'value';
+
+    public function init()
+    {
+        parent::init();
+
+        $this->aForm['inputs']['caption']['info'] = _t('_adm_form_dsc_field_caption');
+
+        $aFields = array(
+            'value' => array(
+                'type' => 'text',
+                'name' => 'value',
+                'caption' => _t('_adm_form_txt_field_value_custom_text'),
+                'info' => '',
+                'value' => '',
+                'required' => '0',
+                'db' => array (
+                    'pass' => 'Xss',
+                )
+            ),
+        );
+        $this->aForm['inputs'] = $this->addInArray($this->aForm['inputs'], 'caption', $aFields);
+
+        unset(
+            $this->aForm['inputs']['collapsed']
+        );
+    }
+}
+
+class BxBaseStudioFormsFieldText extends BxBaseStudioFormsFieldBlockHeader
+{
+    protected $sType = 'text';
+    protected $aCheckFunctions = array('avail', 'length', 'preg', 'email');
+    protected $sDbPass = 'Xss';
+
+    protected $aFieldUnique;
+
+    public function init()
+    {
+        parent::init();
+
+        $this->aParams['table_alter'] = true;
+        $this->aParams['table_field_type'] = 'varchar(255)';
+
+        $this->aForm['inputs']['caption']['info'] = _t('_adm_form_dsc_field_caption');
+
+        $this->aFieldUnique = array(
+            'type' => 'switcher',
+            'name' => 'unique',
+            'caption' => _t('_adm_form_txt_field_unique'),
+            'info' => _t('_adm_form_dsc_field_unique'),
+            'value' => '1',
+            'required' => '0',
+            'attrs' => array(
+                'id' => 'bx-form-field-unique'
+            ),
+            'db' => array (
+                'pass' => 'Int',
+            )
+        );
+
+        $aFields = array(
+            'value' => array(
+                'type' => 'text',
+                'name' => 'value',
+                'caption' => _t('_adm_form_txt_field_value_default'),
+                'info' => '',
+                'value' => '',
+                'required' => '0',
+                'db' => array (
+                    'pass' => 'Xss',
+                )
+            ),
+            'info' => array(
+                'type' => 'textarea_translatable',
+                'name' => 'info',
+                'caption' => _t('_adm_form_txt_field_info'),
+                'info' => _t('_adm_form_dsc_field_info'),
+                'value' => '_sys_form_txt_field',
+                'required' => '0',
+                'code' => 1,
+                'db' => array (
+                    'pass' => 'Xss',
+                )
+            ),
+            'help' => array(
+                'type' => 'textarea_translatable',
+                'name' => 'help',
+                'caption' => _t('_adm_form_txt_field_help'),
+                'info' => _t('_adm_form_dsc_field_help'),
+                'value' => '_sys_form_txt_field',
+                'required' => '0',
+                'html' => 2,
+                'db' => array (
+                    'pass' => 'Xss',
+                )
+            ),
+            'required' => array(
+                'type' => 'switcher',
+                'name' => 'required',
+                'caption' => _t('_adm_form_txt_field_required'),
+                'info' => _t('_adm_form_dsc_field_required'),
+                'value' => '1',
+                'required' => '0',
+                'attrs' => array(
+                    'id' => 'bx-form-field-required',
+                    'onchange' => $this->getJsObject() . ".onCheckRequired(this)"
+                ),
+                'db' => array (
+                    'pass' => 'Int',
+                )
+            ),
+            'unique' => $this->aFieldUnique
+        );
+
+        $aFields = array_merge($aFields, $this->getCheckerFields());
+
+        $this->aForm['inputs'] = $this->addInArray($this->aForm['inputs'], 'caption', $aFields);
+        unset($this->aForm['inputs']['collapsed']);
+    }
+}
+
+class BxBaseStudioFormsFieldPassword extends BxBaseStudioFormsFieldText
+{
+    protected $sType = 'password';
+    protected $aCheckFunctions = array('avail', 'length', 'preg');
+    
+    public function init()
+	{
+		parent::init();
+
+		unset($this->aForm['inputs']['unique']);
+	}
+}
+
+class BxBaseStudioFormsFieldTextarea extends BxBaseStudioFormsFieldText
+{
+    protected $sType = 'textarea';
+    protected $aCheckFunctions = array('avail', 'length', 'preg');
+
+    public function init()
+	{
+		parent::init();
+
+        $this->aParams['table_field_type'] = 'text';
+
+        $this->aForm['inputs']['value']['type'] = $this->sType;
+
+        $aFields = array(
+            'html' => array(
+                'type' => 'select',
+                'name' => 'html',
+                'caption' => _t('_adm_form_txt_field_html'),
+                'info' => _t('_adm_form_dsc_field_html'),
+                'value' => '0',
+                'values' => array(
+                    array('key' => '0', 'value' => _t('_adm_form_txt_field_html_none')),
+                    array('key' => '1', 'value' => _t('_adm_form_txt_field_html_standard')),
+                    array('key' => '2', 'value' => _t('_adm_form_txt_field_html_full')),
+                    array('key' => '3', 'value' => _t('_adm_form_txt_field_html_mini')),
+                ),
+                'required' => '0',
+                'db' => array (
+                    'pass' => 'Int',
+                )
+            ),
+        );
+
+        $this->aForm['inputs'] = $this->addInArray($this->aForm['inputs'], 'help', $aFields);
+        unset($this->aForm['inputs']['unique']);
+    }
+}
+
+class BxBaseStudioFormsFieldDatepicker extends BxBaseStudioFormsFieldText
+{
+    protected $sType = 'datepicker';
+    protected $aCheckFunctions = array('date','date_range');
+    protected $sDbPass = 'DateTs';
+    protected $aDbPassDependency = array(
+        'Date' => array('alter' => 'date'),
+    	'DateTs' => array('alter' => 'int(11)'),
+    	'DateUtc' => array('alter' => 'int(11)'),
+    );
+
+    public function init()
+	{
+		parent::init();
+
+        $this->aParams['table_field_type'] = 'int(11)';
+
+        $this->aForm['inputs']['value']['type'] = $this->sType;
+        $this->aForm['inputs']['value']['db']['pass'] = $this->sDbPass;
+
+        $aFields = array(
+            'db_pass' => array(
+                'type' => 'select',
+                'name' => 'db_pass',
+                'caption' => _t('_adm_form_txt_field_db_pass'),
+                'info' => _t('_adm_form_dsc_field_db_pass'),
+                'value' => $this->sDbPass,
+                'values' => array(
+                    array('key' => '', 'value' => _t('_adm_form_txt_field_db_pass_select_value')),
+                    array('key' => 'Date', 'value' => _t('_adm_form_txt_field_db_pass_date')),
+                    array('key' => 'DateTs', 'value' => _t('_adm_form_txt_field_db_pass_date_ts')),
+                    array('key' => 'DateUtc', 'value' => _t('_adm_form_txt_field_db_pass_date_utc')),
+                ),
+                'required' => '0',
+                'db' => array (
+                    'pass' => 'Xss',
+                )
+            ),
+        );
+
+        unset($this->aForm['inputs']['db_pass']);
+        $this->aForm['inputs'] = $this->addInArray($this->aForm['inputs'], 'controls', $aFields, false);
+    }
+}
+
+class BxBaseStudioFormsFieldDateselect extends BxBaseStudioFormsFieldText
+{
+    protected $sType = 'dateselect';
+    protected $aCheckFunctions = array('date','date_range');
+    protected $sDbPass = 'DateTs';
+    protected $aDbPassDependency = array(
+        'Date' => array('alter' => 'date'),
+    	'DateTs' => array('alter' => 'int(11)'),
+    	'DateUtc' => array('alter' => 'int(11)'),
+    );
+
+    public function init()
+	{
+		parent::init();
+
+        $this->aParams['table_field_type'] = 'int(11)';
+
+        $this->aForm['inputs']['value']['type'] = $this->sType;
+        $this->aForm['inputs']['value']['db']['pass'] = $this->sDbPass;
+
+        $aFields = array(
+            'db_pass' => array(
+                'type' => 'select',
+                'name' => 'db_pass',
+                'caption' => _t('_adm_form_txt_field_db_pass'),
+                'info' => _t('_adm_form_dsc_field_db_pass'),
+                'value' => $this->sDbPass,
+                'values' => array(
+                    array('key' => '', 'value' => _t('_adm_form_txt_field_db_pass_select_value')),
+                    array('key' => 'Date', 'value' => _t('_adm_form_txt_field_db_pass_date')),
+                    array('key' => 'DateTs', 'value' => _t('_adm_form_txt_field_db_pass_date_ts')),
+                    array('key' => 'DateUtc', 'value' => _t('_adm_form_txt_field_db_pass_date_utc')),
+                ),
+                'required' => '0',
+                'db' => array (
+                    'pass' => 'Xss',
+                )
+            ),
+        );
+
+        unset($this->aForm['inputs']['db_pass']);
+        $this->aForm['inputs'] = $this->addInArray($this->aForm['inputs'], 'controls', $aFields, false);
+    }
+}
+
+class BxBaseStudioFormsFieldDatetime extends BxBaseStudioFormsFieldDatepicker
+{
+    protected $sType = 'datetime';
+    protected $aCheckFunctions = array('date_time');
+    protected $sDbPass = 'DateTimeTs';
+    protected $aDbPassDependency = array(
+        'DateTime' => array('alter' => 'datetime'),
+    	'DateTimeTs' => array('alter' => 'int(11)'),
+    	'DateTimeUtc' => array('alter' => 'int(11)'),
+    );
+
+    public function init()
+	{
+		parent::init();
+
+        $this->aForm['inputs']['value']['db']['pass'] = $this->sDbPass;
+
+        $this->aForm['inputs']['db_pass']['values'] = array(
+            array('key' => '', 'value' => _t('_adm_form_txt_field_db_pass_select_value')),
+            array('key' => 'DateTime', 'value' => _t('_adm_form_txt_field_db_pass_date_time')),
+            array('key' => 'DateTimeTs', 'value' => _t('_adm_form_txt_field_db_pass_date_time_ts')),
+            array('key' => 'DateTimeUtc', 'value' => _t('_adm_form_txt_field_db_pass_date_time_utc')),
+        );
+    }
+}
+
+class BxBaseStudioFormsFieldCheckbox extends BxBaseStudioFormsFieldText
+{
+    protected $sType = 'checkbox';
+    protected $aCheckFunctions = array('avail', 'length', 'preg');
+
+    public function init()
+	{
+		parent::init();
+
+        $this->aForm['inputs']['value']['type'] = 'hidden';
+        $this->aForm['inputs']['value']['caption'] = _t('_adm_form_txt_field_value_checkbox');
+        $this->aForm['inputs']['value']['value'] = '1';
+        $this->aForm['inputs']['value']['info'] = _t('_adm_form_dsc_field_value_checkbox');
+
+        $aFields = array(
+            'checked' => array(
+                'type' => 'checkbox',
+                'name' => 'checked',
+                'caption' => _t('_adm_form_txt_field_checked'),
+                'value' => '1',
+                'required' => '0',
+                'db' => array (
+                    'pass' => 'Int',
+                )
+            )
+        );
+        $this->aForm['inputs'] = $this->addInArray($this->aForm['inputs'], 'value', $aFields);
+        unset($this->aForm['inputs']['unique']);
+    }
+}
+
+class BxBaseStudioFormsFieldSwitcher extends BxBaseStudioFormsFieldCheckbox
+{
+    protected $sType = 'switcher';
+
+    public function init()
+	{
+		parent::init();
+
+        $this->aForm['inputs']['checked']['caption'] = _t('_adm_form_txt_field_checked_switcher');
+    }
+}
+
+class BxBaseStudioFormsFieldFile extends BxBaseStudioFormsFieldText
+{
+    protected $sType = 'file';
+    protected $aCheckFunctions = array('avail', 'length', 'preg');
+    protected $sDbPass = '';
+
+    public function init()
+	{
+		parent::init();
+
+        unset(
+            $this->aForm['inputs']['value'],
+            $this->aForm['inputs']['unique']
+        );
+    }
+}
+
+class BxBaseStudioFormsFieldFiles extends BxBaseStudioFormsFieldFile
+{
+    protected $sType = 'files';
+
+    public function init()
+    {
+        parent::init();
+
+        $aFields = array(
+            'values' => array(
+                'type' => 'hidden',
+                'name' => 'values',
+                'value' => array(),
+                'db' => array (
+                    'pass' => 'Xss',
+                )
+            ),
+            'value' => array(
+                'type' => 'checkbox_set',
+                'name' => 'value',
+                'caption' => _t('_adm_form_txt_field_value_files'),
+                'info' => '',
+                'value' => array(),
+            	'values' => array(),
+                'required' => '0',
+                'db' => array (
+                    'pass' => 'Xss',
+                )
+            )
+        );
+
+        $aUploaders = array(
+            'sys_html5' => '_sys_uploader_html5_title'
+        );
+        foreach($aUploaders as $sObject => $sTitle) {
+            $aUploader = BxDolUploaderQuery::getUploaderObject($sObject);
+            if(empty($aUploader) || !is_array($aUploader) || (int)$aUploader['active'] == 0)
+                continue;
+
+            $aFields['values']['value'][$sObject] = $sTitle;
+            $aFields['value']['values'][$sObject] = _t($sTitle);
+        }
+        $aFields['values']['value'] = !empty($aFields['values']['value']) && is_array($aFields['values']['value']) ? serialize($aFields['values']['value']) : '';
+
+        $this->aForm['inputs'] = $this->addInArray($this->aForm['inputs'], 'required', $aFields);
+    }
+
+    protected function onSubmitField(&$oForm)
+    {
+    	//--- Process field value.
+        if(isset($oForm->aInputs['value']['db'])) 
+        	$this->onSubmitFieldValue($oForm);
+
+		parent::onSubmitField($oForm);
+    }
+
+	protected function onSubmitFieldValue(&$oForm)
+    {
+    	$mixedValue = $oForm->getCleanValue('value');
+		if(is_array($mixedValue))
+			BxDolForm::setSubmittedValue('value', serialize($mixedValue), $oForm->aFormAttrs['method']);
+    }
+
+    /*
+     * Note. Value of Values should be saved as is in case of 'Files' field.
+     */
+	protected function onSubmitFieldValues(&$oForm)
+    {
+		return;
+    }
+}
+
+class BxBaseStudioFormsFieldNumber extends BxBaseStudioFormsFieldText
+{
+    protected $sType = 'number';
+    protected $aCheckFunctions = array('avail', 'length', 'preg');
+    protected $sDbPass = 'Int';
+
+    public function init()
+	{
+		parent::init();
+
+        $this->aParams['table_field_type'] = 'int(11)';
+
+        $this->aForm['inputs']['value']['db']['pass'] = 'Int';
+        $this->aForm['inputs']['value']['checker'] = array (
+            'func' => 'preg',
+            'params' => array('/^\d*?$/'),
+            'error' => _t('_adm_form_err_field_value_number'),
+        );
+    }
+}
+
+class BxBaseStudioFormsFieldPrice extends BxBaseStudioFormsFieldText
+{
+    protected $sType = 'price';
+    protected $aCheckFunctions = ['avail', 'length', 'preg'];
+    protected $sDbPass = 'Float';
+
+    public function init() 
+    {
+        parent::init();
+
+        $this->aParams['table_field_type'] = 'float';
+
+        $this->aForm['inputs']['value']['db']['pass'] = 'Float';
+        $this->aForm['inputs']['value']['checker'] = [
+            'func' => 'preg',
+            'params' => array('/^\d*?$/'),
+            'error' => _t('_adm_form_err_field_value_number'),
+        ];
+    }
+}
+
+class BxBaseStudioFormsFieldNestedForm extends BxBaseStudioFormsField
+{
+    protected $sType = 'nested_form';
+
+    public function init()
+	{
+		parent::init();
+        $aFormsData = array();
+		$aParams = array('type' => 'nested', 'parent_form' => $this->aParams['object']);
+		if (bx_get('ids'))
+			$aParams['ids'] = implode(',', bx_get('ids'));
+        $this->oDb->getForms($aParams, $aFormsData, false);
+        foreach($aFormsData as $sKey => $sValue){
+            $aFormsData[$sKey] = _t($sValue);
+        }
+		$this->aForm = array(
+            'form_attrs' => array(
+                'id' => '',
+                'action' => '',
+                'method' => 'post'
+            ),
+            'params' => array (
+                'db' => array(
+                    'table' => 'sys_form_inputs',
+                    'key' => 'id',
+                    'uri' => '',
+                    'uri_title' => '',
+                    'submit_name' => 'do_submit'
+                ),
+            ),
+            'inputs' => array (
+                'module' => array(
+                    'type' => 'hidden',
+                    'name' => 'module',
+                    'caption' => _t('_adm_form_txt_field_module'),
+                    'value' => 'custom',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+                'object' => array(
+                    'type' => 'hidden',
+                    'name' => 'object',
+                    'caption' => _t('_adm_form_txt_field_object'),
+                    'value' => '',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+                'name' => array(
+                    'type' => 'hidden',
+                    'name' => 'name',
+                	'caption' => _t('_adm_form_txt_field_name'),
+                    'value' => '',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    )
+                ),
+                'type' => array(
+                    'type' => 'hidden',
+                    'name' => 'type',
+                    'value' => $this->sType,
+                    'db' => array (
+                        'pass' => 'Xss',
+                    )
+                ),
+                'db_pass' => array(
+                    'type' => 'hidden',
+                    'name' => 'db_pass',
+                    'value' => $this->sDbPass,
+                    'db' => array (
+                        'pass' => 'Xss',
+                    )
+                ),
+                'type_display' => $this->getFieldTypesSelector('type_display', $this->sType),
+                'caption_system' => array(
+                    'type' => 'text_translatable',
+                    'name' => 'caption_system',
+                    'caption' => _t('_adm_form_txt_field_caption_system'),
+                    'info' => _t('_adm_form_dsc_field_caption_system'),
+                    'value' => '_sys_form_txt_field',
+                    'required' => '0',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                ),
+                'caption' => array(
+                    'type' => 'text_translatable',
+                    'name' => 'caption',
+                    'caption' => _t('_adm_form_txt_field_caption'),
+                    'info' => _t('_adm_form_dsc_field_caption_block_header'),
+                    'value' => '_sys_form_txt_field',
+                    'required' => '1',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                    'checker' => array (
+                        'func' => 'LengthTranslatable',
+                        'params' => array(3, 100, 'caption'),
+                        'error' => _t('_adm_form_err_field_caption'),
+                    ),
+                ),
+                'value' => array(
+                    'type' => 'select',
+                    'name' => 'value',
+                    'caption' => _t('_adm_form_txt_field_select_nested_form'),
+                    'values' => $aFormsData,
+                    'required' => '1',
+                    'db' => array (
+                        'pass' => 'Xss',
+                    ),
+                    'checker' => array (
+                        'func' => 'avail',
+                        'error' => _t('_adm_form_err_field_select_nested_form'),
+                    ),
+                ),
+                'privacy' => array(
+                    'type' => 'switcher',
+                    'name' => 'privacy',
+                    'caption' => _t('_adm_form_txt_field_privacy'),
+                    'info' => _t('_adm_form_dsc_field_privacy'),
+                    'value' => '1',
+                    'required' => '0',
+                    'attrs' => array(
+                        'id' => 'bx-form-field-privacy'
+                    ),
+                    'db' => array (
+                        'pass' => 'Int',
+                    )
+                ),
+                'rateable' => array(
+                    'type' => 'select',
+                    'name' => 'rateable',
+                    'caption' => _t('_adm_form_txt_field_rateable'),
+                    'info' => _t('_adm_form_dsc_field_rateable'),
+                    'values' => array(
+                		array('key' => '', 'value' => _t('_adm_form_txt_field_rateable_value_non')),
+                        array('key' => 'sys_form_fields_votes', 'value' => _t('_adm_form_txt_field_rateable_value_votes')),
+                        array('key' => 'sys_form_fields_reaction', 'value' => _t('_adm_form_txt_field_rateable_value_reactions'))
+                	),
+                    'required' => '0',
+                    'attrs' => array(
+                        'id' => 'bx-form-field-rateable'
+                    ),
+                    'db' => array (
+                        'pass' => 'Xss',
+                    )
+                ),
+                'controls' => array(
+                    'name' => 'controls',
+                    'type' => 'input_set',
+                    array(
+                        'type' => 'submit',
+                        'name' => 'do_submit',
+                        'value' => _t('_adm_form_btn_field_add'),
+                    ),
+                    array (
+                        'type' => 'reset',
+                        'name' => 'close',
+                        'value' => _t('_adm_form_btn_field_cancel'),
+                        'attrs' => array(
+                            'onclick' => "$('.bx-popup-applied:visible').dolPopupHide()",
+                            'class' => 'bx-def-margin-sec-left',
+                        ),
+                    )
+                )
+            )
+        );
+    }
+}
+
+class BxBaseStudioFormsFieldTime extends BxBaseStudioFormsFieldText
+{
+    protected $sType = 'time';
+    protected $aCheckFunctions = array('avail');
+    protected $sDbPass = 'Xss';
+    
+    public function init()
+	{
+		parent::init();
+
+        $this->aParams['table_field_type'] = 'time';
+
+        $this->aForm['inputs']['value']['db']['pass'] = 'Xss';
+    }
+}
+
+class BxBaseStudioFormsFieldSlider extends BxBaseStudioFormsFieldNumber
+{
+    protected $sType = 'slider';
+    protected $aCheckFunctions = array('avail', 'length');
+
+    public function init()
+	{
+		parent::init();
+
+        $aFields = array(
+            'attrs' => array(
+                'type' => 'hidden',
+                'name' => 'attrs',
+                'value' => '',
+                'db' => array (
+                    'pass' => 'Xss',
+                ),
+            ),
+            'attrs_min' => array(
+                'type' => 'text',
+                'name' => 'attrs_min',
+                'caption' => _t('_adm_form_txt_field_attrs_min'),
+                'info' => _t('_adm_form_dsc_field_attrs_min'),
+                'value' => '1',
+                'required' => '0',
+                'db' => array (
+                    'pass' => 'Int',
+                )
+            ),
+            'attrs_max' => array(
+                'type' => 'text',
+                'name' => 'attrs_max',
+                'caption' => _t('_adm_form_txt_field_attrs_max'),
+                'info' => _t('_adm_form_dsc_field_attrs_max'),
+                'value' => '100',
+                'required' => '0',
+                'db' => array (
+                    'pass' => 'Int',
+                )
+            ),
+            'attrs_step' => array(
+                'type' => 'text',
+                'name' => 'attrs_step',
+                'caption' => _t('_adm_form_txt_field_attrs_step'),
+                'info' => _t('_adm_form_dsc_field_attrs_step'),
+                'value' => '1',
+                'required' => '0',
+                'db' => array (
+                    'pass' => 'Int',
+                )
+            ),
+        );
+        $this->aForm['inputs'] = $this->addInArray($this->aForm['inputs'], 'value', $aFields);
+        unset($this->aForm['inputs']['unique']);
+    }
+}
+
+class BxBaseStudioFormsFieldDoublerange extends BxBaseStudioFormsFieldSlider
+{
+    protected $sType = 'doublerange';
+    protected $aCheckFunctions = array('avail', 'length');
+    protected $sDbPass = 'Xss';
+
+    public function init()
+	{
+		parent::init();
+
+        $this->aParams['table_field_type'] = 'varchar(255)';
+
+        $this->aForm['inputs']['value']['db']['pass'] = 'Xss';
+        $this->aForm['inputs']['value']['checker'] = array (
+            'func' => 'preg',
+            'params' => array('/^(\d+-\d+)?$/'),
+            'error' => _t('_adm_form_err_field_value_doublerange'),
+        );
+    }
+}
+
+class BxBaseStudioFormsFieldHidden extends BxBaseStudioFormsFieldText
+{
+    protected $sType = 'hidden';
+    protected $aCheckFunctions = array('avail', 'length', 'preg', 'date', 'date_time', 'email');
+    protected $sDbPass = '';
+
+    public function init()
+	{
+		parent::init();
+
+        unset(
+            $this->aForm['inputs']['caption'],
+            $this->aForm['inputs']['info'],
+            $this->aForm['inputs']['required'],
+            $this->aForm['inputs']['unique'],
+            $this->aForm['inputs']['checker_func'],
+            $this->aForm['inputs']['checker_params'],
+            $this->aForm['inputs']['checker_params_length_min'],
+            $this->aForm['inputs']['checker_params_length_max'],
+            $this->aForm['inputs']['checker_params_required'],
+            $this->aForm['inputs']['checker_params_preg'],
+            $this->aForm['inputs']['checker_error']
+        );
+    }
+}
+
+class BxBaseStudioFormsFieldButton extends BxBaseStudioFormsFieldText
+{
+    protected $sType = 'button';
+    protected $sDbPass = '';
+
+    public function init()
+	{
+		parent::init();
+
+        $this->aParams['table_alter'] = false;
+
+        $this->aForm['inputs']['value']['type'] = 'text_translatable';
+        $this->aForm['inputs']['value']['caption'] = _t('_adm_form_txt_field_value_button');
+        $this->aForm['inputs']['value']['info'] = _t('_adm_form_dsc_field_value_button');
+        $this->aForm['inputs']['value']['value'] = '_sys_form_txt_field';
+
+        unset(
+            $this->aForm['inputs']['caption'],
+            $this->aForm['inputs']['info'],
+            $this->aForm['inputs']['required'],
+            $this->aForm['inputs']['unique'],
+            $this->aForm['inputs']['checker_func'],
+            $this->aForm['inputs']['checker_params'],
+            $this->aForm['inputs']['checker_params_length_min'],
+            $this->aForm['inputs']['checker_params_length_max'],
+            $this->aForm['inputs']['checker_params_required'],
+            $this->aForm['inputs']['checker_params_preg'],
+            $this->aForm['inputs']['checker_error']
+        );
+    }
+}
+
+class BxBaseStudioFormsFieldReset extends BxBaseStudioFormsFieldButton
+{
+    protected $sType = 'reset';
+
+    public function init()
+	{
+		parent::init();
+
+        $this->aForm['inputs']['value']['info'] = _t('_adm_form_dsc_field_value_reset');
+    }
+}
+
+class BxBaseStudioFormsFieldSubmit extends BxBaseStudioFormsFieldButton
+{
+    protected $sType = 'submit';
+
+    public function init()
+	{
+		parent::init();
+
+        $this->aForm['inputs']['value']['info'] = _t('_adm_form_dsc_field_value_submit');
+    }
+}
+
+class BxBaseStudioFormsFieldImage extends BxBaseStudioFormsFieldButton
+{
+    protected $sType = 'image';
+
+    public function init()
+	{
+		parent::init();
+
+        $aFields = array(
+            'attrs' => array(
+                'type' => 'hidden',
+                'name' => 'attrs',
+                'value' => '',
+                'db' => array (
+                    'pass' => 'Xss',
+                ),
+            ),
+            'attrs_src' => array(
+                'type' => 'text',
+                'name' => 'attrs_src',
+                'caption' => _t('_adm_form_txt_field_attrs_src'),
+                'info' => _t('_adm_form_dsc_field_attrs_src'),
+                'value' => '',
+                'required' => '0',
+                'db' => array (
+                    'pass' => 'Xss',
+                )
+            ),
+        );
+        $this->aForm['inputs'] = $this->addInArray($this->aForm['inputs'], 'value', $aFields);
+
+        unset(
+            $this->aForm['inputs']['value']
+        );
+    }
+}
+
+class BxBaseStudioFormsFieldSelect extends BxBaseStudioFormsFieldText
+{
+    protected $sType = 'select';
+    protected $aCheckFunctions = array('avail', 'length', 'preg');
+
+    public function init()
+	{
+		parent::init();
+
+        $this->aForm['inputs']['value']['type'] = 'select';
+        $this->aForm['inputs']['value']['values'] = array(
+            array('key' => '', 'value' => _t('_adm_form_txt_field_value_select_value'))
+        );
+        $this->aForm['inputs']['value']['attrs'] = array(
+            'id' => 'adm-form-field-add-value'
+        );
+
+        $aFields = array(
+            'values' => array(
+                'type' => 'select',
+                'name' => 'values',
+                'caption' => _t('_adm_form_txt_field_values'),
+                'info' => '',
+                'value' => '',
+                'values' => array(
+                    array('key' => '', 'value' => _t('_adm_form_txt_field_values_select_list'))
+                ),
+                'required' => '1',
+                'attrs' => array(
+                    'onChange' => $this->getJsObject() . ".onChangeValues(0, this)"
+                ),
+                'db' => array (
+                    'pass' => 'Xss',
+                ),
+                'checker' => array (
+                    'func' => 'avail',
+                    'params' => array(),
+                    'error' => _t('_adm_form_err_field_values'),
+                ),
+            )
+        );
+
+        $aLists = array();
+        $this->oDb->getLists(array('type' => 'all'), $aLists, false);
+        foreach($aLists as $aList)
+            $aFields['values']['values'][] = array('key' => BX_DATA_LISTS_KEY_PREFIX . $aList['key'], 'value' => _t($aList['title']));
+
+        $this->aForm['inputs'] = $this->addInArray($this->aForm['inputs'], 'caption', $aFields);
+        unset($this->aForm['inputs']['unique']);
+    }
+}
+
+class BxBaseStudioFormsFieldRadioSet extends BxBaseStudioFormsFieldSelect
+{
+    protected $sType = 'radio_set';
+    protected $aCheckFunctions = array('avail', 'length', 'preg');
+}
+
+class BxBaseStudioFormsFieldSelectMultiple extends BxBaseStudioFormsFieldSelect
+{
+    protected $sType = 'select_multiple';
+    protected $aCheckFunctions = array('avail', 'length', 'preg');
+    protected $sDbPass = 'Set';
+
+    public function init()
+	{
+		parent::init();
+
+        $this->aParams['table_field_type'] = 'bigint(20)';
+
+        $this->aForm['inputs']['value']['type'] = 'select_multiple';
+        $this->aForm['inputs']['value']['values'] = array();
+        $this->aForm['inputs']['value']['db']['pass'] = 'Set';
+        $this->aForm['inputs']['values']['values'] = array(
+            array('key' => '', 'value' => _t('_adm_form_txt_field_values_select_list'))
+        );
+        $this->aForm['inputs']['values']['attrs']['onChange'] = $this->getJsObject() . ".onChangeValues(1, this)";
+
+        $aLists = array();
+        $this->oDb->getLists(array('type' => 'all_for_sets'), $aLists, false);
+        foreach($aLists as $aList)
+            $this->aForm['inputs']['values']['values'][] = array('key' => BX_DATA_LISTS_KEY_PREFIX . $aList['key'], 'value' => _t($aList['title']));
+    }
+}
+
+class BxBaseStudioFormsFieldCheckboxSet extends BxBaseStudioFormsFieldSelectMultiple
+{
+    protected $sType = 'checkbox_set';
+    protected $aCheckFunctions = array('avail', 'length', 'preg');
+}
+
+class BxBaseStudioFormsFieldCustom extends BxBaseStudioFormsFieldText
+{
+    protected $sType = 'custom';
+    protected $sDbPass = '';
+
+	public function init()
+	{
+		parent::init();
+
+        unset(
+            $this->aForm['inputs']['required'],
+            $this->aForm['inputs']['unique'],
+            $this->aForm['inputs']['checker_func'],
+            $this->aForm['inputs']['checker_params'],
+            $this->aForm['inputs']['checker_params_length_min'],
+            $this->aForm['inputs']['checker_params_length_max'],
+            $this->aForm['inputs']['checker_params_required'], 
+            $this->aForm['inputs']['checker_params_preg'],
+            $this->aForm['inputs']['checker_error']
+        );
+    }
+}
+
+class BxBaseStudioFormsFieldInputSet extends BxBaseStudioFormsFieldCustom
+{
+    protected $sType = 'input_set';
+
+    public function init()
+	{
+		parent::init();
+
+        $this->aParams['table_alter'] = false;
+
+        $aFields = array(
+            'values' => array(
+                'type' => 'value',
+                'name' => 'values',
+                'caption' => _t('_adm_form_txt_field_values'),
+                'value' => ''
+            )
+        );
+        $this->aForm['inputs'] = $this->addInArray($this->aForm['inputs'], 'value', $aFields);
+
+        unset(
+            $this->aForm['inputs']['value']
+        );
+    }
+}
+
+class BxBaseStudioFormsFieldCaptcha extends BxBaseStudioFormsFieldText
+{
+    protected $sType = 'captcha';
+    protected $aCheckFunctions = array('captcha');
+    protected $sDbPass = '';
+
+    public function init()
+    {
+        parent::init();
+
+        $this->aParams['table_alter'] = false;
+
+        $this->aForm['inputs']['required'] = array(
+            'type' => 'hidden',
+            'name' => 'required',
+            'value' => '1',
+            'db' => array (
+                'pass' => 'Int',
+            )
+        );
+
+        $this->aForm['inputs']['checker_func']['type'] = 'hidden';
+        $this->aForm['inputs']['checker_func']['value'] = 'captcha';
+        $this->aForm['inputs']['checker_error']['info'] = _t('_adm_form_dsc_field_checker_error_captcha');
+
+        unset(
+            $this->aForm['inputs']['value'],
+            $this->aForm['inputs']['unique'],
+            $this->aForm['inputs']['checker_func']['tr_attrs']['style'], $this->aForm['inputs']['checker_error']['tr_attrs']['style'],
+            $this->aForm['inputs']['checker_func']['attrs']['disabled'], $this->aForm['inputs']['checker_error']['attrs']['disabled']
+        );
+    }
+}
+
+class BxBaseStudioFormsFieldLocation extends BxBaseStudioFormsFieldText
+{
+    protected $sType = 'location';
+    protected $aCheckFunctions = array('location');
+    protected $sDbPass = '';
+
+    public function init()
+    {
+        parent::init();
+
+        $this->aParams['table_alter'] = false;
+
+        unset(
+            $this->aForm['inputs']['unique']
+        );
+    }
+}
+/** @} */

+ 144 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/studio/template/scripts_templ/BxTemplStudioFormsField.php

@@ -0,0 +1,144 @@
+<?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 BxTemplStudioFormsField extends BxBaseStudioFormsField
+{
+}
+
+class BxTemplStudioFormsFieldBlockHeader extends BxBaseStudioFormsFieldBlockHeader
+{
+}
+
+class BxTemplStudioFormsFieldBlockEnd extends BxBaseStudioFormsFieldBlockEnd
+{
+}
+
+class BxTemplStudioFormsFieldValue extends BxBaseStudioFormsFieldValue
+{
+}
+
+class BxTemplStudioFormsFieldText extends BxBaseStudioFormsFieldText
+{
+}
+
+class BxTemplStudioFormsFieldPassword extends BxBaseStudioFormsFieldPassword
+{
+}
+
+class BxTemplStudioFormsFieldTextarea extends BxBaseStudioFormsFieldTextarea
+{
+}
+
+class BxTemplStudioFormsFieldNumber extends BxBaseStudioFormsFieldNumber
+{
+}
+
+class BxTemplStudioFormsFieldPrice extends BxBaseStudioFormsFieldPrice
+{
+}
+
+class BxTemplStudioFormsFieldNestedForm extends BxBaseStudioFormsFieldNestedForm
+{
+}
+
+class BxTemplStudioFormsFieldTime extends BxBaseStudioFormsFieldTime
+{
+}
+
+class BxTemplStudioFormsFieldDatepicker extends BxBaseStudioFormsFieldDatepicker
+{
+}
+
+class BxTemplStudioFormsFieldDateselect extends BxBaseStudioFormsFieldDateselect
+{
+}
+
+class BxTemplStudioFormsFieldDatetime extends BxBaseStudioFormsFieldDatetime
+{
+}
+
+class BxTemplStudioFormsFieldCheckbox extends BxBaseStudioFormsFieldCheckbox
+{
+}
+
+class BxTemplStudioFormsFieldSwitcher extends BxBaseStudioFormsFieldSwitcher
+{
+}
+
+class BxTemplStudioFormsFieldFile extends BxBaseStudioFormsFieldFile
+{
+}
+
+class BxTemplStudioFormsFieldFiles extends BxBaseStudioFormsFieldFiles
+{
+}
+
+class BxTemplStudioFormsFieldSlider extends BxBaseStudioFormsFieldSlider
+{
+}
+
+class BxTemplStudioFormsFieldDoublerange extends BxBaseStudioFormsFieldDoublerange
+{
+}
+
+class BxTemplStudioFormsFieldHidden extends BxBaseStudioFormsFieldHidden
+{
+}
+
+class BxTemplStudioFormsFieldButton extends BxBaseStudioFormsFieldButton
+{
+}
+
+class BxTemplStudioFormsFieldReset extends BxBaseStudioFormsFieldReset
+{
+}
+
+class BxTemplStudioFormsFieldSubmit extends BxBaseStudioFormsFieldSubmit
+{
+}
+
+class BxTemplStudioFormsFieldImage extends BxBaseStudioFormsFieldImage
+{
+}
+
+class BxTemplStudioFormsFieldSelect extends BxBaseStudioFormsFieldSelect
+{
+}
+
+class BxTemplStudioFormsFieldSelectMultiple extends BxBaseStudioFormsFieldSelectMultiple
+{
+}
+
+class BxTemplStudioFormsFieldCheckboxSet extends BxBaseStudioFormsFieldCheckboxSet
+{
+}
+
+class BxTemplStudioFormsFieldRadioSet extends BxBaseStudioFormsFieldRadioSet
+{
+}
+
+class BxTemplStudioFormsFieldCustom extends BxBaseStudioFormsFieldCustom
+{
+}
+
+class BxTemplStudioFormsFieldInputSet extends BxBaseStudioFormsFieldInputSet
+{
+}
+
+class BxTemplStudioFormsFieldCaptcha extends BxBaseStudioFormsFieldCaptcha
+{
+}
+
+class BxTemplStudioFormsFieldLocation extends BxBaseStudioFormsFieldLocation
+{
+}
+
+/** @} */

+ 36 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/_page_toolbar.html

@@ -0,0 +1,36 @@
+<bx_injection:top />
+<div id="bx-toolbar" class="bx-header bx-def-z-index-nav fixed top-0 w-full ring-1 ring-gray-300 dark:ring-gray-700 bg-white/95 dark:bg-gray-800/95 backdrop-blur shadow">
+    <div class="bx-header-cnt __sys_header_width__ mx-auto px-3 md:px-4 lg:px-6 box-border">
+        <div class="bx-toolbar-content-wrapper">
+            <div id="bx-toolbar-content" class="bx-toolbar-content relative flex items-center justify-start h-16">
+                <!-- Mobile: Hamburger -->
+                <div class="bx-toolbar-item bx-ti-hamburger flex-none items-center lg:hidden ltr:mr-4 rtl:ml-4">
+                    <bx_menu:sys_toolbar_site />
+                </div>
+
+                <div class="bx-toolbar-items-group relative flex flex-1 items-center justify-start h-16">
+                    <!-- Logo -->
+                    <div id="bx-logo-container" class="bx-toolbar-item bx-ti-left bx-ti-logo flex-none items-center">
+                        <bx_injection:injection_logo_before />
+                        __main_logo__
+                        <bx_injection:injection_logo_after />
+                    </div>
+                    <!-- Menu -->
+                    <div class="bx-toolbar-item bx-ti-menu flex-2 hidden lg:block w-64 ltr:ml-4 rtl:mr-4">
+                        <div id="bx-menu-toolbar-1-container" class="bx-toolbar-item bx-ti-menu w-full">
+                            <bx_menu:sys_site />
+                        </div>
+                    </div>
+                    <!-- Search -->
+                    <div class="bx-toolbar-item bx-ti-search flex-1 lg:flex-none flex items-center justify-end">
+                        __sys_toolbar_search__
+                    </div>
+                </div>
+                <!-- Members Toolbar -->
+                <div id="bx-menu-toolbar-2-container" class="bx-toolbar-item bx-ti-right flex-none">
+                    <bx_menu:sys_toolbar_member />
+                </div>
+            </div>
+        </div>
+    </div>
+</div>

+ 12 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/_page_toolbar_search.html

@@ -0,0 +1,12 @@
+<div class="bx-ti-search-box flex-auto flex items-start md:min-w-88 ltr:mr-2 rtl:ml-2 bg-white/95 dark:bg-gray-800/95 rounded-md shadow-md hidden">
+    <div class="relative w-full">__sys_site_search__</div>
+</div>
+<div class="bx-ti-search-button group relative inline-flex items-center mx-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 text-base font-medium leading-6 border border-transparent hover:border-gray-200/50 active:border-gray-300 dark:hover:border-gray-700/50 dark:active:border-gray-700 bg-gray-100 dark:bg-gray-700/50 active:bg-gray-300 dark:active:bg-black/50 hover:bg-gray-200/50 dark:hover:bg-gray-700/50 rounded-full">
+    <a href="javascript:void(0)" onclick="javascript:bx_site_search_show(this)">
+        <span class="relative flex items-center justify-center h-10 w-10">
+            <div class="bx-icon">
+                <bx_image_auto:search />
+            </div>
+        </span>
+    </a>
+</div>

+ 12 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/block_async_create_post.html

@@ -0,0 +1,12 @@
+<div class="flex items-center p-4">
+    <div class="flex-0 h-10 w-10 bg-gray-200 dark:bg-gray-700 rounded-full">&nbsp;</div>
+    <div class="flex-1 ltr:ml-4 rtl:mr-4 space-y-2 animate-pulse">
+        <div class="h-8 w-full bg-gray-300 dark:bg-gray-600 rounded-md">&nbsp;</div>
+    </div>
+</div>
+<script>
+    var iBlockID = '__id__';
+    $(document).ready(function () {
+        getHtmlData($('#bx-page-block-' + iBlockID), bx_append_url_params(document.location.href, 'dynamic=tab&pageBlock=' + iBlockID), onAsyncBlockLoad, 'post', false, {includedCss: JSON.stringify(aIncludedCss), includedJs: JSON.stringify(aIncludedJs)}, false);
+    });
+</script>

+ 24 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/block_async_profile_units.html

@@ -0,0 +1,24 @@
+<div class="bx-cont-ph-profile-unit relative flex space-x-6 rtl:space-x-reverse">
+    <div class="flex-1 mb-4 ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800 rounded-md">
+        <div class="bx-base-pofile-unit-cover flex align-items justify-center p-4 bg-gray-100 dark:bg-gray-600 rounded-t-md">
+            <div class="flex-0 h-24 w-24 bg-gray-200 dark:bg-gray-700 rounded-full">&nbsp;</div>
+        </div>
+        <div class="p-2 animate-pulse">
+            <div class="flex justify-center">
+                <div class="h-5 w-1/2 bg-gray-300 dark:bg-gray-600 rounded-full">&nbsp;</div>
+            </div>
+            <div class="flex mt-2 space-x-1 rtl:space-x-reverse">
+                <div class="h-3 w-1/2 bg-gray-200 dark:bg-gray-700 rounded-xl"></div>
+                <div class="h-3 w-1/2 bg-gray-200 dark:bg-gray-700 rounded-xl"></div>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    var iBlockID = '__id__';
+    var sTmpl = $('#bx-page-block-' + iBlockID + ' .bx-cont-ph-profile-unit').html();
+    $('#bx-page-block-' + iBlockID + ' .bx-cont-ph-profile-unit').append(sTmpl + sTmpl);
+    $(document).ready(function () {
+        getHtmlData($('#bx-page-block-' + iBlockID), bx_append_url_params(document.location.href, 'dynamic=tab&pageBlock=' + iBlockID), onAsyncBlockLoad, 'post', false, {includedCss: JSON.stringify(aIncludedCss), includedJs: JSON.stringify(aIncludedJs)}, false);
+    });
+</script>

+ 32 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/block_async_text_units_gallery.html

@@ -0,0 +1,32 @@
+<div class="bx-cont-ph-profile-unit relative flex space-x-6 rtl:space-x-reverse">
+    <div class="flex-1 mb-4 ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800 rounded-md">
+        <div class="h-48 m-1 mb-0 bg-gray-200 dark:bg-gray-700 rounded animate-pulse">&nbsp;</div>
+        <div class="p-4 space-y-4 animate-pulse">
+            <div class="h-3 w-1/3 bg-gray-200 dark:bg-gray-700 rounded-xl"></div>
+            <div class="space-y-2">
+                <div class="flex space-x-1 rtl:space-x-reverse">
+                    <div class="h-5 w-1/2 bg-gray-300 dark:bg-gray-600 rounded-xl"></div>
+                    <div class="h-5 w-1/2 bg-gray-300 dark:bg-gray-600 rounded-xl"></div>
+                </div>
+                <div class="h-5 w-2/3 bg-gray-300 dark:bg-gray-600 rounded-xl"></div>
+            </div>
+            <div class="flex space-x-1 rtl:space-x-reverse">
+                <div class="h-3 w-1/2 bg-gray-200 dark:bg-gray-700 rounded-xl"></div>
+                <div class="h-3 w-1/2 bg-gray-200 dark:bg-gray-700 rounded-xl"></div>
+            </div>
+            <div class="flex space-x-1 rtl:space-x-reverse">
+                <div class="h-3 w-1/3 bg-gray-200 dark:bg-gray-700 rounded-xl"></div>
+                <div class="h-3 w-2/3 bg-gray-200 dark:bg-gray-700 rounded-xl"></div>
+            </div>
+            <div class="bg-gray-200 dark:bg-gray-700 h-3 w-2/3 rounded-xl"></div>
+        </div>
+    </div>
+</div>
+<script>
+    var iBlockID = '__id__';
+    var sTmpl = $('#bx-page-block-' + iBlockID + ' .bx-cont-ph-profile-unit').html();
+    $('#bx-page-block-' + iBlockID + ' .bx-cont-ph-profile-unit').append(sTmpl + sTmpl);
+    $(document).ready(function () {
+        getHtmlData($('#bx-page-block-' + iBlockID), bx_append_url_params(document.location.href, 'dynamic=tab&pageBlock=' + iBlockID), onAsyncBlockLoad, 'post', false, {includedCss: JSON.stringify(aIncludedCss), includedJs: JSON.stringify(aIncludedJs)}, false);
+    });
+</script>

+ 32 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/block_async_text_units_list.html

@@ -0,0 +1,32 @@
+<div class="bx-base-text-units flex flex-col">
+    <div class="flex-1 mb-4 ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800 rounded-md">
+        <div class="h-48 m-1 mb-0 bg-gray-200 dark:bg-gray-700 rounded animate-pulse">&nbsp;</div>
+        <div class="p-4 space-y-4 animate-pulse">
+            <div class="h-3 w-1/3 bg-gray-200 dark:bg-gray-700 rounded-xl"></div>
+            <div class="space-y-2">
+                <div class="flex space-x-1 rtl:space-x-reverse">
+                    <div class="h-5 w-1/2 bg-gray-300 dark:bg-gray-600 rounded-xl"></div>
+                    <div class="h-5 w-1/2 bg-gray-300 dark:bg-gray-600 rounded-xl"></div>
+                </div>
+                <div class="h-5 w-2/3 bg-gray-300 dark:bg-gray-600 rounded-xl"></div>
+            </div>
+            <div class="flex space-x-1 rtl:space-x-reverse">
+                <div class="h-3 w-1/2 bg-gray-200 dark:bg-gray-700 rounded-xl"></div>
+                <div class="h-3 w-1/2 bg-gray-200 dark:bg-gray-700 rounded-xl"></div>
+            </div>
+            <div class="flex space-x-1 rtl:space-x-reverse">
+                <div class="h-3 w-1/3 bg-gray-200 dark:bg-gray-700 rounded-xl"></div>
+                <div class="h-3 w-2/3 bg-gray-200 dark:bg-gray-700 rounded-xl"></div>
+            </div>
+            <div class="bg-gray-200 dark:bg-gray-700 h-3 w-2/3 rounded-xl"></div>
+        </div>
+    </div>
+</div>
+<script>
+    var iBlockID = '__id__';
+    var sTmpl = $('#bx-page-block-' + iBlockID + ' .bx-base-text-units').html();
+    $('#bx-page-block-' + iBlockID + ' .bx-base-text-units').append(sTmpl + sTmpl);
+    $(document).ready(function () {
+        getHtmlData($('#bx-page-block-' + iBlockID), bx_append_url_params(document.location.href, 'dynamic=tab&pageBlock=' + iBlockID), onAsyncBlockLoad, 'post', false, {includedCss: JSON.stringify(aIncludedCss), includedJs: JSON.stringify(aIncludedJs)}, false);
+    });
+</script>

+ 27 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/comment.html

@@ -0,0 +1,27 @@
+<li id="cmt__id__" class="__style_prefix____class__ mb-4">
+    <a id="__anchor__" class="__style_prefix__-anchor"></a>
+    <div class="__style_prefix__-cnt__class_cnt__ flex items-start">
+        <div class="__style_prefix__-blink-plate"></div>
+        <div class="__style_prefix__-avatar bx-def-thumb-size flex-none">__author_unit__</div>
+        <div class="__style_prefix__-info bx-def-thumb-size-min-height flex-auto min-w-0 ltr:pl-2 ltr:lg:pl-4 rtl:pr-2 rtl:lg:pr-4">
+            <bx_if:show_notes>
+                <div class="__style_prefix__-notes mb-1">
+                    <bx_repeat:notes>
+                        <div class="__style_prefix__-note mt-1 first:mt-0 text-sm text-gray-500 dark:text-gray-400">
+                            <div class="__style_prefix__-n-text<bx_if:show_note_color> col-__item_note_color__</bx_if:show_note_color>">__item_note__</div>
+                        </div>
+                    </bx_repeat:notes>
+                </div>
+            </bx_if:show_notes>
+            <div class="__style_prefix__-head flex">
+                <bx_if:meta><span class="__style_prefix__-meta bx-base-general-unit-meta">__meta__</span></bx_if:meta>
+                <bx_if:show_pinned><span class="__style_prefix__-pinned ml-2"><i class="sys-icon thumbtack" title="<bx_text:_cmt_txt_pinned />"></i></span></bx_if:show_pinned>
+            </div>
+            <div class="__style_prefix__-cont mt-2">
+                <div class="__style_prefix__-cont-cnt">__content__</div>
+            </div>
+            __actions__
+        </div>
+    </div>
+    <ul class="cmts cmts-all cmts-margin">__replies__</ul>
+</li>

+ 1 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/comment_do_comment_label.html

@@ -0,0 +1 @@
+<span class="sys-action-do __style_prefix__-do bx-def-valign-center"><bx_if:show_icon><span class="sys-action-do-icon __style_prefix__-do-icon bx-def-align-center">__icon__</span></bx_if:show_icon><bx_if:show_text><span class="sys-action-do-text __style_prefix__-do-text ltr:ml-2 ltr:first:ml-0 rtl:mr-2 rtl:first:mr-0">__text__</span></bx_if:show_text></span>

+ 13 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/comment_reply_box.html

@@ -0,0 +1,13 @@
+<div class="__style_prefix__-reply__class__ mb-4">
+    <div class="__style_prefix__-cont __style_prefix__-post-reply">
+        <div class="__style_prefix__-cont-cnt">
+            <bx_if:show_form_min>
+                <div class="__style_prefix__-body-min__class_body_min__ flex items-center">
+                    <div class="__style_prefix__-bm-user">__author_unit__</div>
+                    <div class="__style_prefix__-bm-content w-full ltr:pl-2 ltr:lg:pl-4 rtl:pr-2 rtl:lg:pr-4 text-sm text-gray-500 dark:text-gray-400 box-border">__placeholder__</div>
+                </div>
+            </bx_if:show_form_min>
+            <div class="__style_prefix__-body__class_body__">__form__</div>
+        </div>
+    </div>
+</div>

+ 17 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/connection_counter_label.html

@@ -0,0 +1,17 @@
+<span class="__style_prefix__-counter-label flex items-center">
+    <bx_if:show_icon>
+        <span class="sys-action-counter-icon __style_prefix__-counter-icon relative flex items-center justify-center ring-1  h-6 w-6 rounded-full ring-white dark:ring-gray-800 h-6 w-6 bg-gray-200 dark:bg-gray-800">
+            __name__
+        </span>
+    </bx_if:show_icon>
+    <bx_if:show_profiles>
+        <span class="sys-action-counter-profiles __style_prefix__-counter-profiles flex">
+            <bx_repeat:profiles>
+                <span class="sys-action-counter-profile __style_prefix__-counter-profile block ltr:-ml-2 ltr:first:ml-0 rtl:-mr-2 rtl:first:mr-0">__unit__</span>
+            </bx_repeat:profiles>
+        </span>
+    </bx_if:show_profiles>
+    <bx_if:show_text>
+        <span class="sys-action-counter-text __style_prefix__-counter-text ltr:ml-1 rtl:mr-1">__text__</span>
+    </bx_if:show_text>
+</span>

+ 27 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/create_post_form.html

@@ -0,0 +1,27 @@
+<div id="sys-create-post-form" class="sys-create-post-form relative">
+    <div class="sys-cpf-cnt">
+        <div id="sys-cpf-form">
+            <div class="sys-cpf-form sys-cpf-loading bx-def-bc-margin" style="display:none">
+                <div class="flex items-center p-4">
+                    <div class="flex-0 h-10 w-10 bg-gray-200 dark:bg-gray-700 rounded-full">&nbsp;</div>
+                    <div class="flex-1 ml-4 space-y-2 animate-pulse">
+                        <div class="h-8 w-full bg-gray-300 dark:bg-gray-600 rounded-md">&nbsp;</div>
+                    </div>
+                </div>
+            </div>
+            <div class="sys-cpf-form sys-cpf-default flex items-center p-4 bg-gray-50 hover:bg-white dark:bg-gray-900/50 dark:hover:bg-gray-900 shadow hover:shadow-md ring-1 ring-gray-300 dark:ring-gray-700 rounded-md cursor-pointer" onclick="__js_object__.getForm('__default__')">
+                <div class="sys-cpf-user">__user_thumb__</div>
+                <div class="sys-cpf-content flex text-base ltr:ml-4 rtl:mr-4 text-gray-500 dark:text-gray-400">
+                    <div>__placeholder__</div>
+                </div>
+            </div>
+            <div class="sys-cpf-form sys-cpf-__default__" style="display:none">__form__</div>
+            <div class="sys-cpf-close absolute hidden right-0 bottom-0">
+                <a class="bx-btn" href="javascript:void(0)" onclick="__js_object__.hideForm(this)">
+                    <bx_image_auto:times />
+                </a>
+            </div>
+        </div>
+    </div>
+</div>
+__js_content__

+ 104 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/css/cmts.css

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

+ 969 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/css/common.css

@@ -0,0 +1,969 @@
+* {
+    -webkit-font-smoothing: antialiased;
+    -webkit-text-size-adjust: 100%;
+    -moz-text-size-adjust: 100%;
+    -ms-text-size-adjust: 100%;
+    text-size-adjust: 100%;
+}
+
+a {
+    text-decoration:none;
+    outline:none;
+    color:#3e8685; /* 26.13.13: prev val #387877; */
+}
+
+a:hover {
+    text-decoration:none;
+}
+
+a > img {
+    border:none;
+}
+
+html {
+    height:100%;
+}
+/*-- Tailwind overwrite --*/
+html.hidden {
+    display: block !important;
+}
+body {
+	min-height: 100%;
+    min-height: 100vh;
+    padding: 0;
+    margin: 0;
+}
+
+blockquote {
+    /* NOTE: this style was copied to editor.css */
+    border-left: 2px solid #ccc;
+    padding-left: 10px;
+}
+
+.bx-tl-content pre,
+.bx-base-text-entity-content pre,
+.bx-content-description pre,
+.cmt-body pre {
+    /* NOTE: this style was copied to editor.css */
+    padding: 5px;
+    border: 1px solid green;
+    background-color: white;
+    color: #009900;
+    display: block;
+    margin: 0;
+    overflow-x: auto;
+}
+
+.bx-main {
+    position: relative;
+    display: -webkit-flex;
+    display: flex;
+    -webkit-flex-direction: column;
+    flex-direction: column;
+    min-height: 100%;
+    min-height: 100vh;
+}
+
+/*-- Application mode: Sidebar --*/
+.bx-main.bx-pt-application .bx-main-column .bx-menu-panel .bx-def-thumb-size {
+    width: 2rem;
+    height: 2rem;
+}
+.bx-main.bx-pt-application .bx-main-column.narrow.bx-mc-narrowed .bx-menu-panel .bx-profile-avatar-cnt {
+    -webkit-justify-content:center;
+    justify-content:center;
+}
+.bx-main.bx-pt-application .bx-main-column.narrow.bx-mc-narrowed .bx-mc-cnt {
+    padding-left: 0;
+    padding-right: 0;
+}
+
+/*-- Application mode: Toolbar --*/
+.bx-main.bx-pt-application .bx-toolbar-content {
+    position: relative;
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+}
+.bx-main.bx-pt-application .bx-toolbar-content .bx-toolbar-item.bx-ti-left {
+    -webkit-flex: 0 0 auto;
+    -ms-flex: 0 0 auto;
+    flex: 0 0 auto;
+}
+.bx-main.bx-pt-application .bx-toolbar-content .bx-toolbar-item.bx-ti-left .bx-menu-item-main-menu {
+    display: none;
+}
+.bx-main.bx-pt-application .bx-toolbar-content .bx-toolbar-item.bx-ti-menu {
+    -webkit-flex: 2 1 auto;
+    -ms-flex: 2 1 auto;
+    flex: 2 1 auto;
+}
+.bx-main.bx-pt-application .bx-toolbar-content .bx-toolbar-item.bx-ti-right {
+    -webkit-flex: 0 1 0%;
+    -ms-flex: 0 1 0%;
+    flex: 0 1 0%;
+}
+
+.bx-clear {
+    clear:both;
+    float:none;
+    display:block;
+    width: 0px;
+    height: 0px;
+    font-size:1px;
+}
+
+.bx-clearfix:before,
+.bx-clearfix:after {
+    content:"";
+    display:table;
+}
+.bx-clearfix:after {
+    clear:both;
+}
+
+.bx-anchor-invisible,
+.bx-anchor {
+    -webkit-background-clip:content-box;
+    background-clip:content-box;
+}
+.bx-anchor-invisible {
+    display:block;
+    height:0px;
+    width:0px;
+    overflow:hidden;
+}
+
+.bx-bullet:before {
+    content: "\00A0\00b7\00A0";
+
+    color: #999999;
+}
+.bx-bullet-narrow:before {
+    content: "\00b7";
+
+    color: #999999;
+}
+
+.bx-tag b,
+.bx-tag s {
+    text-decoration: none;
+    font-weight: normal;
+}
+
+.sys-icon-a {
+    width:1.5rem;
+    height:1.5rem;
+}
+
+.sys-icon-emoji{
+    font-weight: normal !important;
+}
+
+.sys-icon-image,
+.sys-icon-svg{
+    display: inline-block;
+    width: 1.5rem;
+    height: 1.5rem;
+    background-size: contain;
+    background-position: center;
+    background-repeat: no-repeat;
+}
+
+.bx-search-site {
+    width:20rem;
+    margin-left:auto;
+    margin-right:auto;
+}
+.bx-search-site .sys-search-results-quick {
+    padding-top: 0.5rem
+}
+.bx-search-site .sys-search-results-quick .bx-btn.sys-agents-ask {
+    display: flex;
+    float: none;
+    
+    width: 100%;
+    margin-bottom: 0.5rem;
+}
+.bx-agents-popup .cmts {
+    max-height: calc(0.5 * 100vh);
+    overflow-y: auto;
+}
+.bx-agents-popup .cmts .cmt .bx-agents-loading {
+    padding: 0;
+    background-color: transparent;
+    border-color: transparent;
+    box-shadow: none;
+}
+.bx-acl-err-msg {
+    padding: 0 10%;
+}
+
+/* --------------- design boxes */
+
+.bx-db-container {
+    position:relative;
+}
+.bx-db-header {
+    display: -webkit-box;
+    display: -webkit-flex;
+    display: -ms-flexbox;
+    display: flex;
+
+    -webkit-box-align: center;
+    -webkit-align-items: center;
+    -ms-flex-align: center;
+    align-items: center;
+
+    -webkit-flex-wrap: nowrap;
+    -ms-flex-wrap: nowrap;
+    flex-wrap: nowrap;
+}
+.bx-db-container.bx-dbc-nopadding > .bx-db-header {
+    padding-left: 0;
+    padding-right: 0;
+}
+.bx-db-container.bx-dbc-nobg > .bx-db-divider {
+    display: none;
+}
+.bx-db-title {
+    -webkit-flex: 1 1 auto;
+    -ms-flex: 1 1 auto;
+    flex: 1 1 auto;
+
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    overflow: hidden;
+
+    line-height: 1.5;
+    font-size: 1.0rem;
+    font-weight: 600;
+}
+
+.bx-db-menu {
+    -webkit-flex: 1 2 auto;
+    -ms-flex: 1 2 auto;
+    flex: 1 2 auto;
+
+    max-width: 75%;
+}
+
+.bx-media-phone .bx-db-menu {
+    max-width: 65%;
+}
+
+.bx-db-menu-tab {
+    margin-left: 5px;
+    padding-top: 1px;
+    padding-bottom: 1px;
+    text-align: right;
+    overflow-x: auto;
+}
+.bx-db-menu-tab.bx-db-menu-tab-more-auto {
+    overflow-x: visible;
+}
+
+.bx-db-menu-tab .bx-menu-custom-hor:not(.bx-menu-more-auto) {
+    width: max-content;
+}
+.bx-db-menu-tab-active {
+    text-decoration:underline;
+}
+
+.bx-db-menu-tab-btn a {
+    text-decoration:none;
+}
+
+.bx-db-menu-tab-btn a.bx-btn {
+    display: inline-block;
+    float: none;
+}
+
+.bx-db-menu .sys-icon-a {
+    vertical-align:middle;
+}
+
+.bx-db-menu-popup {
+    text-align: left;
+}
+
+.bx-db-content {
+    position: relative;
+}
+
+.bx-content-padding-block {
+    padding: 1rem;
+}
+
+.bx-db-content .bx-db-help {
+    position: absolute;
+    top: 0;
+    right: 0;
+    width: 2.0rem;
+    height: 2.0rem;
+    border-radius: 2rem;
+    background-color: rgba(0, 0, 0, 0.1);
+    line-height: 2rem;
+    text-align: center;
+}
+
+.bx-db-help-popup {
+    max-width: 25rem;
+    max-height: 25rem;
+    overflow: auto;
+}
+
+/*--- Design Boxes Menu ---*/
+.bx-db-menu ul li {
+    border-radius: 0;
+
+    white-space: nowrap;
+}
+.bx-db-menu ul.bx-menu-hor,
+.bx-db-menu ul.bx-menu-custom.bx-menu-custom-hor,
+.bx-db-menu ul.bx-menu-custom.bx-menu-custom-hor.bx-menu-more-auto:not(.bx-menu-submenu-more-auto) {
+    display: -webkit-inline-flex;
+    display: inline-flex;
+    -webkit-justify-content: flex-end;
+    justify-content: flex-end;
+}
+.bx-db-menu ul.bx-menu-hor > li,
+.bx-db-menu ul.bx-menu-custom.bx-menu-custom-hor > li {
+    display: block;
+
+    -webkit-flex: 0 1 auto; 
+    -ms-flex:  0 1 auto; 
+    flex:  0 1 auto;
+
+    padding: 0 0.5rem;
+
+    border-bottom: 1px solid transparent;
+}
+.bx-db-menu .bx-menu-custom.bx-menu-custom-hor > li > span {
+    display: block;
+
+    margin: 0;
+    padding: 0;
+}
+.bx-db-menu ul.bx-menu-hor > li a.bx-btn,
+.bx-db-menu ul.bx-menu-custom.bx-menu-custom-hor > li a.bx-btn {
+    display: inline-block;
+    float: none;
+}
+.bx-db-menu ul.bx-menu-hor > li a > span,
+.bx-db-menu ul.bx-menu-custom.bx-menu-custom-hor > li a > span {
+    vertical-align: middle;
+}
+.bx-db-menu ul > li:not(.bx-menu-item-more-auto) a .sys-icon,
+.bx-db-menu ul > li:not(.bx-menu-item-more-auto) a .sys-icon-image {
+    display: none;
+}
+.bx-db-menu ul.bx-menu-submenu-more-auto {
+    line-height: normal;
+}
+.bx-db-menu ul li:not(.bx-menu-item-more-auto) a span,
+.bx-db-menu .bx-menu-item-more-popup ul.bx-menu-custom.bx-menu-submenu-more-auto li:not(.bx-menu-item-more-auto) a span,
+.bx-db-menu .bx-popup .bx-db-menu-popup ul.bx-menu-custom.bx-menu-custom-ver li a > span,
+.bx-db-menu .bx-popup .bx-db-menu-popup ul.bx-menu-ver li a > div > span {
+    margin: 0;
+}
+.bx-db-menu ul.bx-menu-hor li.bx-menu-tab-active,
+.bx-db-menu ul.bx-menu-custom.bx-menu-custom-hor li.bx-menu-tab-active {
+    border-bottom: 0;
+}
+.bx-db-menu ul.bx-menu-custom-ver li.bx-menu-item-more-auto {
+    display: none;
+}
+
+/* --------------- popup boxes */
+
+html.bx-popup-lock, body.bx-popup-lock {
+    overflow: hidden;
+}
+
+body > .bx-popup-wrapper {
+    max-height:100%;
+    overflow-x:hidden;
+    overflow-y:auto;
+    /* -webkit-overflow-scrolling:touch; */
+}
+
+.bx-popup-transitions {
+    -webkit-transition: opacity 0.2s ease-in-out, -webkit-transform 0.2s ease-in-out;
+    transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
+}
+
+.bx-popup-inactive {
+    -webkit-opacity: 0;
+    opacity: 0;
+
+    -webkit-transform: scale(0.75);
+    transform: scale(0.75);
+}
+
+.bx-popup-active {
+    max-height:100%;
+    overflow-y:auto;
+
+    -webkit-opacity: 1;
+    opacity: 1;
+
+    -webkit-transform: scale(1);
+    transform: scale(1);
+
+    -webkit-transition: opacity 0.2s ease-in-out, -webkit-transform 0.2s ease-in-out;
+    transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
+}
+
+.bx-popup-width {
+    width: 500px;
+}
+
+.bx-popup-width-small {
+    width: 14rem;
+}
+
+.bx-popup-border {
+    border:1px solid #383d66;
+}
+
+.bx-popup-color-bg {
+    background-color:#fff;
+}
+
+.bx-popup-fog {
+    position: fixed;
+    top: 0;
+    left: 0;
+    opacity: 0.7;
+    background-color: #ffffff;
+    -webkit-backface-visibility: hidden;
+}
+
+.bx-popup {    
+    overflow:hidden;
+}
+
+.bx-popup-header {
+    position: relative;
+}
+.bx-popup-header-color-bg {
+    background-color: #3b8686;
+}
+
+.bx-popup-header .bx-db-menu-tab-btn {
+    padding-top:16px;
+}
+
+.bx-popup-header h2 {
+    margin:0;
+    text-align:center;
+    color:#FFFFFF;
+}
+
+.bx-popup-box-pointer {
+    display:none;
+    position:relative;
+    top:10px;
+    width:13px;
+    height:7px;
+    overflow:hidden;
+}
+
+.bx-popup-box-pointer > i {
+    display:block;
+    position:absolute;
+    top:-5px;
+    width:0px;
+    height:0px;
+    border-width:6px;
+    border-style:solid;
+    border-top-color:transparent;
+    border-right-color:transparent;
+    border-left-color:transparent;
+}
+
+.bx-popup-image-container {
+    position: relative;
+}
+
+.bx-popup-image-btn {
+    position: absolute;
+    right: 0;
+    top: 0;
+}
+
+.bx-popup-image-wrapper img {
+    display: block;
+}
+
+.bx-popup-element-close:hover,
+.bx-popup-element-close {
+    position: absolute;
+    top: 0;
+    right: 0;
+    text-decoration: none;
+    font-size: 1.75rem !important;
+}
+
+.bx-popup-slide-wrapper {
+    width:100%;
+
+    background-color: rgba(255, 255, 255, 0.9);
+}
+.bx-popup-slide-wrapper li {
+    text-align:right;
+}
+
+.bx-popup-trans-wrapper .bx-popup-close-wrapper {
+    display: -webkit-flex;
+    display: flex;
+    -webkit-justify-content: flex-end;
+    justify-content: flex-end;
+}
+.bx-popup-trans-wrapper .bx-popup-close-wrapper .bx-popup-element-close {
+    position: relative;
+    line-height: 1.2rem;
+}
+.bx-popup-image-wrapper .bx-popup-close-wrapper,
+.bx-popup-image-wrapper .bx-popup-element-close {
+    display:none;
+}
+
+#bx-popup-alert,
+#bx-popup-confirm,
+#bx-popup-prompt {
+	max-width: 500px;
+	max-width: 31.25rem;
+}
+
+.popup_alert_actions .bx-btn,
+.popup_confirm_actions .bx-btn,
+.popup_prompt_actions .bx-btn {
+    float: none;
+}
+
+/* --------------- full screen popup */
+.bx-popup-full-screen.bx-popup-trans-wrapper,
+.bx-popup-full-screen.bx-popup-box-wrapper,
+.bx-popup-full-screen .bx-popup-trans,
+.bx-popup-full-screen .bx-popup-box {
+    position:fixed !important;
+    top:0 !important;
+    left:0 !important;
+    bottom:0 !important;
+    right:0 !important;
+    width:100%;
+    width:100vw;
+    height:100%;
+    height:100vh;
+    overflow:auto;
+}
+
+.bx-popup-full-screen .bx-popup-trans > div,
+.bx-popup-full-screen .bx-popup-box > div {
+    border-radius:0;
+    -moz-border-radius:0;
+    -webkit-border-radius:0;
+}
+
+.bx-popup-full-screen.bx-popup-trans-wrapper .bx-popup-border,
+.bx-popup-full-screen.bx-popup-box-wrapper .bx-popup-border {
+    border:none;
+}
+
+.bx-popup-full-screen.bx-popup-trans-wrapper .bx-popup,
+.bx-popup-full-screen.bx-popup-box-wrapper .bx-popup {
+    -moz-box-shadow:none;
+    -webkit-box-shadow:none;
+    box-shadow:none;
+}
+
+.bx-popup-full-screen .bx-popup-width {
+    width:100%;
+}
+
+.bx-popup-full-screen .bx-popup .bx-popup-close-wrapper {
+    display: flex !important;
+}
+
+/* --------------- notification boxes */
+.bx-popup-inline-holder {
+	padding-top: 70px;
+   	padding-top: 4.375rem;
+}
+.bx-popup-inline {
+	width: 100%;
+}
+.bx-popup-inline .bx-popup {
+	position: relative;
+
+	width: 100%;
+    height: 4.25rem;
+
+    border-width: 1px 0px 1px 0px;
+	border-color: #000000;
+	border-radius: 0px;
+
+	background-color: rgba(85, 85, 85, .9);
+
+	color: #ffffff;
+}
+.bx-popup-inline .bx-popup .bx-popup-content {
+	position: relative;
+	display: flex;
+
+	height: 100%;
+
+	padding: 0px 3.75rem;
+
+	align-items: center; 
+}
+.bx-popup-inline .bx-popup .bx-popup-content > * {
+	max-height: 100%; 
+}
+.bx-popup-inline .bx-popup .bx-popup-actions {
+	position: absolute;
+	display: flex;
+
+	height: 100%;
+	top: 0px;
+
+	align-items: center;
+}
+.bx-popup-inline .bx-popup .bx-pa-left {
+	left: 0px;
+}
+.bx-popup-inline .bx-popup .bx-pa-right {
+	right: 0px;
+}
+.bx-popup-inline .bx-popup .bx-popup-actions a {
+	text-decoration: none;
+	font-size: 1.75rem;
+	color: #9c9c9c;
+}
+
+/* --------------- chain notifications */
+div.bx-popup-chain {
+    position: relative;
+    max-width: 22.5rem;
+}
+div.bx-popup-chain-cnt {
+    position: relative;
+}
+div.bx-popup-chain-item {
+    position: relative;
+    width: 100%;
+    overflow: hidden;
+}
+div.bx-popup-chain-item-actions {
+    position: absolute;
+    top: 0px;
+}
+div.bx-popup-cia-left {
+    left: 0px;
+}
+div.bx-popup-cia-right {
+    right: 0px;
+}
+div.bx-popup-chain-item-actions a {
+    line-height: initial;
+    text-decoration: none;
+}
+div.bx-popup-chain-item-actions .sys-icon {
+	display: inline-block;
+	width: 1.5rem;
+	height: 1.5rem;
+	line-height: 1.5rem;
+	text-align: center;
+}
+div.bx-popup-chain-item .bx-loading-ajax {
+    height: auto;
+}
+
+/* --------------- message box */
+.bx-msg-box-container {
+    padding:0px 5px;
+    text-align:center;
+}
+
+.bx-msg-box {
+    display:inline-block; 
+    margin-right:auto;
+    margin-left:auto;
+
+    text-align:center;
+    font-size:1rem;
+    padding:10px;
+    text-transform:uppercase;
+}
+
+/* --------------- loading indicator */
+
+.bx-loading-ajax {
+    position:absolute;
+    width:100px;
+    height:100px;
+    margin-left:auto;
+    margin-right:auto;
+
+    background-color: transparent;
+
+	border-radius:10px;
+	-moz-border-radius:10px;
+	-webkit-border-radius:10px;
+
+    text-align:center;
+    color:#CCCCCC;
+}
+.bx-loading-ajax.static {
+    position: static;
+}
+.bx-loading-ajax svg {
+    box-sizing: content-box;
+    width: 50px;
+    height: 50px;
+    padding: 25px;  
+}
+
+.bx-loading-ajax-btn {
+    position: relative;
+    display: inline-block;
+    vertical-align: top;
+
+    min-width: 2.0rem;
+
+    margin-left: 5px;
+}
+
+@-moz-document url-prefix() {
+    button.bx-btn .bx-loading-ajax-btn {
+        top: -1px;
+    }
+}
+
+button.bx-btn:focus,
+button.bx-btn:focus .bx-loading-ajax-btn {
+    outline: 0;	
+}
+
+.bx-form-element-wrapper .bx-loading-ajax {
+    width: 4rem;
+    height: 4rem;
+}
+
+
+/* --------------- SVG loading indicator */
+svg .outer, 
+svg .inner{
+	-webkit-transform-origin: 50% 50%;
+   	-moz-transform-origin: 50% 50%;
+    transform-origin: 50% 50%;
+}
+svg .outer{
+	-webkit-animation: clockwise 5s infinite linear;
+	-moz-animation: clockwise 5s infinite linear;
+    animation: clockwise 5s infinite linear;
+    
+
+}
+svg .inner{
+	-webkit-animation: counter-clockwise 2s infinite linear;
+	-moz-animation: counter-clockwise 2s infinite linear;
+    animation: counter-clockwise 2s infinite linear;
+    
+}
+svg .colored .inner .p1 {
+	fill:#2AB191;
+}
+svg .colored .inner .p2,
+svg .colored .outer .p1 {
+	fill:#4990E2;
+}
+svg .colored .outer .p2 {
+	fill:#EE6666;
+}
+svg .gray .inner .p1,
+svg .gray .inner .p2,
+svg .gray .outer .p1,
+svg .gray .outer .p2 {
+	fill:#4A4A4A;
+}
+
+@-webkit-keyframes counter-clockwise {
+    0% { -webkit-transform: rotate(0deg); }
+    100% { -webkit-transform: rotate(360deg); }
+}
+@-webkit-keyframes clockwise {
+    0% { -webkit-transform: rotate(360deg); }
+    100% { -webkit-transform: rotate(0deg); }
+}	
+@-moz-keyframes counter-clockwise {
+    0% { -moz-transform: rotate(0deg); }
+    100% { -moz-transform: rotate(360deg); }
+}
+@-moz-keyframes clockwise {
+    0% { -moz-transform: rotate(360deg); }
+    100% { -moz-transform: rotate(0deg); }
+}
+
+@keyframes counter-clockwise {
+    0% { transform: rotate(0deg); }
+    100% { transform: rotate(360deg); }
+}
+@keyframes clockwise {
+    0% { transform: rotate(360deg); }
+    100% { transform: rotate(0deg); }
+}
+	
+/* --------------- button */
+.bx-btn-height {
+    height: 2.2rem;
+    line-height: 2.2rem;
+}
+.bx-btn-border {
+	border: none;
+}
+.bx-btn:active,
+.bx-btn:hover {	
+    background-color: #79bd9a;
+    text-decoration:none;
+}
+.bx-btn img {
+    width:1.2rem;
+    height:1.2rem;
+    max-width: none;
+
+    background-size: contain;
+    background-repeat: no-repeat;
+    background-position:center center;
+}
+.bx-btn .bx-def-valign-center{
+    height: 100%;
+}
+.bx-btn u {
+    margin-left: 10px;
+    text-decoration: none;
+}
+.bx-btn u:first-child {
+	margin-left: 0px;
+}
+.bx-btn .sys-icon {
+    font-size:1.2em;
+}
+.bx-btn .sys-icon-bigger {
+    font-size:1.4em;
+}
+
+.bx-btn-small-height {
+    height: 1.6rem;
+    line-height: 1.5rem;
+}
+
+.bx-btn-small-border {
+	border: none;
+}
+
+.bx-btn.bx-btn-primary {
+    background-color: #357877;
+}
+.bx-btn.bx-btn-primary:hover,
+.bx-btn.bx-btn-primary:active { 
+    background-color: #3a8686;
+}
+
+.bx-btn.bx-btn-symbol {
+    font-family: "Times New Roman", Georgia, Serif;
+    font-size:1.3rem;
+    line-height:32px;
+}
+.bx-btn.bx-btn-symbol:hover {
+    text-decoration:none;
+}
+.bx-btn.bx-btn-symbol-small {
+    font-size:1rem;
+}
+
+.bx-btn-group .bx-btn {
+    margin-left:0px;
+    margin-right:0px;
+}
+.bx-btn-group .bx-btn:not(:first-child):after {
+    content:"";
+    position:absolute;
+    display:block;
+    top:0px;
+    left:-0.05em;
+    width:0.1em;
+    height:100%;
+    background-color:rgba(255,255,255,0.1);
+}
+.bx-btn-group .bx-btn + .bx-btn:not(:last-child) {
+	border-radius:0px;
+	-moz-border-radius:0px;
+	-webkit-border-radius:0px;    
+}
+.bx-btn-group .bx-btn:first-child {
+    -webkit-border-top-right-radius: 0px;
+    -webkit-border-bottom-right-radius: 0px;
+    -moz-border-radius-topright: 0px;
+    -moz-border-radius-bottomright: 0px;
+    border-top-right-radius: 0px;
+    border-bottom-right-radius: 0px;
+}
+.bx-btn-group .bx-btn:last-child {
+    -webkit-border-top-left-radius: 0px;
+    -webkit-border-bottom-left-radius: 0px;
+    -moz-border-radius-topleft: 0px;
+    -moz-border-radius-bottomleft: 0px;
+    border-top-left-radius: 0px;
+    border-bottom-left-radius: 0px;
+}
+
+/* --------------- button at the end of cut string */
+
+.bx-str-limit {
+    margin-left:0.5rem;
+    cursor:pointer;
+}
+
+/* --------------- categories */
+
+.bx-category-list-item {
+	line-height: 1.5rem;
+}
+.bx-category-list-item-selected {
+	font-weight: bold;
+}
+
+/* --------------- Show More */
+.bx-overflow-ready {
+    max-height: 250px;
+    overflow: hidden;
+}
+.bx-overflow-ready.bx-or-cmt {
+    max-height: 80px;
+}
+.bx-overflow:before {
+    position: absolute;
+    bottom: 0;
+    width: 100%;
+    height: 20%;
+
+    content: '';
+}
+
+.bx-show-more {
+    padding-top: 0.25rem;
+    padding-bottom: 0.25rem;
+
+    text-align: center
+}
+
+/* --------------- Flickity: Tailwind overwrite */
+.flickity-enabled .flickity-viewport .flickity-slider img,
+.flickity-enabled .flickity-viewport .flickity-slider video {
+    display: inline;
+    max-width: none;
+}

+ 56 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/css/editor.quill.css

@@ -0,0 +1,56 @@
+.bx-form-input-html-quill{
+    z-index: 1;
+}
+.ql-picker-options{
+    z-index: 2 !important;
+}
+.ql-toolbar{
+    border-radius: 0.3rem 0.3rem 0 0;
+}
+.ql-container.ql-snow{
+    border-radius: 0 0 0.3rem 0.3rem;
+    border: 1px solid #ccc;
+}
+.dark .ql-container.ql-snow{
+    border-color: rgb(55 65 81);
+}
+.ql-mention-list{
+    overflow-y: scroll !important;
+    max-height: 300px;
+}
+.ql-mention-list li{
+    padding: 0.5rem;
+    min-width: 200px;
+}
+.ql-mention-list li:hover{
+    cursor: pointer;
+    
+}
+.ql-editor.bx-def-vanilla-html{
+    max-width: none;
+}
+.dark .ql-toolbar.ql-snow{
+    border-color: rgb(55 65 81);
+    background-color: rgb(55 65 81);
+}
+.dark .ql-snow .ql-stroke{
+    stroke: rgb(209, 213, 219);
+}
+.dark .ql-snow .ql-fill, 
+.dark .ql-snow .ql-stroke.ql-fill{
+    fill: rgb(209, 213, 219);
+}
+.dark .ql-snow .ql-picker-label{
+    color: rgb(209, 213, 219);
+}
+.dark .ql-picker-options{
+    background-color: rgb(31 41 55);
+}
+.dark .ql-picker-options .ql-picker-item{
+    color: rgb(209, 213, 219);
+}
+
+.bx-dir-rtl .ql-editor {
+    direction: rtl;
+    text-align: right;
+}

+ 686 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/css/forms.css

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

+ 176 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/css/grid.css

@@ -0,0 +1,176 @@
+
+.bx-grid-table {
+    width:100%;
+    border-collapse:collapse;
+}
+.bx-grid-table-wrapper {    
+    overflow-x:hidden;
+    overflow-y:hidden;    
+}
+.bx-grid-table-wrapper2 {
+    position:relative;
+}
+.bx-grid-table-wrapper2.bx-grid-table-hide-shadow-before:before,
+.bx-grid-table-wrapper2.bx-grid-table-hide-shadow-after:after {
+    display:none !important;
+}
+.bx-grid-table-wrapper2:before,
+.bx-grid-table-wrapper2:after {
+    display:none;
+    content:"";
+    position:absolute;
+    top:0;
+    width:1.5rem;
+    height:100%;
+    z-index:1;
+}
+.bx-grid-table-wrapper2:before {
+    left:-1px;
+    background: linear-gradient(to right, rgba(255,255,255,1) 0%,rgba(255,255,255,0) 100%);
+}
+.bx-grid-table-wrapper2:after {
+    right:-1px;
+    background: linear-gradient(to right, rgba(255,255,255,0) 0%,rgba(255,255,255,1) 100%);
+}
+.bx-grid-table-wrapper .bx-def-hr {
+    margin:0;
+}
+.bx-grid-header div.bx-form-element-wrapper {
+    width: 10rem;
+    margin: 0;
+}
+.bx-grid-header-cnt {
+    position: relative;
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+    -webkit-justify-content: flex-start;
+    justify-content: flex-start;
+    flex-wrap: wrap;
+    gap: 10px;
+}
+.bx-grid-header-controls-filter,
+.bx-grid-header-controls-independent {
+    position: relative;
+    display: flex;
+    align-items: center;
+    flex-wrap: wrap;
+    column-gap: 0.5rem;
+    row-gap: 0.25rem;
+}
+.bx-grid-header-controls-independent {
+    justify-content: flex-end;
+}
+.bx-grid-header-controls-counter {
+    -webkit-flex-grow: 2;
+    flex-grow: 2;
+
+    white-space: nowrap;
+}
+.bx-gh-no-counter .bx-grid-header-controls-independent {
+    -webkit-flex-grow: 2;
+    flex-grow: 2;
+
+    text-align: right;
+}
+.bx-grid-header-controls-independent .bx-btn {
+    margin: 0;
+    float: none;
+    white-space: nowrap;
+}
+
+.bx-media-phone .bx-grid-header-cnt {
+    -webkit-flex-wrap: wrap;
+    flex-wrap: wrap;
+    flex-direction: column;
+    align-items: flex-start;
+}
+.bx-media-phone .bx-grid-header-controls-counter{
+    text-align: left;
+}
+.bx-grid-table .bx-grid-table-row-hr td:first-child,
+.bx-grid-table .bx-grid-table-row-hr td {
+    padding:0;
+}
+
+.bx-grid-table-row-trans {
+   -webkit-transition:background-color 1s linear;
+   -moz-transition:background-color 1s linear;
+   -o-transition:background-color 1s linear;
+   transition:background-color 1s linear;
+}
+
+.bx-grid-table .bx-grid-table-row-empty td {
+    text-align:center;
+}
+
+.bx-grid-table th,
+.bx-grid-table td { 
+    padding-left:5px;
+    padding-right:5px;
+    text-align:left;
+}
+.bx-dir-rtl .bx-grid-table th,
+.bx-dir-rtl .bx-grid-table td {
+    text-align:right;
+}
+
+.bx-grid-table th:first-child,
+.bx-grid-table td:first-child {
+    padding-left:10px;
+}
+.bx-grid-table th:last-child,
+.bx-grid-table td:last-child {
+    padding-right:10px;
+}
+
+.bx-grid-actions-divider {
+    float:left;
+    text-align:center;
+    width:4px;
+}
+
+div.bx-grid-drag-handle {
+    display:none;
+    width:16px;
+    height:16px;
+
+    color:#666666;
+    line-height:18px;
+    font-size:14px;
+
+    cursor:move;
+}
+
+
+.bx-grid-gragging-row {    
+    -moz-border-radius: 3px;
+    -webkit-border-radius: 3px;
+    border-radius: 3px;
+
+    -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=90)"; 
+    filter: alpha(opacity=90); 
+    -moz-opacity: 0.9;
+    -khtml-opacity: 0.9;
+    opacity: 0.9;
+
+    -moz-box-shadow: 1px 1px 5px rgba(51, 51, 51, 0.5);
+    -webkit-box-shadow: 1px 1px 5px rgba(51, 51, 51, 0.5);
+    box-shadow: 1px 1px 5px rgba(51, 51, 51, 0.5);    
+}
+
+.bx-grid-table .bx-grid-drag-placeholder td {
+    padding-left:0;
+}
+
+td:last-child .bx-grid-cell-single-actions-wrapper {
+    float:right;
+}
+.bx-dir-rtl td:last-child .bx-grid-cell-single-actions-wrapper {
+    float:left;
+}
+
+.bx-grid-table-wrapper td .bx-def-unit {
+    margin-bottom:0;
+}

+ 154 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/css/label.css

@@ -0,0 +1,154 @@
+/*-- Labels Form Field --*/
+.sys-labels .bx-form-input-text.bx-form-input-autotoken {
+    display: -webkit-flex;
+    display: flex;
+    -webkit-flex-wrap: wrap;
+    flex-wrap: wrap;
+
+    box-sizing: border-box;
+    height: auto;
+    min-height: 2.5rem;
+}
+.sys-labels .bx-form-input-text.bx-form-input-autotoken b,
+.sys-labels .bx-form-input-text.bx-form-input-autotoken input:not([type = 'hidden']) {
+    position: relative;
+    display: block;
+    float: none;
+
+    height: 1.625rem;
+
+    line-height: 1.625rem;
+}
+.sys-labels .bx-form-input-text.bx-form-input-autotoken b {
+    margin: 0 0 0 0.125rem;
+    padding: 0 0.5rem;
+    
+    border-radius: 1.25rem;
+
+    font-weight: normal;
+    white-space: nowrap;
+    cursor: pointer;
+}
+.bx-dir-rtl .sys-labels .bx-form-input-text.bx-form-input-autotoken b {
+    margin: 0 0.125rem 0 0;
+}
+
+.sys-labels .bx-form-input-text.bx-form-input-autotoken b:after {
+    top: -1px;
+
+    margin: 0;
+    padding: 0 0.5rem;
+}
+.sys-labels .bx-form-input-text.bx-form-input-autotoken b.val-placeholder {
+    padding-left: 1.5rem;
+    padding-right: 0.5rem;
+    height: 2rem;
+    line-height: 2rem;    
+}
+.bx-dir-rtl .sys-labels .bx-form-input-text.bx-form-input-autotoken b.val-placeholder {
+    padding-left: 0.5rem;
+    padding-right: 1.5rem;
+}
+.sys-labels .bx-form-input-text.bx-form-input-autotoken b.val-placeholder::before {
+    font-family: 'sys-user', 'sys-user-fal';
+    content: "\f067";
+    position: absolute;
+    margin-left: -1.5em;
+    font-size: 0.7em;
+    font-weight: bold;
+}
+.bx-dir-rtl .sys-labels .bx-form-input-text.bx-form-input-autotoken b.val-placeholder::before {
+    margin-left: 0;
+    margin-right: -1.5em;
+}
+
+.sys-labels .bx-form-input-text.bx-form-input-autotoken input:not([type = 'hidden']) {
+    width: auto !important;
+    padding: 0;
+}
+
+/*-- Labels Popup Search --*/
+.sys-labels-select .bx-form-input-text.bx-form-input-autotoken b {
+    padding-left: 0.5rem;
+}
+
+
+/*-- Labels Popup List and Labels Browse  --*/
+.sys-labels-list-cnt {
+    max-height: 8.5rem;
+    overflow-y: auto;
+}
+.sys-labels-level {
+    position: relative;
+    
+    width: 100%;
+}
+.sys-labels-level.sys-ll-0 {
+    position: relative;
+    display: block;
+
+    display: -webkit-flex;
+    display: flex;
+    -webkit-flex-wrap: wrap;
+    flex-wrap: wrap;
+}
+.sys-labels-level.sys-ll-0 > .sys-labels-list-item {
+    -webkit-flex: 0 0 auto;
+    -ms-flex: 0 0 auto;
+    flex: 0 0 auto;
+
+    padding-left: 0.25rem;
+    padding-right: 0.25rem;
+}
+.sys-labels-list .sys-labels-level.sys-ll-0 > .sys-labels-list-item {
+    width: 50%;
+}
+.bx-layout-column-thin .sys-labels-browse .sys-labels-level.sys-ll-0 > .sys-labels-list-item {
+    width: 50%;
+}
+.bx-layout-column-half .sys-labels-browse .sys-labels-level.sys-ll-0 > .sys-labels-list-item {
+    width: 33.3%;
+}
+.bx-layout-column-wide .sys-labels-browse .sys-labels-level.sys-ll-0 > .sys-labels-list-item {
+    width: 25%;
+}
+.bx-layout-column-fullwidth .sys-labels-browse .sys-labels-level.sys-ll-0 > .sys-labels-list-item {
+    width: 16.6%;
+}
+.bx-media-phone .sys-labels-browse .sys-labels-level.sys-ll-0 > .sys-labels-list-item {
+    width: 50%;
+}
+.sys-labels-li-sublist {
+    display: none;
+}
+.sys-labels-li-sublist.sys-labels-lis-opened {
+    display: block;
+}
+
+
+/*-- Labels List Item --*/
+.sys-labels-list-item-box {
+    position: relative;
+
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+    -webkit-flex-wrap: nowrap;
+    flex-wrap: nowrap;
+}
+.sys-labels-list-item-box > * {
+    -webkit-flex-grow: 0;
+    flex-grow: 0;
+}
+.sys-labels-li-checbox input {
+    margin: 0;
+}
+.sys-labels-li-title {
+    -webkit-flex-grow: 1;
+    flex-grow: 1;
+
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    overflow: hidden;
+}

+ 23 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/css/manage_tools.css

@@ -0,0 +1,23 @@
+/*--- Administration tools ---*/
+/*+++ Controls filter +++*/
+div.bx-grid-header div.bx-grid-header-controls-filter {
+    position: relative;
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+}
+div.bx-grid-header div.bx-grid-header-controls-filter > .bx-form-element-wrapper {
+    margin-top: 0px;
+    margin-bottom: 0px;
+}
+div.bx-grid-header .bx-form-element-wrapper.bx-grid-controls-filter-datepicker {
+    width: 11.375rem;
+}
+div.bx-grid-header .bx-form-element-wrapper.bx-grid-controls-filter-label {
+    width: auto;
+}
+div.bx-grid-header .bx-form-element-wrapper.bx-grid-controls-filter-label .bx-form-input-wrapper{
+    height:2.5rem;
+    line-height:2.4rem;
+}

+ 1546 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/css/tailwind.css

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

+ 277 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/css/vote.css

@@ -0,0 +1,277 @@
+.bx-vote .sys-action-element-holder .sys-action-do-icon svg,
+.bx-vote-counter .bx-vote-counter-label .bx-vote-counter-icon svg {
+    width: 1.25rem;
+    height: 1.25rem;
+}
+.bx-vote .bx-vote-counter .bx-vote-counter-label {
+    padding: 0;
+}
+.bx-vote .bx-vote-counter .bx-vote-counter-label .bx-vote-counter-text {
+    margin: 0;
+}
+
+/*----------------------------------*/
+/*--- Likes based Element Styles ---*/
+/*----------------------------------*/
+.bx-vote-counter-likes span.bx-vote-counter-text {
+    width: auto;
+    height: auto;
+}
+
+/*--------------------------------------*/
+/*--- Reactions based Element Styles ---*/
+/*--------------------------------------*/
+.bx-vote-counter-wrapper-reactions .bx-vote-counter {
+    display: inline-block;
+}
+.bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound {
+    position: relative;
+}
+.bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound .bx-vote-counter {
+    display: block;
+}
+.bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-divided a.bx-vote-counter {
+    margin-right: 0.5rem;
+}
+.bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound .sys-ac-link {
+    padding: 0.25rem 0.25rem 0.25rem 0.75rem;
+}
+.bx-dir-rtl .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound .sys-ac-link {
+    padding-left: 0.25rem;
+    padding-right: 0.75rem;
+}
+.bx-media-phone .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound .sys-ac-link,
+.bx-media-tablet .bx-layout-column-thin .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound .sys-ac-link,
+.bx-media-tablet .bx-layout-column-half .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound .sys-ac-link {
+    padding-left: 1.0rem;
+}
+.bx-media-phone .bx-dir-rtl .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound .sys-ac-link,
+.bx-media-tablet .bx-dir-rtl .bx-layout-column-thin .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound .sys-ac-link,
+.bx-media-tablet .bx-dir-rtl .bx-layout-column-half .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound .sys-ac-link {
+    padding-left: 0.25rem;
+    padding-right: 1.0rem;
+}
+.bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound a.bx-vote-counter.total-count,
+.bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound span.bx-vote-counter.total-count {
+    width: auto;
+    height: auto;
+    margin-left: 0.5rem;
+}
+.bx-dir-rtl .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound a.bx-vote-counter.total-count,
+.bx-dir-rtl .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound span.bx-vote-counter.total-count {
+    margin-left: 0;
+    margin-right: 0.5rem;
+}
+.bx-dir-ltr .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound a.bx-vote-counter.total-count .sys-action-counter-text:first-child,
+.bx-dir-ltr .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound span.bx-vote-counter.total-count .sys-action-counter-text:first-child {
+    margin-left: 0;
+}
+.bx-dir-rtl .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound a.bx-vote-counter.total-count .sys-action-counter-text:first-child,
+.bx-dir-rtl .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound span.bx-vote-counter.total-count .sys-action-counter-text:first-child {
+    margin-right: 0;
+}
+.bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound span.bx-vote-counter:not(.total-count) {
+    margin-left: -0.5rem;
+}
+.bx-dir-rtl .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound span.bx-vote-counter:not(.total-count) {
+    margin-left: 0;
+    margin-right: -0.5rem;
+}
+.bx-media-phone .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound span.bx-vote-counter:not(.total-count),
+.bx-media-tablet .bx-layout-column-thin .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound span.bx-vote-counter:not(.total-count),
+.bx-media-tablet .bx-layout-column-half .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound span.bx-vote-counter:not(.total-count) {
+    margin-left: -0.75rem;
+}
+.bx-media-phone .bx-dir-rtl .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound span.bx-vote-counter:not(.total-count),
+.bx-media-tablet .bx-dir-rtl .bx-layout-column-thin .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound span.bx-vote-counter:not(.total-count),
+.bx-media-tablet .bx-dir-rtl .bx-layout-column-half .bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound span.bx-vote-counter:not(.total-count) {
+    margin-left: 0;
+    margin-right: -0.75rem;
+}
+.bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound .bx-vote-counter-label {
+    padding: 0;
+}
+.bx-vote-counter-wrapper-reactions.bx-vote-counter-wrapper-reactions-compound span.bx-vote-counter:not(.total-count) span.bx-vote-counter-text {
+    display: none;
+}
+.bx-vote-counter-wrapper-reactions.bx-vc-hidden,
+.bx-vote-counter-wrapper-reactions .bx-vote-counter.bx-vc-hidden {
+    display: none;
+}
+.bx-menu-object-sys_vote_reactions_do li.bx-menu-item {
+    padding: 0.25rem;
+}
+.bx-menu-object-sys_vote_reactions_do li.bx-menu-item .sys-icon {
+    width: 2rem;
+    height: 2rem;
+    font-size: 1.5rem;
+}
+
+@media (any-pointer: fine) {
+	.bx-popup-vote-reactions .bx-popup-close-wrapper {
+		display:none;  
+	}
+}
+
+/*----------------------------------*/
+/*--- Stars based Element Styles ---*/
+/*----------------------------------*/
+.bx-vote-stars {
+    -webkit-flex-direction: column;
+    flex-direction: column;
+    -webkit-align-items: flex-start;
+    align-items: flex-start;
+}
+.bx-vote-stars .bx-vote-element-holder a.bx-vote-button,
+.bx-vote-stars .bx-vote-element-holder i.bx-vote-star,
+.bx-vote-stars .bx-vote-element-holder span.bx-vote-star {
+    position: relative;
+    display: inline-block;
+    vertical-align: top;
+    height: 1rem;
+    width: 1rem;
+    margin: 0;
+    padding: 0;
+    text-align: center;
+}
+
+
+/*--- Voted By ---*/
+div.bx-vote-by-list {}
+div.bx-vote-by-list-cnt {
+    max-height: 12.5rem;
+    min-width: 13.75rem;
+    overflow: auto;
+}
+div.bx-vote-by-list div.bx-base-pofile-unit {
+    float: none;
+    margin-right: 0px;
+    margin-bottom: 0px;
+}
+.bx-vote-bls-header {
+    display: -webkit-flex;
+    display: flex;
+
+    margin: -1.25rem -1.25rem 0 -1.25rem;
+}
+.bx-vote-bls-content {
+    margin-left: -0.5rem;
+    margin-right: -0.5rem;
+}
+.bx-vote-bls-title,
+.bx-vote-bls-submenu {
+    -webkit-flex: 0 1 auto; 
+    -ms-flex:  0 1 auto; 
+    flex:  0 1 auto;
+    line-height: 3.125rem;
+}
+.bx-vote-bls-title,
+.bx-vote-bls-submenu-cnt,
+.bx-vote-bls-filler {
+    height: 3.125rem;
+}
+.bx-vote-bls-title,
+.bx-vote-bls-filler {
+    -webkit-flex-grow: 3;
+    flex-grow: 3;
+
+    border-bottom: 2px solid rgba(232, 232, 232, 1.0);
+}
+.bx-vote-bls-title {
+    display: none;
+
+    white-space: nowrap;
+    font-size: 1rem;
+    font-weight: 400;
+}
+.bx-vote-bls-submenu {
+    overflow-x: auto;
+    overflow-y: hidden;
+
+    -webkit-overflow-scrolling: touch;
+}
+.bx-vote-bls-submenu-cnt {
+    box-sizing: border-box;
+    padding: 0 0.5rem;
+    border-bottom-width: 2px;
+    border-bottom-style: solid;
+}
+.bx-vote-bls-submenu::-webkit-scrollbar {
+    width: 0.5rem;
+}
+.bx-vote-bls-submenu::-webkit-scrollbar-track {
+    -webkit-border-radius: 0.5rem;
+    border-radius: 0.5rem;
+
+    background-color: rgba(0, 0, 0, 0.1); 
+}
+.bx-vote-bls-submenu::-webkit-scrollbar-thumb {
+    -webkit-border-radius: 0.5rem;
+    border-radius: 0.5rem;
+
+    background: rgba(255, 255, 255, 0.9); 
+    -webkit-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5);
+}
+.bx-vote-bls-submenu::-webkit-scrollbar-thumb:window-inactive {
+    background: rgba(255, 255, 255, 0.4); 
+}
+.bx-vote-bls-submenu-cnt ul {
+    display: -webkit-flex;
+    display: flex;
+}
+.bx-vote-bls-submenu-cnt ul li {
+    -webkit-flex: 0 1 auto; 
+    -ms-flex:  0 1 auto; 
+    flex:  0 1 auto;
+    box-sizing: border-box;
+    height: 3.125rem;
+
+    border-bottom: 2px solid transparent;
+    border-radius: 0;
+
+    line-height: 3.0rem;
+    white-space: nowrap;
+}
+.bx-vote-bls-submenu-cnt ul li .bx-stl-mil,
+.bx-vote-bls-submenu-cnt ul li .bx-stl-mil-wo-gaps {
+    border-width: 0;
+}
+.bx-vote-bls-submenu-cnt ul li.bx-menu-inter-pas .bx-menu-item-cnt,
+.bx-vote-bls-submenu-cnt ul li.bx-menu-inter-act .bx-menu-item-inter {
+    line-height: 3.0rem;
+}
+.bx-vote-bls-submenu-cnt ul li a.bx-menu-item-inter,
+.bx-vote-bls-submenu-cnt ul li div.bx-menu-item-inter {
+    padding-top: 0;
+    padding-bottom: 0;
+}
+.bx-vote-bls-submenu-cnt ul li.bx-menu-inter-act > div {
+    font-weight: bold;
+}
+.bx-vote-bls-content .bx-vote-by-list:not(.bx-vote-bl-summary) {
+    display: none;
+}
+.bx-vote-bls-content .bx-vote-by-items {
+    padding-left: 0.5rem;
+    padding-right: 0.5rem;
+}
+.bx-vote-bls-content .bx-vote-by-item {
+    position: relative;
+    display: -webkit-flex;
+    display: flex;
+    -webkit-justify-content: space-between;
+    justify-content: space-between;
+    -webkit-align-items: center;
+    align-items: center;
+}
+.bx-vote-bls-content .bx-vote-by-item:first-child {
+    padding-top: 1rem;
+}
+.bx-vote-bls-content .bx-vote-by-item:last-child {
+    padding-bottom: 0;
+}
+
+.bx-vote-bls-content .bx-vote-by-item:before,
+.bx-vote-bls-content .bx-vote-by-item:after {
+    display: none;
+}

+ 1 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/favorite_do_favorite_label.html

@@ -0,0 +1 @@
+<span class="sys-action-do __style_prefix__-do bx-def-valign-center"><bx_if:show_icon><span class="sys-action-do-icon __style_prefix__-do-icon bx-def-align-center">__icon__</span></bx_if:show_icon><bx_if:show_text><span class="sys-action-do-text __style_prefix__-do-text bx-def-margin-sec-left-auto bx-dir">__text__</span></bx_if:show_text></span>

+ 1 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/feature_do_feature_label.html

@@ -0,0 +1 @@
+<span class="sys-action-do __style_prefix__-do bx-def-valign-center"><bx_if:show_icon><span class="sys-action-do-icon __style_prefix__-do-icon bx-def-align-center">__icon__</span></bx_if:show_icon><bx_if:show_text><span class="sys-action-do-text __style_prefix__-do-text bx-def-margin-sec-left-auto bx-dir">__text__</span></bx_if:show_text></span>

+ 18 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/form_field_section_title.html

@@ -0,0 +1,18 @@
+<bx_if:show_view>
+    <div class="bx-form-section-title px-6 py-5">__caption__<bx_if:show_view_info><br /><span class="bx-def-font-grayed">__info__</span></bx_if:show_view_info></div>
+</bx_if:show_view>
+<bx_if:show_default>
+    <div class="bx-form-section-title group relative flex justify-between align-center flex-nowrap w-full">
+        <div class="flex-1 max-w-full ltr:pr-12 rtl:pl-12">
+            <a class="block w-full ltr:pl-6 rtl:pr-6 py-5 text-inherit hover:text-gray-800 dark:hover:text-gray-200 font-semibold" href="javascript:void(0);">
+                __caption__
+                <bx_if:show_default_info>
+                    <br /><span class="bx-def-font-grayed block w-full truncate">__info__</span>
+                </bx_if:show_default_info>
+            </a>
+        </div>
+        <div class="absolute ltr:right-0 rtl:left-0 py-5 ltr:pr-6 rtl:pl-6">
+            <u class="bx-form-section-toggler"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z"></path></svg></u>
+        </div>
+    </div>
+</bx_if:show_default>

+ 17 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_2_columns.html

@@ -0,0 +1,17 @@
+<!-- layout 2 columns [ start ] -->
+<div id="__page_id__" class="bx-layout-wrapper bx-layout-2-columns">
+    <div class="bx-layout-row">
+        <aside class="bx-layout-col bx-layout-2-columns-column1 bx-layout-column-half md:w-1/2 ltr:md:pr-4 ltr:lg:pr-6 rtl:md:pl-4 rtl:lg:pl-6" aria-label="Sidebar">
+            <div class="bx-layout-cell" id="bx-layout-cell-1">
+                __cell_1__
+            </div>
+        </aside>
+        <main class="bx-layout-col bx-layout-2-columns-column2 bx-layout-column-half md:w-1/2" aria-label="Main">
+            <div class="bx-layout-cell" id="bx-layout-cell-2">
+                __cell_2__
+            </div>
+        </main>
+    </div>
+    <bx_include_auto:_row_1_column_dump.html />
+</div>
+<!-- layout 2 columns [  end  ] -->

+ 24 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_3_columns.html

@@ -0,0 +1,24 @@
+<!-- layout 3 columns [ start ] -->
+<div id="__page_id__" class="bx-layout-wrapper bx-layout-3-columns">
+    <div class="bx-layout-row">
+        <section class="bx-layout-col bx-layout-bcb-bar1 bx-layout-column-thin md:w-1/3 flex-auto md:flex-none ltr:md:pr-4 ltr:lg:pr-6 rtl:md:pl-4 rtl:lg:pl-6" aria-label="Sidebar" >
+            <div class="bx-layout-cell" id="bx-layout-cell-1">
+                __cell_1__
+            </div>
+        </section>
+        <section aria-label="Content" class="max-w-full md:w-2/3 flex-1 flex flex-wrap lg:flex-nowrap bx-layout-gap">
+            <main class="bx-layout-col bx-layout-bcb-content bx-layout-column-half flex-auto lg:flex-none lg:w-1/2 ltr:lg:pr-6 rtl:lg:pl-6" aria-label="Main">
+                <div class="bx-layout-cell" id="bx-layout-cell-2">
+                    __cell_2__
+                </div>
+            </main>
+            <aside class="bx-layout-col bx-layout-bcb-bar2 bx-layout-column-thin lg:w-1/2 flex-auto" aria-label="Aside">
+                <div class="bx-layout-cell" id="bx-layout-cell-3">
+                    __cell_3__
+                </div>
+            </aside>
+        </section>
+    </div>
+    <bx_include_auto:_row_1_column_dump.html />
+</div>
+<!-- layout 3 columns [  end  ] -->

+ 24 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_bar_content_bar.html

@@ -0,0 +1,24 @@
+<!-- layout bar content bar (bcb) [ start ] -->
+<div id="__page_id__" class="bx-layout-wrapper bx-layout-bcb">
+    <div class="bx-layout-row">
+        <section class="bx-layout-col bx-layout-bcb-bar1 bx-layout-column-thin md:w-1/4 flex-auto md:flex-none ltr:md:pr-4 ltr:lg:pr-6 rtl:md:pl-4 rtl:lg:pl-6" aria-label="Sidebar" >
+            <div class="bx-layout-cell" id="bx-layout-cell-1">
+                __cell_1__
+            </div>
+        </section>
+        <section aria-label="Content" class="max-w-full md:w-3/4 flex-1 flex flex-wrap lg:flex-nowrap bx-layout-gap">
+            <main class="bx-layout-col bx-layout-bcb-content bx-layout-column-half flex-auto lg:w-2/3 ltr:lg:pr-6 rtl:lg:pl-6" aria-label="Main">
+                <div class="bx-layout-cell" id="bx-layout-cell-2">
+                    __cell_2__
+                </div>
+            </main>
+            <aside class="bx-layout-col bx-layout-bcb-bar2 bx-layout-column-thin lg:w-1/3" aria-label="Aside">
+                <div class="bx-layout-cell" id="bx-layout-cell-3">
+                    __cell_3__
+                </div>
+            </aside>
+        </section>
+    </div>
+    <bx_include_auto:_row_1_column_dump.html />
+</div>
+<!-- layout bar content bar (bcb) [  end  ] -->

+ 17 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_bar_left.html

@@ -0,0 +1,17 @@
+<!-- layout bar left [ start ] -->
+<div id="__page_id__" class="bx-layout-wrapper bx-layout-bar-left">
+    <div class="bx-layout-row">
+        <aside class="bx-layout-col bx-layout-bar-left-bar bx-layout-column-thin md:w-1/3 ltr:md:pr-4 ltr:lg:pr-6 rtl:md:pl-4 rtl:lg:pl-6" aria-label="Sidebar">
+            <div class="bx-layout-cell" id="bx-layout-cell-1">
+                __cell_1__
+            </div>
+        </aside>
+        <main class="bx-layout-col bx-layout-bar-left-content bx-layout-column-wide md:w-2/3" aria-label="Main">
+            <div class="bx-layout-cell" id="bx-layout-cell-2">
+                __cell_2__
+            </div>
+        </main>
+    </div>
+    <bx_include_auto:_row_1_column_dump.html />
+</div>
+<!-- layout bar left [  end  ] -->

+ 17 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_bar_right.html

@@ -0,0 +1,17 @@
+<!-- layout bar right [ start ] -->
+<div id="__page_id__" class="bx-layout-wrapper bx-layout-bar-right">
+    <div class="bx-layout-row">
+        <main class="bx-layout-col bx-layout-bar-right-content bx-layout-column-wide md:w-2/3 ltr:md:pr-4 ltr:lg:pr-6 rtl:md:pl-4 rtl:lg:pl-6" aria-label="Main">
+            <div class="bx-layout-cell" id="bx-layout-cell-1">
+                __cell_1__
+            </div>
+        </main>
+        <aside class="bx-layout-col bx-layout-bar-right-bar bx-layout-column-thin md:w-1/3" aria-label="Aside">
+            <div class="bx-layout-cell" id="bx-layout-cell-2">
+                __cell_2__
+            </div>
+        </aside>
+    </div>
+    <bx_include_auto:_row_1_column_dump.html />
+</div>
+<!-- layout bar right [  end  ] -->

+ 19 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_bottom_area_2_columns.html

@@ -0,0 +1,19 @@
+<!-- layout top area 2 columns [ start ] -->
+<div id="__page_id__" class="bx-layout-wrapper bx-layout-bottom-area-2-columns">
+    <div class="bx-layout-row">
+        <aside class="bx-layout-col bx-layout-2-columns-column1 bx-layout-column-half md:w-1/2 ltr:md:pr-4 ltr:lg:pr-6 rtl:md:pl-4 rtl:lg:pl-6" aria-label="Sidebar">
+            <div class="bx-layout-cell" id="bx-layout-cell-2">
+                __cell_2__
+            </div>
+        </aside>
+        <main class="bx-layout-col bx-layout-2-columns-column2 bx-layout-column-half md:w-1/2" aria-label="Main">
+            <div class="bx-layout-cell" id="bx-layout-cell-3">
+                __cell_3__
+            </div>
+        </main>
+    </div>
+    <bx_include_auto:_row_1_column.html />
+    <bx_include_auto:_row_1_column_dump.html />
+</div>
+<bx_include_auto:_layout_align_columns.html />
+<!-- layout top area 2 columns [  end  ] -->

+ 18 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_top_area_2_columns.html

@@ -0,0 +1,18 @@
+<!-- layout top area 2 columns [ start ] -->
+<div id="__page_id__" class="bx-layout-wrapper bx-layout-top-area-2-columns">
+    <bx_include_auto:_row_1_column.html />
+    <div class="bx-layout-row">
+        <aside class="bx-layout-col bx-layout-2-columns-column1 bx-layout-column-half md:w-1/2 ltr:md:pr-4 ltr:lg:pr-6 rtl:md:pl-4 rtl:lg:pl-6" aria-label="Sidebar">
+            <div class="bx-layout-cell" id="bx-layout-cell-2">
+                __cell_2__
+            </div>
+        </aside>
+        <main class="bx-layout-col bx-layout-2-columns-column2 bx-layout-column-half md:w-1/2" aria-label="Main">
+            <div class="bx-layout-cell" id="bx-layout-cell-3">
+                __cell_3__
+            </div>
+        </main>
+    </div>
+    <bx_include_auto:_row_1_column_dump.html />
+</div>
+<!-- layout top area 2 columns [  end  ] -->

+ 25 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_top_area_3_columns.html

@@ -0,0 +1,25 @@
+<!-- layout top area 3 columns [ start ] -->
+<div id="__page_id__" class="bx-layout-wrapper bx-layout-top-area-3-columns">
+    <bx_include_auto:_row_1_column.html />
+    <div class="bx-layout-row" >
+        <section class="bx-layout-col bx-layout-bcb-bar1 bx-layout-column-thin md:w-1/3 flex-auto md:flex-none ltr:md:pr-4 ltr:lg:pr-6 rtl:md:pl-4 rtl:lg:pl-6" aria-label="Sidebar">
+            <div class="bx-layout-cell" id="bx-layout-cell-2">
+                __cell_2__
+            </div>
+        </section>
+        <section aria-label="Content" class="max-w-full md:w-2/3 flex-1 flex flex-wrap lg:flex-nowrap bx-layout-gap" >
+            <main class="bx-layout-col bx-layout-bcb-content bx-layout-column-half flex-auto lg:flex-none lg:w-1/2 ltr:lg:pr-6 rtl:lg:pl-6" aria-label="Main">
+                <div class="bx-layout-cell" id="bx-layout-cell-3">
+                    __cell_3__
+                </div>
+            </main>
+            <aside class="bx-layout-col bx-layout-bcb-bar2 bx-layout-column-thin flex-auto lg:w-1/2 " aria-label="Aside">
+                <div class="bx-layout-cell" id="bx-layout-cell-4">
+                    __cell_4__
+                </div>
+            </aside>
+        </section>
+    </div>
+    <bx_include_auto:_row_1_column_dump.html />
+</div>
+<!-- layout top area 3 columns [  end  ] -->

+ 25 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_top_area_bar_content_bar.html

@@ -0,0 +1,25 @@
+<!-- layout top area bar content bar (bcb) [ start ] -->
+<div id="__page_id__" class="bx-layout-wrapper bx-layout-top-area-bcb">
+    <bx_include_auto:_row_1_column.html />
+    <div class="bx-layout-row">
+        <section class="bx-layout-col bx-layout-bcb-bar1 bx-layout-column-thin md:w-1/4 flex-auto md:flex-none ltr:md:pr-4 ltr:lg:pr-6 rtl:md:pl-4 rtl:lg:pl-6" aria-label="Sidebar" >
+            <div class="bx-layout-cell" id="bx-layout-cell-2">
+                __cell_2__
+            </div>
+        </section>
+        <section aria-label="Content" class="max-w-full md:w-3/4 flex-1 flex flex-wrap lg:flex-nowrap bx-layout-gap">
+            <main class="bx-layout-col bx-layout-bcb-content bx-layout-column-half flex-auto lg:flex-none lg:w-2/3 ltr:lg:pr-6 rtl:lg:pl-6" aria-label="Main">
+                <div class="bx-layout-cell" id="bx-layout-cell-3">
+                    __cell_3__
+                </div>
+            </main>
+            <aside class="bx-layout-col bx-layout-bcb-bar2 bx-layout-column-thin flex-auto lg:w-1/3" aria-label="Aside">
+                <div class="bx-layout-cell" id="bx-layout-cell-4">
+                    __cell_4__
+                </div>
+            </aside>
+        </section>
+    </div>
+    <bx_include_auto:_row_1_column_dump.html />
+</div>
+<!-- layout top area bar content bar (bcb) [  end  ] -->

+ 18 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_top_area_bar_left.html

@@ -0,0 +1,18 @@
+<!-- layout top area bar left [ start ] -->
+<div id="__page_id__" class="bx-layout-wrapper bx-layout-top-area-bar-left">
+    <bx_include_auto:_row_1_column.html />
+    <div class="bx-layout-row">
+        <main class="bx-layout-col bx-layout-bar-left-bar bx-layout-column-thin md:w-1/3 ltr:md:pr-4 ltr:lg:pr-6 rtl:md:pl-4 rtl::lg:pl-6" aria-label="Main">
+            <div class="bx-layout-cell" id="bx-layout-cell-2">
+                __cell_2__
+            </div>
+        </main>
+        <aside class="bx-layout-col bx-layout-bar-left-content bx-layout-column-wide md:w-2/3" aria-label="Aside">
+            <div class="bx-layout-cell" id="bx-layout-cell-3">
+                __cell_3__
+            </div>
+        </aside>
+    </div>
+    <bx_include_auto:_row_1_column_dump.html />
+</div>
+<!-- layout top area bar left [  end  ] -->

+ 18 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_top_area_bar_right.html

@@ -0,0 +1,18 @@
+<!-- layout top area bar right [ start ] -->
+<div id="__page_id__" class="bx-layout-wrapper bx-layout-top-area-bar-right">
+    <bx_include_auto:_row_1_column.html />
+    <div class="bx-layout-row">
+        <main class="bx-layout-col bx-layout-bar-right-content bx-layout-column-wide md:w-2/3 ltr:md:pr-4 ltr:lg:pr-6 rtl:md:pl-4 rtl:lg:pl-6" aria-label="Main">
+            <div class="bx-layout-cell" id="bx-layout-cell-2">
+                __cell_2__
+            </div>
+        </main>
+        <aside class="bx-layout-col bx-layout-bar-right-bar bx-layout-column-thin md:w-1/3" aria-label="Aside">
+            <div class="bx-layout-cell" id="bx-layout-cell-3">
+                __cell_3__
+            </div>
+        </aside>
+    </div>
+    <bx_include_auto:_row_1_column_dump.html />
+</div>
+<!-- layout top area bar right [  end  ] -->

+ 26 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_topbottom_area_2_columns.html

@@ -0,0 +1,26 @@
+<!-- layout top area 2 columns [ start ] -->
+<div id="__page_id__" class="bx-layout-wrapper bx-layout-topbottom-area-2-columns">
+    <bx_include_auto:_row_1_column.html />
+    <div class="bx-layout-row">
+        <aside class="bx-layout-col bx-layout-2-columns-column1 bx-layout-column-half md:w-1/2 ltr:md:pr-4 ltr:lg:pr-6 rtl:md:pl-4 rtl:lg:pl-6" aria-label="Sidebar">
+            <div class="bx-layout-cell" id="bx-layout-cell-2">
+                __cell_2__
+            </div>
+        </aside>
+        <main class="bx-layout-col bx-layout-2-columns-column2 bx-layout-column-half md:w-1/2" aria-label="Main">
+            <div class="bx-layout-cell" id="bx-layout-cell-3">
+                __cell_3__
+            </div>
+        </main>
+    </div>
+    <div class="bx-layout-row">
+        <div class="bx-layout-col bx-layout-1-column-column1 bx-layout-column-fullwidth">
+            <div class="bx-layout-cell" id="bx-layout-cell-4">
+                __cell_4__
+            </div>
+        </div>
+    </div>
+    <bx_include_auto:_row_1_column_dump.html />
+</div>
+<bx_include_auto:_layout_align_columns.html />
+<!-- layout top area 2 columns [  end  ] -->

+ 26 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_topbottom_area_bar_left.html

@@ -0,0 +1,26 @@
+<!-- layout top & bottom areas bar left [ start ] -->
+<div id="__page_id__" class="bx-layout-wrapper bx-layout-topbottom-area-bar-left">
+    <bx_include_auto:_row_1_column.html />
+    <div class="bx-layout-row">
+        <aside class="bx-layout-col bx-layout-bar-left-bar bx-layout-column-thin md:w-1/3 ltr:md:pr-4 ltr:lg:pr-6 rtl:md:pl-4 rtl:lg:pl-6" aria-label="Sidebar">
+            <div class="bx-layout-cell" id="bx-layout-cell-2">
+                __cell_2__
+            </div>
+        </aside>
+        <main class="bx-layout-col bx-layout-bar-left-content bx-layout-column-wide md:w-2/3" aria-label="Main">
+            <div class="bx-layout-cell" id="bx-layout-cell-3">
+                __cell_3__
+            </div>
+        </main>
+    </div>
+    <div class="bx-layout-row">
+        <div class="bx-layout-col bx-layout-1-column-column1 bx-layout-column-fullwidth">
+            <div class="bx-layout-cell" id="bx-layout-cell-4">
+                __cell_4__
+            </div>
+        </div>
+    </div>
+    <bx_include_auto:_row_1_column_dump.html />
+</div>
+<bx_include_auto:_layout_align_columns.html />
+<!-- layout top area 2 columns [  end  ] -->

+ 26 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_topbottom_area_bar_right.html

@@ -0,0 +1,26 @@
+<!-- layout top & bottom areas bar right [ start ] -->
+<div id="__page_id__" class="bx-layout-wrapper bx-layout-topbottom-area-bar-right">
+    <bx_include_auto:_row_1_column.html />
+    <div class="bx-layout-row">
+        <main class="bx-layout-col bx-layout-bar-right-content bx-layout-column-wide md:w-2/3 ltr:md:pr-4 ltr:lg:pr-6 rtl:md:pl-4 rtl:lg:pl-6" aria-label="Main">
+            <div class="bx-layout-cell" id="bx-layout-cell-2">
+                __cell_2__
+            </div>
+        </main>
+        <aside class="bx-layout-col bx-layout-bar-right-bar bx-layout-column-thin md:w-1/3" aria-label="Aside">
+            <div class="bx-layout-cell" id="bx-layout-cell-3">
+                __cell_3__
+            </div>
+        </aside>
+    </div>
+    <div class="bx-layout-row">
+        <div class="bx-layout-col bx-layout-1-column-column1 bx-layout-column-fullwidth">
+            <div class="bx-layout-cell" id="bx-layout-cell-4">
+                __cell_4__
+            </div>
+        </div>
+    </div>
+    <bx_include_auto:_row_1_column_dump.html />
+</div>
+<bx_include_auto:_layout_align_columns.html />
+<!-- layout top area 2 columns [  end  ] -->

+ 35 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_topbottom_area_col1_col3_col2.html

@@ -0,0 +1,35 @@
+<!-- layout top & bottom areas with 3 columns (1:3:2) [ start ] -->
+<div id="__page_id__" class="bx-layout-wrapper bx-layout-topbottom-area-col1-col3-col2">
+    <bx_include_auto:_row_1_column.html />
+    <div class="bx-layout-row flex flex-col md:flex-row">
+        <div class="bx-layout-row">
+            <section class="bx-layout-col bx-layout-bcb-bar1 bx-layout-column-thin md:w-1/6 flex-auto md:flex-none ltr:md:pr-4 ltr:lg:pr-6 rtl:md:pl-4 rtl:lg:pl-6" aria-label="Sidebar" >
+                <div class="bx-layout-cell" id="bx-layout-cell-2">
+                    __cell_2__
+                </div>
+            </section>
+            <section aria-label="Content" class="max-w-full md:w-5/6 flex-1 flex flex-wrap lg:flex-nowrap bx-layout-gap">
+                <main class="bx-layout-col bx-layout-bcb-content bx-layout-column-half flex-auto md:flex-none lg:w-3/5 ltr:lg:pr-6 rtl:lg:pl-6" aria-label="Main">
+                    <div class="bx-layout-cell" id="bx-layout-cell-3">
+                        __cell_3__
+                    </div>
+                </main>
+                <aside class="bx-layout-col bx-layout-bcb-bar2 bx-layout-column-thin lg:w-2/5" aria-label="Aside">
+                    <div class="bx-layout-cell" id="bx-layout-cell-4">
+                        __cell_4__
+                    </div>
+                </aside>
+            </section>
+        </div>
+    </div>
+    <div class="bx-layout-row">
+        <div class="bx-layout-col bx-layout-1-column-column1 bx-layout-column-fullwidth">
+            <div class="bx-layout-cell" id="bx-layout-cell-5">
+                __cell_5__
+            </div>
+        </div>
+    </div>
+    <bx_include_auto:_row_1_column_dump.html />
+</div>
+<bx_include_auto:_layout_align_columns.html />
+<!-- layout top & bottom areas with 3 columns (1:3:2) [  end  ] -->

+ 26 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_topbottom_area_col1_col5.html

@@ -0,0 +1,26 @@
+<!-- layout top & bottom areas with 2 columns (1:5) [ start ] -->
+<div id="__page_id__" class="bx-layout-wrapper bx-layout-topbottom-area-col1-col5">
+    <bx_include_auto:_row_1_column.html />
+    <div class="bx-layout-row">
+        <aside class="bx-layout-col bx-layout-col1-col5-column1 bx-layout-column-thin md:w-1/6 ltr:md:pr-4 ltr:lg:pr-6 rtl:md:pl-4 rtl:lg:pl-6" aria-label="Sidebar">
+            <div class="bx-layout-cell" id="bx-layout-cell-2">
+                __cell_2__
+            </div>
+        </aside>
+        <main class="bx-layout-col bx-layout-col1-col5-column2 bx-layout-column-wide md:w-5/6" aria-label="Main">
+            <div class="bx-layout-cell" id="bx-layout-cell-3">
+                __cell_3__
+            </div>
+        </main>
+    </div>
+    <div class="bx-layout-row">
+        <div class="bx-layout-col bx-layout-1-column-column1 bx-layout-column-fullwidth">
+            <div class="bx-layout-cell" id="bx-layout-cell-4">
+                __cell_4__
+            </div>
+        </div>
+    </div>
+    <bx_include_auto:_row_1_column_dump.html />
+</div>
+<bx_include_auto:_layout_align_columns.html />
+<!-- layout top & bottom areas with 2 columns (1:5) [  end  ] -->

+ 33 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/layout_topbottom_area_col2_col5_col3.html

@@ -0,0 +1,33 @@
+<!-- layout top & bottom areas with 3 columns (2:5:3) [ start ] -->
+<div id="__page_id__" class="bx-layout-wrapper bx-layout-topbottom-area-col2-col5-col3">
+    <bx_include_auto:_row_1_column.html />
+    <div class="bx-layout-row">
+        <section class="bx-layout-col bx-layout-bcb-bar1 bx-layout-column-thin md:w-2/10 flex-auto md:flex-none ltr:md:pr-4 ltr:lg:pr-6 rtl:md:pl-4 rtl:lg:pl-6" aria-label="Sidebar" >
+            <div class="bx-layout-cell" id="bx-layout-cell-2">
+                __cell_2__
+            </div>
+        </section>
+        <section aria-label="Content" class="max-w-full md:w-8/10 flex-1 flex flex-wrap lg:flex-nowrap bx-layout-gap">
+            <main class="bx-layout-col bx-layout-bcb-content bx-layout-column-half flex-auto lg:flex-none lg:w-3/5 ltr:lg:pr-6 rtl:lg:pl-6" aria-label="Main">
+                <div class="bx-layout-cell" id="bx-layout-cell-3">
+                    __cell_3__
+                </div>
+            </main>
+            <aside class="bx-layout-col bx-layout-bcb-bar2 bx-layout-column-thin lg:w-2/5" aria-label="Aside">
+                <div class="bx-layout-cell" id="bx-layout-cell-4">
+                    __cell_4__
+                </div>
+            </aside>
+        </section>
+    </div>
+    <div class="bx-layout-row">
+        <div class="bx-layout-col bx-layout-1-column-column1 bx-layout-column-fullwidth">
+            <div class="bx-layout-cell" id="bx-layout-cell-5">
+                __cell_5__
+            </div>
+        </div>
+    </div>
+    <bx_include_auto:_row_1_column_dump.html />
+</div>
+<bx_include_auto:_layout_align_columns.html />
+<!-- layout top & bottom areas with 3 columns (2:5:3) [  end  ] -->

+ 10 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/menu_buttons_hor.html

@@ -0,0 +1,10 @@
+<ul class="bx-menu-btns-hor bx-menu-object-__object__ flex flex-row @container/menu">
+    <bx_repeat:menu_items>
+        <li class="bx-menu-item __name__ __class_add__ flex-1 @lg/menu:flex-none flex flex-row items-stretch min-w-0 my-1 ltr:mr-4 ltr:last:mr-0 rtl:ml-4 rtl:last:ml-0">
+            <a class="bx-btn __name__ flex flex-col @lg/menu:flex-row justify-center @lg/menu:justify-start w-full" href="__link__" <bx_if:onclick>onclick="__onclick__"</bx_if:onclick> title="__title_attr__"__attrs__>
+                <bx_include_auto:menu_icon.html />
+                <bx_if:title><u class="w-full mr-2 @lg/menu:mr-0 truncate text-center @lg/menu:text-left">__title__</u></bx_if:title>
+            </a>
+        </li>
+    </bx_repeat:menu_items>
+</ul>

+ 1 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/menu_switch_language_title.html

@@ -0,0 +1 @@
+<span class="sys-sl-title">__icon__<span class="sys-sl-text ltr:ml-2 ltr:first:ml-0 rtl:mr-2 rtl:first:mr-0">__title__</span></span>

+ 1 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/report_do_report_label.html

@@ -0,0 +1 @@
+<span class="sys-action-do __style_prefix__-do bx-def-valign-center"><bx_if:show_icon><span class="sys-action-do-icon __style_prefix__-do-icon bx-def-align-center"><i class="sys-icon __name__"></i></span></bx_if:show_icon><bx_if:show_text><span class="sys-action-do-text __style_prefix__-do-text bx-def-margin-sec-left-auto bx-dir">__text__</span></bx_if:show_text></span>

+ 16 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/score_by_list.html

@@ -0,0 +1,16 @@
+<div class="__style_prefix__-by-list">
+	<div class="__style_prefix__-by-list-cnt">
+		<div class="__style_prefix__-by-items">
+			<bx_repeat:list>
+				<div class="__style_prefix__-by-item bx-def-padding-sec bx-clearfix">
+				    <div class="__style_prefix__-bi-vote">__vote__</div>
+				    <div class="__style_prefix__-bi-user bx-def-padding-sec-left bx-dir">__user_unit__</div>
+				    <div class="__style_prefix__-bi-info bx-def-padding-sec-left bx-dir">
+				        <div><a href="__user_link__">__user_name__</a></div>
+				        <div class="bx-def-font-small bx-def-font-grayed">__date__</div>
+				    </div>
+				</div>
+			</bx_repeat:list>
+		</div>
+	</div>
+</div>

+ 1 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/score_do_vote_label.html

@@ -0,0 +1 @@
+<span class="sys-action-do __style_prefix__-do bx-def-valign-center"><bx_if:show_icon><span class="sys-action-do-icon __style_prefix__-do-icon bx-def-align-center">__icon__</span></bx_if:show_icon><bx_if:show_text><span class="sys-action-do-text __style_prefix__-do-text bx-def-margin-sec-left-auto bx-dir">__text__</span></bx_if:show_text></span>

+ 388 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/scripts/BxBaseAccountForms.php

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

+ 743 - 0
upgrade/files/14.0.0.RC3-14.0.0.RC4/files/template/scripts/BxBaseCmtsServices.php

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

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