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

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

AntonLV 2 лет назад
Родитель
Сommit
f8f85eb921
67 измененных файлов с 36460 добавлено и 1 удалено
  1. 142 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/classes/BxDolCaptcha.php
  2. 2351 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/classes/BxDolCmts.php
  3. 334 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/classes/BxDolContentInfo.php
  4. 88 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/classes/BxDolContentInfoCmts.php
  5. 351 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/classes/BxDolRecommendation.php
  6. 180 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/classes/BxDolRecommendationQuery.php
  7. 342 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/classes/BxDolReport.php
  8. 1252 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/classes/BxDolSearch.php
  9. 3604 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/classes/BxDolTemplate.php
  10. 1130 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/js/classes/BxDolCmts.js
  11. 2488 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/utils.inc.php
  12. 12 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/version.inc.php
  13. 138 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/modules/base/general/classes/BxBaseModGeneralCmtsSearchResult.php
  14. 599 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/modules/base/general/classes/BxBaseModGeneralMenuViewActions.php
  15. 3814 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/modules/base/general/classes/BxBaseModGeneralModule.php
  16. 232 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/modules/base/general/classes/BxBaseModGeneralSearchResult.php
  17. 54 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/modules/base/groups/classes/BxBaseModGroupsMenuViewActionsAll.php
  18. 2304 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/modules/base/profile/classes/BxBaseModProfileModule.php
  19. 762 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/modules/base/profile/classes/BxBaseModProfileTemplate.php
  20. 508 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/akeeba/s3/src/Signature/V4.php
  21. 7 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/autoload.php
  22. 266 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/chargebee/chargebee-php/lib/ChargeBee/Models/Invoice.php
  23. 17 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/chargebee/chargebee-php/lib/ChargeBee/Models/InvoiceStatementDescriptor.php
  24. 770 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/chargebee/chargebee-php/lib/ChargeBee/Result.php
  25. 10 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/chargebee/chargebee-php/lib/ChargeBee/Version.php
  26. 222 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/chargebee/chargebee-php/lib/init.php
  27. 75 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/composer/autoload_real.php
  28. 226 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/composer/autoload_static.php
  29. 2219 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/composer/installed.json
  30. 375 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/composer/installed.php
  31. 221 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Embera.php
  32. 56 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Assemblrworld.php
  33. 56 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Behance.php
  34. 57 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Docswell.php
  35. 54 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/EchoesHQ.php
  36. 74 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Fader.php
  37. 56 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Fooday.php
  38. 58 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Spotify.php
  39. 56 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Spyke.php
  40. 83 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Tickcounter.php
  41. 57 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/TonicAudio.php
  42. 64 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Twitter.php
  43. 59 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Wistia.php
  44. 256 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/ProviderCollection/DefaultProviderCollection.php
  45. 52 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/ProviderCollection/SlimProviderCollection.php
  46. 615 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/studio/classes/BxDolStudioBuilderPageQuery.php
  47. 2008 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/studio/template/scripts/BxBaseStudioBuilderPage.php
  48. 51 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/css/editor.quill.css
  49. 305 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseAccountForms.php
  50. 96 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseCaptchaHCaptcha.php
  51. 96 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseCaptchaReCAPTCHANew.php
  52. 168 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseCategory.php
  53. 678 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseCmtsServices.php
  54. 412 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseMenu.php
  55. 40 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseMenuSubmenuWithAddons.php
  56. 1171 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBasePage.php
  57. 112 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBasePageHome.php
  58. 596 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseReport.php
  59. 99 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseReportServices.php
  60. 443 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseSearchResult.php
  61. 714 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseServiceAccount.php
  62. 1146 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseServiceProfiles.php
  63. 154 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseServiceRecommendations.php
  64. 1368 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseServices.php
  65. 21 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts_templ/BxTemplMenuSubmenuWithAddons.php
  66. 16 0
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts_templ/BxTemplReportServices.php
  67. 20 1
      upgrade/files/13.1.0.RC2-13.1.0.RC3/files_delete.php

+ 142 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/classes/BxDolCaptcha.php

@@ -0,0 +1,142 @@
+<?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
+ * @{
+ */
+
+/**
+ * CAPTCHA objects.
+ *
+ * Default captcha is stored in 'sys_captcha_default' setting option.
+ *
+ * @section captcha_create Creating the Captcha object:
+ *
+ *
+ * Add record to 'sys_objects_captcha' table:
+ *
+ * - object: name of the captcha object, in the format: vendor prefix, underscore, module prefix, underscore, internal identifier or nothing; for example: bx_blogs - custom captcha in blogs module.
+ * - title: captcha title.
+ * - override_class_name: user defined class name which is derived from one of base captcha classes.
+ * - override_class_file: the location of the user defined class, leave it empty if class is located in system folders.
+ *
+ *
+ * @section example Example of usage
+ *
+ * Display captcha:
+ *
+ * @code
+ *  $oCaptcha = BxDolCaptcha::getObjectInstance(); // get default captcha object instance
+ *  if ($oCaptcha) // check if captcha is available for using
+ *      echo $oCaptcha->display (); // output HTML which will automatically show captcha
+ * @endcode
+ *
+ * Check captcha:
+ *
+ * @code
+ *  $oCaptcha = BxDolCaptcha::getObjectInstance(); // get default captcha object instance
+ *  if ($oCaptcha && $oCaptcha->check ()) // check if captcha is correct
+ *      echo 'captcha is OK';
+ *  else
+ *      echo 'captcha is incorrect'; //
+ * @endcode
+ */
+class BxDolCaptcha extends BxDolFactory implements iBxDolFactoryObject
+{
+    protected $_sObject;
+    protected $_aObject;
+
+    protected $_sKeyPublic;
+    protected $_sKeyPrivate;
+
+    protected $_oTemplate;
+
+    /**
+     * Constructor
+     * @param $aObject array of captcha options
+     */
+    protected function __construct($aObject, $oTemplate)
+    {
+        parent::__construct();
+
+        $this->_sObject = $aObject['object'];
+        $this->_aObject = $aObject;
+        
+        $this->_sKeyPublic = getParam('sys_recaptcha_key_public');
+        $this->_sKeyPrivate = getParam('sys_recaptcha_key_private');
+        
+        if ($oTemplate)
+            $this->_oTemplate = $oTemplate;
+        else
+            $this->_oTemplate = BxDolTemplate::getInstance();
+    }
+
+    /**
+     * Get captcha object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    static public function getObjectInstance($sObject = false)
+    {
+        if (!$sObject)
+            $sObject = getParam('sys_captcha_default');
+
+        if (isset($GLOBALS['bxDolClasses']['BxDolCaptcha!'.$sObject]))
+            return $GLOBALS['bxDolClasses']['BxDolCaptcha!'.$sObject];
+
+        $aObject = BxDolCaptchaQuery::getCaptchaObject($sObject);
+        
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        if (empty($aObject['override_class_name']))
+            return false;
+
+        $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);
+        if (!$o->isAvailable())
+            return false;
+
+        return ($GLOBALS['bxDolClasses']['BxDolCaptcha!'.$sObject] = $o);
+    }
+
+    /**
+     * Display captcha.
+     * @param $bDynamicMode - is AJAX mode or not, if true then HTML code with captcha is loaded dynamically.
+     */
+    public function display ($bDynamicMode = false)
+    {
+        // override this function in particular class
+    }
+
+    /**
+     * Check captcha.
+     */
+    public function check ()
+    {
+        // override this function in particular class
+    }
+
+    /**
+     * Return text entered by user
+     */
+    public function getUserResponse ()
+    {
+        // override this function in particular class
+    }
+
+    /**
+     * Check if captcha is available, like all API keys are specified.
+     */
+    public function isAvailable ()
+    {
+        return !empty($this->_sKeyPublic) && !empty($this->_sKeyPrivate);
+    }
+}
+
+/** @} */

+ 2351 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/classes/BxDolCmts.php

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

+ 334 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/classes/BxDolContentInfo.php

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

+ 88 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/classes/BxDolContentInfoCmts.php

@@ -0,0 +1,88 @@
+<?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 BxDolContentInfoCmts extends BxDolContentInfo
+{
+    protected $_oCmts;
+
+    protected function __construct($sSystem)
+    {
+        parent::__construct($sSystem);
+
+        $this->_oCmts = BxDolCmts::getObjectInstance($this->_aSystem['alert_unit'], 0, false);
+    }
+
+    public function getContentAuthor ($iContentId)
+    {
+        return $this->_oCmts->serviceGetAuthor($iContentId);
+    }
+
+    public function getContentDateAdded ($iContentId)
+    {
+        return $this->_oCmts->serviceGetDateAdded($iContentId);
+    }
+
+    public function getContentDateChanged ($iContentId)
+    {
+        return $this->_oCmts->serviceGetDateChanged($iContentId);
+    }
+
+    public function getContentTitle ($iContentId)
+    {
+        return $this->_oCmts->serviceGetTitle($iContentId);
+    }
+
+    public function getContentThumb ($iContentId)
+    {
+        return $this->_oCmts->serviceGetThumb($iContentId);
+    }
+
+    public function getContentLink ($iContentId)
+    {
+        return $this->_oCmts->serviceGetLink($iContentId);
+    }
+
+    public function getContentText ($iContentId)
+    {
+        return $this->_oCmts->serviceGetText($iContentId);
+    }
+
+    public function getContentInfo ($iContentId, $bSearchableFieldsOnly = true)
+    {
+        return $this->_oCmts->serviceGetInfo($iContentId, $bSearchableFieldsOnly);
+    }
+    
+    public function getContentInfoAPI ($iContentId, $bExtendedUnits = false)
+    {
+        return $this->_oCmts->serviceGetInfoApi($iContentId, $bExtendedUnits);
+    }
+
+    public function getContentSearchResultUnit ($iContentId, $sUnitTemplate = '')
+    {
+        $this->_oCmts->addCssJs();
+        return '<ul class="cmts">' . $this->_oCmts->serviceGetSearchResultUnit($iContentId, $sUnitTemplate) . '</ul>';
+    }
+
+    public function getAll ($aParams = array())
+    {
+        return $this->_oCmts->serviceGetAll($aParams);
+    }
+    
+    public function getSearchableFieldsExtended ()
+    {
+        return $this->_oCmts->serviceGetSearchableFieldsExtended();
+    }
+
+    public function getSearchResultExtended ($aParams, $iStart = 0, $iPerPage = 0, $bFilterMode = false)
+    {
+        return $this->_oCmts->serviceGetSearchResultExtended($aParams, $iStart, $iPerPage, $bFilterMode);
+    }
+}
+
+/** @} */

+ 351 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/classes/BxDolRecommendation.php

@@ -0,0 +1,351 @@
+<?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
+ */
+
+class BxDolRecommendation extends BxDolFactory implements iBxDolFactoryObject
+{
+    protected $_bIsApi;
+
+    protected $_oDb;
+
+    protected $_iObject;
+    protected $_sObject;
+    protected $_aObject;
+    protected $_aCriteria;
+
+    protected $_iProfileId;
+
+    protected $_iReducerAdd;
+    protected $_iReducerIgnore;
+
+    protected $_iPerPageDefault;
+
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+
+        $this->_bIsApi = bx_is_api();
+
+        $this->_aObject = $aObject['object'];
+        $this->_iObject = (int)$this->_aObject['id'];
+        $this->_sObject = $this->_aObject['name'];
+        $this->_aCriteria = $aObject['criteria'];
+
+        $this->_oDb = new BxDolRecommendationQuery();
+        $this->_oDb->init($this->_aObject);
+
+        $this->_iProfileId = bx_get_logged_profile_id();
+
+        $this->_iReducerAdd = 5;
+        $this->_iReducerIgnore = 10;
+
+        $this->_iPerPageDefault = 6;
+    }
+
+    public static function getObjectInstance($sObject)
+    {
+        if(isset($GLOBALS['bxDolClasses']['BxDolRecommendation!' . $sObject]))
+            return $GLOBALS['bxDolClasses']['BxDolRecommendation!' . $sObject];
+
+        $aObject = BxDolRecommendationQuery::getObject($sObject);
+        if(!$aObject || !is_array($aObject) || empty($aObject['object']))
+            return false;
+
+        $sClass = 'BxTemplRecommendation';
+        if(!empty($aObject['object']['class_name'])) {
+            $sClass = $aObject['object']['class_name'];
+            if(!empty($aObject['object']['class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aObject['object']['class_file']);
+        }
+
+        $o = new $sClass($aObject);
+        return ($GLOBALS['bxDolClasses']['BxDolRecommendation!' . $sObject] = $o);
+    }
+
+    public static function updateData($iProfileId = 0)
+    {
+        if(!$iProfileId)
+            $iProfileId = bx_get_logged_profile_id();
+
+        $oCronQuery = BxDolCronQuery::getInstance();
+
+        $sName = 'recommendations_for_' . $iProfileId;
+        if(!$oCronQuery->isTransientJobService($sName))
+            $oCronQuery->addTransientJobService($sName, ['system', 'update_data', [$iProfileId], 'TemplServiceRecommendations']);
+    }
+
+    public function getConnection()
+    {
+        return $this->_aObject['connection'];
+    }
+
+    public function getContentInfo()
+    {
+        return $this->_aObject['content_info'];
+    }
+    
+    public function getCount($iProfileId = 0, $aParams = [])
+    {
+        if(!$iProfileId)
+            $iProfileId = $this->_iProfileId;
+
+        return $this->_oDb->get($iProfileId, $this->_aObject['id'], array_merge($aParams, ['count_only' => true]));
+    }
+
+    public function actionAdd($iProfileId = 0, $iItemId = 0)
+    {
+        if(!$iProfileId)
+            $iProfileId = $this->_iProfileId;
+
+        if(!$iItemId)
+            $iItemId = bx_process_input(bx_get('id'), BX_DATA_INT);
+
+        if(!$this->add($iProfileId, $iItemId))
+            return ['code' => 1, 'msg' => '_sys_txt_error_occured'];
+
+        return ['code' => 0];
+    }
+
+    public function actionIgnore($iProfileId = 0, $iItemId = 0)
+    {
+        if(!$iProfileId)
+            $iProfileId = $this->_iProfileId;
+
+        if(!$iItemId)
+            $iItemId = bx_process_input(bx_get('id'), BX_DATA_INT);
+
+        if(!$this->ignore($iProfileId, $iItemId))
+            return ['code' => 1, 'msg' => '_sys_txt_error_occured'];
+
+        return ['code' => 0];
+    }
+
+    public function add($iProfileId, $iItemId)
+    {
+        $aItem = $this->_oDb->getItem($iProfileId, $this->_iObject, $iItemId);
+
+        /**
+         * Action 'add' moves the item to the end not so much as 'ignore'.
+         * For this purpose (+) is used instead of the (*). Is needed to hide
+         * the recomended item. Isn't removed completely because friend request 
+         * can be rejected.
+         */
+        $iReducer = (int)$aItem['item_reducer'] + $this->_iReducerAdd;
+
+        return $this->_oDb->update($iProfileId, $this->_iObject, $iItemId, ['item_reducer' => $iReducer]);
+    }
+
+    public function ignore($iProfileId, $iItemId)
+    {
+        $aItem = $this->_oDb->getItem($iProfileId, $this->_iObject, $iItemId);
+
+        /**
+         * Action 'ignore' moves the item to the end.
+         * Repeated action do it much more, therefore (*) is used.
+         */
+        $iReducer = !empty($aItem['item_reducer']) ? (int)$aItem['item_reducer'] : 1;
+        $iReducer *= $this->_iReducerIgnore;
+
+        return $this->_oDb->update($iProfileId, $this->_iObject, $iItemId, ['item_reducer' => $iReducer]);
+    }
+
+    public function processCriteria($iProfileId, $iLimit = 100)
+    {
+        $aWeights = array_map(fn($fValue): float => $fValue * $iLimit, $this->_aObject['weights']);
+
+        $aCriterionItems = [];
+        foreach($this->_aCriteria as $sCriterion => $aCriterion) {
+            $aCriterionParams = $this->_getCriterionParams($iProfileId, $aCriterion['params']);
+            $iCriterionWeight = (float)$aCriterion['weight'];           
+
+            switch($aCriterion['source_type']) {
+                case 'sql':
+                    $aCriterionItems[$sCriterion] = [];
+
+                    if(empty($aCriterion['source']))
+                        break;
+
+                    $sQuery = bx_replace_markers($aCriterion['source'], $aCriterionParams);
+                    if($this->_aObject['countable']) {
+                        if(strpos($sQuery, '{order_by}') !== false)
+                            $sQuery = bx_replace_markers($sQuery, ['order_by' => '`value` DESC']);
+                        else
+                            $sQuery .= ' ORDER BY `value` DESC';
+                    }
+                    $sQuery .= ' LIMIT ' . $iLimit;
+
+                    $aCriterionItems[$sCriterion] = $this->_oDb->getPairs($sQuery, 'id', 'value');
+                    break;
+
+                case 'service':
+                    $aCriterionItems[$sCriterion] = BxDolService::callSerialized($aCriterion['source'], $aCriterionParams);
+                    break;
+            }
+        }
+
+        $aResults = [];
+        foreach($aCriterionItems as $sCriteria => $aItems) {
+            $iCriteriaResults = 0;
+            foreach($aItems as $iId => $iValue) {
+                if(!isset($aResults[$iId]))
+                    $aResults[$iId] = 0;
+
+                $aResults[$iId] += $iValue;
+
+                if(++$iCriteriaResults >= $aWeights[$sCriteria])
+                    break;
+            }
+        }
+
+        $this->_oDb->clean($iProfileId, $this->_iObject);
+
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        $oConnection = BxDolConnection::getObjectInstance($this->getConnection());
+        $bConnectionMutual = $oConnection->getType() == BX_CONNECTIONS_TYPE_MUTUAL;
+
+        $aItems = $this->_oDb->getBy(['type' => 'profile_object_ids', 'profile_id' => $iProfileId, 'object_id' => $this->_iObject]);
+        foreach($aItems as $aItem)
+            if(!$oProfile->isActive($aItem['item_id']) || $oConnection->isConnected($iProfileId, $aItem['item_id'], $bConnectionMutual))
+                $this->_oDb->delete($iProfileId, $this->_iObject, $aItem['item_id']);
+
+        $aItemItt = $this->getItemsTypes(array_keys($aResults));
+
+        $iResult = 0;
+        foreach($aResults as $iId => $iValue) {
+            if(!$oProfile->isActive($iId))
+                continue;
+
+            if($this->_oDb->add($iProfileId, $this->_iObject, $iId, (isset($aItemItt[$iId]) ? $aItemItt[$iId] : ''), $iValue))
+                $iResult++;
+        }
+
+        return $iResult;
+    }
+
+    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;
+    }
+
+    protected function _getContextName()
+    {
+        return str_replace('sys_', 'recom_', $this->_sObject); 
+    }
+
+    protected function _getCriterionParams($iProfileId, $aParams)
+    {
+        $aResult = ['profile_id' => $iProfileId];
+        if(empty($aParams))
+            return $aResult;
+
+        if(is_string($aParams))
+            $aParams = unserialize($aParams);
+
+        if(!empty($aParams) && is_array($aParams))
+            $aResult = array_merge($aResult, $aParams);
+
+        return $aResult;
+    }
+
+    /**
+     * The first variant. Isn't used.
+     */
+    public function processCriteriaForSelection($iProfileId, $iStart, $iPerPage, &$bShowPaginate)
+    {
+        if(($iStartGet = bx_get('start')) !== false)
+            $iStart = (int)bx_get('start');
+
+        $aStarts = [];
+        if(($sStartsGet = bx_get('starts')) !== false)
+            $aStarts = array_combine(array_keys($this->_aCriteria), json_decode($sStartsGet, true));
+
+        $aWeights = array_map(fn($fValue): float => $fValue * $iPerPage, $this->_aObject['weights']);
+
+        $aCriterionItems = [];
+        foreach($this->_aCriteria as $sCriterion => $aCriterion) {
+            $aCriterionParams = $this->_getCriterionParams($iProfileId, $aCriterion['params']);
+            $iCriterionWeight = (float)$aCriterion['weight'];
+            $iCriterionStart = isset($aStarts[$sCriterion]) ? (int)$aStarts[$sCriterion] : $iStart * $iCriterionWeight;
+
+            switch($aCriterion['source_type']) {
+                case 'sql':
+                    $aCriterionItems[$sCriterion] = [];
+
+                    if(empty($aCriterion['source']))
+                        break;
+
+                    $sQuery = bx_replace_markers($aCriterion['source'], $aCriterionParams);
+                    if($this->_aObject['countable'])
+                        $sQuery .= ' ORDER BY `value` DESC';
+                    $sQuery .= ' LIMIT ' . $iCriterionStart . ', ' . $iPerPage;
+
+                    $aCriterionItems[$sCriterion] = $this->_oDb->getPairs($sQuery, 'id', 'value');
+                    break;
+
+                case 'service':
+                    //TODO: Realize this!
+                    break;
+            }           
+        }
+
+        $aResults = [];
+        $aHeap = [];
+        $aStats = [];
+        foreach($aCriterionItems as $sCriteria => $aItems) {
+            $iCriteriaAdded = 0;
+            $iCriteriaUpdated = 0;
+            foreach($aItems as $iId => $iValue) {
+                $bAddded = isset($aResults[$iId]);
+                if(!$bAddded && $iCriteriaAdded < $aWeights[$sCriteria]) {
+                    $aResults[$iId] = $iValue;
+                    $iCriteriaAdded++;
+                }
+                else {
+                    if($bAddded) {
+                        $aResults[$iId] += $iValue;
+                        $iCriteriaUpdated++;
+                    }
+                    else
+                        $aHeap[$sCriteria][$iId] = $iValue;
+                }
+            }
+
+            $aStats[$sCriteria] = $iCriteriaAdded + $iCriteriaUpdated;
+        }
+
+        $iResults = count($aResults);
+        if($iResults < $iPerPage && !empty($aHeap)) {
+            foreach($aHeap as $sCriteria => $aItems) {
+                if(!$aItems)
+                    continue;
+
+                arsort($aItems);
+
+                foreach($aItems as $iId => $iValue) {
+                    $aResults[$iId] = $iValue;
+
+                    $aStats[$sCriteria]++;
+                    if(++$iResults == $iPerPage)
+                        break;
+                }
+            }
+        }
+
+        arsort($aResults);
+        return $aResults;
+    }
+}
+
+/** @} */

+ 180 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/classes/BxDolRecommendationQuery.php

@@ -0,0 +1,180 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolRecommendationQuery extends BxDolDb
+{
+    public static $sTableObjects = 'sys_objects_recommendation';
+    public static $sTableCriteria = 'sys_recommendation_criteria';
+    public static $sTableData = 'sys_recommendation_data';
+            
+    protected $_aObject;
+
+    public function __construct()
+    {
+        parent::__construct();
+
+        $this->_aObject = [];
+    }
+
+    static public function getObjects($bActiveOnly = true)
+    {
+        $sWhereClause = "";
+        if($bActiveOnly)
+            $sWhereClause = " AND `active` = 1";
+
+        return BxDolDb::getInstance()->getAllWithKey("SELECT * FROM `" . self::$sTableObjects . "` WHERE 1" . $sWhereClause, 'name');
+    }
+
+    static public function getObject($sName)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $aObject = $oDb->getRow("SELECT * FROM `" . self::$sTableObjects . "` WHERE `name` = :name", ['name' => $sName]);
+        if(!$aObject || !is_array($aObject))
+            return false;
+        
+        $aCriteria = $oDb->getAllWithKey("SELECT * FROM `" . self::$sTableCriteria . "` WHERE `object_id` = :object_id AND `weight` > 0 AND `active` = 1 ORDER BY `weight` DESC", 'name', ['object_id' => $aObject['id']]);
+        if(!$aCriteria || !is_array($aCriteria))
+            return false;
+
+        $aObject['weights'] = $oDb->getPairs("SELECT `name`, `weight` FROM `" . self::$sTableCriteria . "` WHERE `object_id` = :object_id AND `weight` > 0 AND `active` = 1 ORDER BY `weight` DESC", 'name', 'weight', ['object_id' => $aObject['id']]);
+
+        return [
+            'object' => $aObject,
+            'criteria' => $aCriteria
+        ];
+    }
+
+    public function init($aObject = [])
+    {
+        if(empty($aObject) || !is_array($aObject)) 
+            return;
+
+        $this->_aObject = $aObject;
+    }
+
+    public function clean($iProfileId, $iObjectId, $bAll = false)
+    {
+        $sWhereClause = "";
+        if(!$bAll)
+            $sWhereClause = " AND `item_reducer` = 0";
+
+        return $this->query("DELETE FROM `" . self::$sTableData . "` WHERE `profile_id` = :profile_id AND `object_id` = :object_id" . $sWhereClause, [
+            'profile_id' => $iProfileId,
+            'object_id' => $iObjectId
+        ]);
+    }
+
+    public function add($iProfileId, $iObjectId, $iItemId, $sItemType, $iItemValue)
+    {
+        return $this->query("INSERT INTO `" . self::$sTableData . "` (`profile_id`, `object_id`, `item_id`, `item_type`, `item_value`) VALUES (:profile_id, :object_id, :item_id, :item_type, :item_value) ON DUPLICATE KEY UPDATE `item_value` = :item_value", [
+            'profile_id' => $iProfileId,
+            'object_id' => $iObjectId,
+            'item_id' => $iItemId, 
+            'item_type' => $sItemType,
+            'item_value' => $iItemValue
+        ]) !== false;
+    }
+    
+    public function update($iProfileId, $iObjectId, $iItemId, $aSet)
+    {
+        if(empty($aSet) || !is_array($aSet))
+            return false;
+
+        return $this->query("UPDATE `" . self::$sTableData . "` SET " . $this->arrayToSQL($aSet) . " WHERE `profile_id` = :profile_id AND `object_id` = :object_id AND `item_id` = :item_id ", [
+            'profile_id' => $iProfileId,
+            'object_id' => $iObjectId,
+            'item_id' => $iItemId
+        ]) !== false;
+    }
+
+    public function delete($iProfileId, $iObjectId, $iItemId)
+    {
+        return $this->query("DELETE FROM `" . self::$sTableData . "` WHERE `profile_id` = :profile_id AND `object_id` = :object_id AND `item_id` = :item_id ", [
+            'profile_id' => $iProfileId,
+            'object_id' => $iObjectId,
+            'item_id' => $iItemId
+        ]) !== false;
+    }
+
+    public function get($iProfileId, $iObjectId, $aParams = [])
+    {
+        $aMethod = ['name' => 'getPairs', 'params' => [0 => 'query', 1 => 'id', 2 => 'value']];
+
+        $sSelectClause = "`item_id` AS `id`, (`item_value` - `item_reducer`) AS `value`";
+
+        $sWhereClause = "";
+        if(!empty($aParams['type']))
+            $sWhereClause = " AND `item_type` IN (" . $this->implode_escape(is_array($aParams['type']) ? $aParams['type'] : [$aParams['type']]) . ")";
+
+        $sOrderClause = "ORDER BY `value` DESC";
+
+        $sLimitClause = "";
+        if(isset($aParams['start']) && !empty($aParams['per_page']))
+            $sLimitClause = " LIMIT " . $aParams['start'] . ", " . $aParams['per_page'];
+
+        if(isset($aParams['count_only']) && $aParams['count_only'] === true) {
+            $aMethod = ['name' => 'getOne', 'params' => [0 => 'query']];
+
+            $sSelectClause = "COUNT(`id`)";
+            $sOrderClause = "";
+        }
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . " FROM `" . self::$sTableData . "` WHERE `profile_id` = :profile_id AND `object_id` = :object_id AND (`item_value` - `item_reducer`) >= 0" . $sWhereClause . " " . $sOrderClause . " " . $sLimitClause;
+        $aMethod['params'][] = [
+            'profile_id' => $iProfileId,
+            'object_id' => $iObjectId
+        ];
+
+        return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
+    }
+
+    public function getBy($aParams)
+    {
+        $aMethod = ['name' => 'getAll', 'params' => [0 => 'query']];
+
+    	$sFieldsClause = "*"; 
+    	$sJoinClause = $sWhereClause = $sGroupClause = $sLimitClause = $sOrderClause = "";
+
+    	switch($aParams['type']) {              
+            case 'profile_object_ids':
+                $aMethod['params'][1] = [
+                    'profile_id' => $aParams['profile_id'],
+                    'object_id' => $aParams['object_id']
+                ];
+
+                $sWhereClause = " AND `profile_id`=:profile_id AND `object_id`=:object_id";
+                break;
+
+            case 'all':
+                break;
+    	}
+
+        $sOrderClause = $sOrderClause ? "ORDER BY " . $sOrderClause : "";
+        $sLimitClause = $sLimitClause ? "LIMIT " . $sLimitClause : "";
+
+        $aMethod['params'][0] = "SELECT
+            " . $sFieldsClause . "
+            FROM `" . self::$sTableData . "` " . $sJoinClause . "
+            WHERE 1" . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause . " " . $sLimitClause;
+
+        return call_user_func_array([$this, $aMethod['name']], $aMethod['params']);
+    }
+
+    public function getItem($iProfileId, $iObjectId, $iItemId)
+    {
+        return $this->getRow("SELECT * FROM `" . self::$sTableData . "` WHERE `profile_id` = :profile_id AND `object_id` = :object_id AND `item_id` = :item_id", [
+            'profile_id' => $iProfileId,
+            'object_id' => $iObjectId,
+            'item_id' => $iItemId
+        ]);
+    }
+}
+
+/** @} */

+ 342 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/classes/BxDolReport.php

@@ -0,0 +1,342 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+bx_import('BxDolAcl');
+
+define('BX_DOL_REPORT_USAGE_BLOCK', 'block');
+define('BX_DOL_REPORT_USAGE_INLINE', 'inline');
+define('BX_DOL_REPORT_USAGE_DEFAULT', BX_DOL_REPORT_USAGE_BLOCK);
+
+define('BX_DOL_REPORT_STASUS_NEW', 0);
+define('BX_DOL_REPORT_STASUS_IN_PROCESS', 1);
+define('BX_DOL_REPORT_STASUS_PROCESSED', 2);
+/**
+ * Report any content
+ *
+ * Related classes:
+ * - BxDolReportQuery - report database queries
+ * - BxBaseReport - report base representation
+ * - BxTemplReport - custom template representation
+ *
+ * AJAX report for any content.
+ *
+ * To add report section to your feature you need to add a record to 'sys_objects_report' table:
+ *
+ * - id - autoincremented id for internal usage
+ * - name - your unique module name, with vendor prefix, lowercase and spaces are underscored
+ * - table_main - table name where summary reports are stored
+ * - table_track - table name where each report is stored
+ * - is_on - is this report object enabled
+ * - base_url - base URL for reported object
+ * - trigger_table - table to be updated upon each report
+ * - trigger_field_id - trigger_table table field with unique record id, primary key
+ * - trigger_field_author - trigger_table table field with author
+ * - trigger_field_count - trigger_table table field with reports count
+ * - class_name - your custom class name, if you overrride default class
+ * - class_file - your custom class path
+ *
+ * You can refer to BoonEx modules for sample record in this table.
+ *
+ *
+ *
+ * @section example Example of usage:
+ * You can show report in any place, using the following code:
+ * @code
+ * $o = BxDolReport::getObjectInstance('system object name', $iYourEntryId);
+ * if (!$o->isEnabled()) 
+ *     return '';
+ * echo $o->getElementBlock();
+ * @endcode
+ *
+ *
+ * @section acl Memberships/ACL:
+ * - report
+ * - report view
+ *
+ *
+ * @section alerts Alerts:
+ * Alerts type/unit - every module has own type/unit, it equals to ObjectName.
+ * The following alerts are rised:
+ *
+ * - type: 'object name', action: doReport
+ * - type: report, action: do
+ *
+ */
+
+class BxDolReport extends BxDolObject
+{
+    protected $_oTemplate;
+
+    protected $_bUndo;
+    protected $_sBaseUrl;
+
+    protected $_sObjectCmts;
+
+    protected $_sFormObject;
+    protected $_sFormDisplayPost;
+
+    protected $_sTypesPreList;
+    protected $_aTypes;
+
+    protected $_aReport;
+
+    protected function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
+    {
+        parent::__construct($sSystem, $iId, $iInit);
+        if(empty($this->_sSystem))
+            return;
+
+        $this->_oQuery = new BxDolReportQuery($this);
+
+        if ($oTemplate)
+            $this->_oTemplate = $oTemplate;
+        else
+            $this->_oTemplate = BxDolTemplate::getInstance();
+
+        $this->_bUndo = true;
+
+        $this->_sBaseUrl = BxDolPermalinks::getInstance()->permalink($this->_aSystem['base_url']);
+        if(get_mb_substr($this->_sBaseUrl, 0, 4) != 'http')
+            $this->_sBaseUrl = BX_DOL_URL_ROOT . $this->_sBaseUrl;
+
+        $this->_sObjectCmts = $this->_aSystem['object_comment'];
+
+        $this->_sFormObject = 'sys_report';
+        $this->_sFormDisplayPost = 'sys_report_post';
+
+        $this->_sTypesPreList = 'sys_report_types';
+
+        $aTypes = BxDolFormQuery::getDataItems($this->_sTypesPreList);
+        if(!empty($aTypes) && is_array($aTypes))
+            $this->_aTypes = array_keys($aTypes);
+        else
+            $this->_aTypes = array('spam', 'scam', 'fraud', 'nude', 'other');
+
+        $this->_aReport = [];
+    }
+
+    /**
+     * get reports object instanse
+     * @param $sSys report object name
+     * @param $iId associated content id, where report is available
+     * @param $iInit perform initialization
+     * @return null on error, or ready to use class instance
+     */
+    public static function getObjectInstance($sSys, $iId, $iInit = true, $oTemplate = false)
+    {
+        $sKey = 'BxDolReport!' . $sSys . $iId . ($oTemplate ? $oTemplate->getClassName() : '');
+        if(isset($GLOBALS['bxDolClasses'][$sKey]))
+            return $GLOBALS['bxDolClasses'][$sKey];
+
+        $aSystems = self::getSystems();
+        if(!isset($aSystems[$sSys]))
+            return null;
+
+        $sClassName = 'BxTemplReport';
+        if(!empty($aSystems[$sSys]['class_name'])) {
+            $sClassName = $aSystems[$sSys]['class_name'];
+            if(!empty($aSystems[$sSys]['class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aSystems[$sSys]['class_file']);
+        }
+
+        $o = new $sClassName($sSys, $iId, $iInit, $oTemplate);
+        return ($GLOBALS['bxDolClasses'][$sKey] = $o);
+    }
+
+    public static function &getSystems()
+    {
+        $sKey = 'bx_dol_cache_memory_report_systems';
+
+        if(!isset($GLOBALS[$sKey]))
+            $GLOBALS[$sKey] = BxDolDb::getInstance()->fromCache('sys_objects_report', 'getAllWithKey', '
+                SELECT
+                    `id` as `id`,
+                    `name` AS `name`,
+                    `module` AS `module_name`,
+                    `table_main` AS `table_main`,
+                    `table_track` AS `table_track`,
+                    `pruning` AS `pruning`,
+                    `is_on` AS `is_on`,
+                    `base_url` AS `base_url`,
+                    `object_comment` AS `object_comment`,
+                    `trigger_table` AS `trigger_table`,
+                    `trigger_field_id` AS `trigger_field_id`,
+                    `trigger_field_author` AS `trigger_field_author`,
+                    `trigger_field_count` AS `trigger_field_count`,
+                    `class_name` AS `class_name`,
+                    `class_file` AS `class_file`
+                FROM `sys_objects_report`', 'name');
+
+        return $GLOBALS[$sKey];
+    }
+
+    public static function onAuthorDelete ($iAuthorId)
+    {
+        $aSystems = self::getSystems();
+        foreach($aSystems as $sSystem => $aSystem)
+            self::getObjectInstance($sSystem, 0)->getQueryObject()->deleteAuthorEntries($iAuthorId);
+
+        return true;
+    }
+
+    /**
+     * Interface functions for outer usage
+     */
+    public function isUndo()
+    {
+        return (int)$this->_bUndo == 1;
+    }
+
+    public function getBaseUrl()
+    {
+        return $this->_replaceMarkers($this->_sBaseUrl);
+    }
+
+    public function getStatCounter()
+    {
+        $aReport = $this->_oQuery->getReport($this->getId());
+        return $aReport['count'];
+    }
+    
+    public function getCountByStatus($iStatus)
+    {
+        return $this->_oQuery->getReportsCountByStatus($iStatus);
+    }
+
+    public function getSocketName()
+    {
+        return $this->_sSystem . '_reports';
+    }
+
+    /**
+     * Actions functions
+     */
+    public function actionReport()
+    {
+        return echoJson($this->report());
+    }
+
+    public function actionClearReport()
+    {
+       $this->_oQuery->clearReports($this->getId());
+    }
+
+    public function actionGetReportedBy()
+    {
+        if (!$this->isEnabled())
+           return '';
+
+	    if(!$this->isAllowedReportView(true))
+            return $this->msgErrAllowedReportView();
+
+        return $this->_getReportedBy();
+    }
+
+    /**
+     * Permissions functions
+     */
+    public function isAllowedReport($isPerformAction = false)
+    {
+        if(isAdmin())
+            return true;
+
+        return $this->checkAction('report', $isPerformAction);
+    }
+
+    public function msgErrAllowedReport()
+    {
+        return $this->checkActionErrorMsg('report');
+    }
+
+    public function isAllowedReportView($isPerformAction = false)
+    {
+        if(isAdmin())
+            return true;
+
+        return $this->checkAction('report_view', $isPerformAction);
+    }
+    
+    public function changeStatusReport($iStatus, $iAuthorId, $sCmtText)
+    {
+        $iId = $this->getId();
+        $aReport = $this->_oQuery->getDataById($iId);
+        if(!empty($this->_sObjectCmts) && ($oCmts = BxDolCmts::getObjectInstance($this->_sObjectCmts, $aReport['object_id']))) {
+            $oCmts->add(array(
+                'cmt_author_id' => $iAuthorId,
+                'cmt_parent_id' => 0,
+                'cmt_text' => $sCmtText
+            ));
+        }
+        $this->_oQuery->changeStatusReport($iId, $iStatus, $iAuthorId);
+    }
+
+    public function msgErrAllowedReportView()
+    {
+        return $this->checkActionErrorMsg('report_view');
+    }
+
+    /**
+     * Internal functions
+     */
+    protected function _isCount($aVote = array())
+    {
+        if(empty($aReport))
+            $aReport = $this->_getReport();
+
+        return isset($aReport['count']) && (int)$aReport['count'] != 0;
+    }
+
+    protected function _getReport($iObjectId = 0, $bForceGet = false)
+    {
+        if(!empty($this->_aReport) && !$bForceGet)
+            return $this->_aReport;
+
+        if(empty($iObjectId))
+            $iObjectId = $this->getId();
+
+        $this->_aReport = $this->_oQuery->getReport($iObjectId);
+        return $this->_aReport;
+    }
+
+    protected function _isReport($iObjectId = 0, $bForceGet = false)
+    {
+        $aReport = $this->_getReport($iObjectId, $bForceGet);
+
+        return (int)$aReport['count'] > 0;
+    }
+
+    protected function _getTrack($iObjectId, $iAuthorId)
+    {
+        return $this->_oQuery->getTrack($iObjectId, $iAuthorId);
+    }
+
+    protected function _getIconDoReport($bPerformed)
+    {
+    	return $bPerformed && $this->isUndo() ? 'exclamation-circle' : 'exclamation-circle';
+    }
+
+    protected function _getTitleDoReport($bPerformed)
+    {
+        if($bPerformed && $this->isUndo())
+            return ['_report_do_unreport'];
+
+        $sSample = '_' . $this->_sSystem . '_txt_sample_single';
+        if(($$sSample = _t($sSample)) && strcmp($sSample, $$sSample) != 0)
+            return ['_report_do_report_with_sample', $$sSample];
+
+    	return ['_report_do_report'];
+    }
+
+    protected function _getFormObject()
+    {
+        return BxDolForm::getObjectInstance($this->_sFormObject, $this->_sFormDisplayPost);
+    }
+}
+
+/** @} */

+ 1252 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/classes/BxDolSearch.php

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

+ 3604 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/classes/BxDolTemplate.php

@@ -0,0 +1,3604 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+ini_set('pcre.backtrack_limit', 1000000);
+
+define('BX_DOL_TEMPLATE_INJECTIONS_CACHE', 'sys_injections.inc');
+
+define('BX_DOL_TEMPLATE_CHECK_IN_BOTH', 'both');
+define('BX_DOL_TEMPLATE_CHECK_IN_BASE', 'base');
+define('BX_DOL_TEMPLATE_CHECK_IN_TMPL', 'tmpl');
+
+define('BX_DOL_COLOR_BG', 'bg');
+define('BX_DOL_COLOR_FT', 'ft');
+
+define('BX_DOL_PAGE_WIDTH', '1024px');
+
+/**
+ * Page display levels.
+ * Note. Both levels may refer to the same HTML templates. 
+ * 
+ * 'System' level. It uses page's NameIndex and
+ * is mainly used for pages which aren't registered 
+ * in 'sys_objects_page' table. Also it's used 
+ * in 'Injections' engine.  
+ */
+define('BX_PAGE_DEFAULT', 0); ///< default, regular page
+define('BX_PAGE_CLEAR', 2); ///< clear page, without any headers and footers
+define('BX_PAGE_EMBED', 22); ///< page used for embeds
+define('BX_PAGE_POPUP', 44); ///< popup page, without any headers and footers
+define('BX_PAGE_TRANSITION', 150); ///< transition page with redirect to display some msg, like 'please wait', without headers footers
+
+/**
+ * 'Builder based' level. It uses page's Type 
+ * from BxDolPage class and is used for pages 
+ * which are registered in 'sys_objects_page' table.
+ * Changeable in Studio -> Pages Builder -> Settings. 
+ */
+define('BX_PAGE_TYPE_DEFAULT', 1); ///< default, depends on the settins
+define('BX_PAGE_TYPE_DEFAULT_WO_HF', 2); ///< clear page, without any headers and footers
+define('BX_PAGE_TYPE_STANDARD', 3); ///< regular page divided on columns
+define('BX_PAGE_TYPE_APPLICATION', 4); ///< regular page divided on columns with left vertical menu(s) column
+
+/**
+ * Template engine.
+ *
+ * An object of the class allows to:
+ *  1. Manage HTML templates.
+ *  2. Get URL/path for any template image/icon.
+ *  3. Attach CSS/JavaScript files to the output.
+ *  4. Add some content to any template key using Injection engine.
+ *
+ *
+ * Avalable constructions.
+ *  1. &lt;bx_include_auto:template_name.html /&gt; - the content of the file would be inserted. File would be taken from current template if it existes there, and from base directory otherwise.
+ *  2. &lt;bx_include_base:template_name.html /&gt; - the content of the file would be inserted. File would be taken from base directory.
+ *  3. &lt;bx_include_tmpl:template_name.html /&gt; - the content of the file would be inserted. File would be taken from tmpl_xxx directory.
+ *  4. &lt;bx_url_root /&gt; - the value of BX_DOL_URL_ROOT variable will be inserted.
+ *  5. &lt;bx_url_admin /&gt; - the value of BX_DOL_URL_ADMIN variable will be inserted.
+ *  6. &lt;bx_text:_language_key /&gt; - _language_key will be translated using language file(function _t()) and inserted.
+       &lt;bx_text_js:_language_key /&gt; - _language_key will be translated using language file(function _t()) and inserted, use it to insert text into js string.
+       &lt;bx_text_attribute:_language_key /&gt; - _language_key will be translated using language file(function _t()) and inserted, use it to insert text into html attribute.
+ *  7. &lt;bx_image_url:image_file_name /&gt; - image with 'image_file_name' file name will be searched in the images folder of current template.
+ *     If it's not found, then it will be searched in the images folder of base template. On success full URL will be inserted, otherwise an empty string.
+ *  8. &lt;bx_icon_url:icon_file_name /&gt; - the same with &lt;bx_image_url:image_file_name /&gt;, but icons will be searched in the images/icons/ folders.
+ *  9. &lt;bx_injection:injection_name /&gt; - will be replaced with injections registered with the page and injection_name in the `sys_injections`/`sys_injections_admin`/ tables.
+ *  10. &lt;bx_if:tag_name&gt;some_HTML&lt;/bx_if:tag_name&gt; - will be replaced with provided content if the condition is true, and with empty string otherwise.
+ *  11. &lt;bx_repeat:cycle_name&gt;some_HTML&lt;/bx_repeat:cycle_name&gt; - an inner HTML content will be repeated in accordance with received data.
+ *
+ *
+ * Related classes:
+ *  BxDolTemplateAdmin - for processing admin templates.
+ *  Template classes in modules - for processing modiles' templates.
+ *
+ *
+ * Global variables:
+ *  oSysTemplate - is used for template processing in user part.
+ *  oAdmTemplate - is used for template processing in admin part.
+ *
+ *
+ * Add injection:
+ *  1. Register it in the `sys_injections` table or `sys_injections_admin` table for admin panel.
+ *  2. Clear injections cache(sys_injections.inc and sys_injections_admin.inc in cache folder).
+ *
+ *
+ * Predefined template keys to add injections:
+ *  1. injection_head - add injections in the &lt;head&gt; tag.
+ *  2. injection_body - add ingection(attribute) in the &lt;body&gt; tag.
+ *  3. injection_header - add injection inside the &lt;body&gt; tag at the very beginning.
+ *  4. injection_logo_before - add injection at the left of the main logo(inside logo's DIV).
+ *  5. injection_logo_after - add injection at the right of the main logo(inside logo's DIV).
+ *  6. injection_between_logo_top_menu - add injection between logo and top menu.
+ *  7. injection_top_menu_before - add injection at the left of the top menu(inside top menu's DIV).
+ *  8. injection_top_menu_after - add injection at the right of the top menu(inside top menu's DIV).
+ *  13. injection_content_before - add injection just before main content(inside content's DIV).
+ *  14. injection_content_after - add injection just after main content(inside content's DIV).
+ *  15. injection_between_content_footer - add injection between content and footer.
+ *  16. injection_footer_before - add injection at the left of the footer(inside footer's DIV).
+ *  17. injection_footer_after - add injection at the right of the footer(inside footer's DIV).
+ *  18. injection_footer - add injection inside the &lt;body&gt; tag at the very end.
+ *
+ *
+ * Example of usage:
+ * @code
+ *  $oSysTemplate = BxDolTemplate::getInstance();
+ *
+ *  $oSysTemplate->addCss(array('test1.css', 'test2.css'));
+ *  $oSysTemplate->addJs(array('test1.js', 'test2.js'));
+ *  $oSysTemplate->parseHtmlByName('messageBox.html', array(
+ *    'id' => $iId,
+ *     'msgText' => $sText,
+ *     'bx_if:timer' => array(
+ *        'condition' => $iTimer > 0,
+ *        'content' => array(
+ *           'id' => $iId,
+ *           'time' => 1000 * $iTimer,
+ *           'on_close' => $sOnClose,
+ *        )
+ *     ),
+ *     'bx_if:timer' => array(
+ *        array(
+ *           'name' => $sName,
+ *           'title' => $sTitle
+ *        ),
+ *        array(
+ *           'name' => $sName,
+ *           'title' => $sTitle
+ *        )
+ *     )
+ *  ));
+ * @endcode
+ *
+ *
+ * Memberships/ACL:
+ * Doesn't depend on user's membership.
+ *
+ *
+ * Alerts:
+ * no alerts available
+ *
+ */
+class BxDolTemplate extends BxDolFactory implements iBxDolSingleton
+{
+    protected static $_sColorClassPrefix = 'col-';
+    protected static $_sColorClassPrefixBg = 'bg-col-';
+    protected static $_aColors = array(
+    	'red1' => array(216, 9, 96), 
+    	'red1-dark' => array(194, 7, 86), 
+    	'red2' => array(231, 68, 30), 
+    	'red2-dark' => array(207, 60, 25), 
+    	'red3' => array(243, 143, 0), 
+    	'red3-dark' => array(218, 128, 0), 
+    	'green1' => array(96, 174, 0), 
+    	'green1-dark' => array(86, 156, 0), 
+    	'green2' => array(209, 211, 0), 
+    	'green2-dark' => array(186, 188, 0), 
+    	'green3' => array(48, 116, 36), 
+    	'green3-dark' => array(43, 104, 32), 
+    	'blue1' => array(10, 61, 143), 
+    	'blue1-dark' => array(9, 54, 128), 
+    	'blue2' => array(0, 164, 165), 
+    	'blue2-dark' => array(0, 146, 148), 
+    	'blue3' => array(0, 160, 206), 
+    	'blue3-dark' => array(0, 143, 184), 
+    	'gray' => array(97, 97, 97), 
+    	'gray-dark' => array(87, 87, 87)
+    );
+
+    protected static $_aImages;
+    protected static $_sImagesCacheKey;
+    protected static $_iImagesCacheTTL;
+
+    /**
+     * Main fields
+     */
+    protected $_sName;
+    protected $_sPrefix;
+    protected $_sRootPath;
+    protected $_sRootUrl;
+    protected $_sSubPath;
+    protected $_sInjectionsTable;
+    protected $_sInjectionsCache;
+    protected $_sCode;
+    protected $_sCodeKey;
+    protected $_iMix;
+    protected $_sMixKey;
+    protected $_sKeyWrapperHtml;
+    protected $_sFolderHtml;
+    protected $_sFolderCss;
+    protected $_sFolderImages;
+    protected $_sFolderIcons;
+    protected $_aTemplates;
+
+    protected $_aLocations;
+    protected $_aLocationsJs;
+
+    /**
+     * Cache related fields
+     */
+    protected $_bCacheEnable;
+    protected $_sCacheFolderUrl;
+    protected $_sCachePublicFolderUrl;
+    protected $_sCachePublicFolderPath;
+    protected $_sCacheFilePrefix;
+    protected $_aCacheExceptions;
+
+    protected $_bImagesInline;
+    protected $_iImagesMaxSize;
+
+    protected $_bCssLess;
+    protected $_bCssCache;
+    protected $_bCssMinify;
+    protected $_bCssArchive;
+    protected $_sCssLessPrefix;
+    protected $_sCssCachePrefix;
+
+    protected $_bJsLess;
+    protected $_bJsCache;
+    protected $_bJsMinify;
+    protected $_bJsArchive;
+    protected $_sJsCachePrefix;
+
+    protected $aPage;
+    protected $aPageContent;
+    protected $aPageSnapshot = array();
+
+    protected $_oTemplateConfig;
+    protected $_oTemplateFunctions;
+
+    /**
+     * Constructor
+     */
+    protected function __construct($sRootPath = BX_DIRECTORY_PATH_ROOT, $sRootUrl = BX_DOL_URL_ROOT)
+    {
+        if(isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+
+        $this->_sPrefix = 'BxDolTemplate';
+
+        $this->_sRootPath = $sRootPath;
+        $this->_sRootUrl = $sRootUrl;
+        $this->_sInjectionsTable = 'sys_injections';
+        $this->_sInjectionsCache = BX_DOL_TEMPLATE_INJECTIONS_CACHE;
+
+        $this->_sCodeKey = BX_DOL_TEMPLATE_CODE_KEY;
+        $this->_sMixKey = BX_DOL_TEMPLATE_MIX_KEY;
+        list(
+            $this->_sCode, 
+            $this->_sName, 
+            $this->_sSubPath
+        ) = self::retrieveCode($this->_sCodeKey, $this->_sMixKey, $this->_sRootPath);
+
+        $this->_iMix = 0;
+        if(is_array($this->_sCode))
+            list($this->_sCode, $this->_iMix) = $this->_sCode;
+
+        if(!$this->_sSubPath)
+            $this->_sSubPath = 'boonex/' . BX_DOL_TEMPLATE_DEFAULT_CODE . '/';
+
+        if(!file_exists(BX_DIRECTORY_PATH_MODULES . $this->_sSubPath)) // just for 8.0.0-A6 upgrade
+            $this->_sSubPath = 'boonex/uni/';
+
+        if(isset($_GET[$this->_sCodeKey])) {
+            if(BxDolPermalinks::getInstance()->redirectIfNecessary(array($this->_sCodeKey)))
+                exit;
+        }
+
+        $this->_sKeyWrapperHtml = '__';
+        $this->_sFolderHtml = '';
+        $this->_sFolderCss = 'css/';
+        $this->_sFolderImages = 'images/';
+        $this->_sFolderIcons = 'images/icons/';
+        $this->_aTemplates = array('html_tags', 'menu_item_addon', 'menu_item_addon_small', 'menu_item_addon_middle');
+
+        $this->addLocation('system', $this->_sRootPath, $this->_sRootUrl);
+
+        $this->addLocationJs('system_inc_js', BX_DIRECTORY_PATH_INC . 'js/' , BX_DOL_URL_ROOT . 'inc/js/');
+        $this->addLocationJs('system_inc_js_classes', BX_DIRECTORY_PATH_INC . 'js/classes/' , BX_DOL_URL_ROOT . 'inc/js/classes/');
+        $this->addLocationJs('system_plugins_public', BX_DIRECTORY_PATH_PLUGINS_PUBLIC, BX_DOL_URL_PLUGINS_PUBLIC);
+
+        $this->_bCacheEnable = !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'];
+
+        $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;
+
+        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 = 'db_layout_images_' . $this->_sCode .  '_' . bx_site_hash('images') . '.php';
+        self::$_aImages = BxDolDb::getInstance()->getDbCacheObject()->getData(self::$_sImagesCacheKey);
+        if(!self::$_aImages)
+            self::$_aImages = [];
+
+        bx_alert('system', 'get_layout_images', 0, false, [
+            'code' => $this->_sCode,
+            'override_result' => &self::$_aImages
+        ]);
+    }
+
+    protected function saveImages()
+    {
+        if(!self::$_iImagesCacheTTL)
+            $this->initImages();
+
+        BxDolDb::getInstance()->getDbCacheObject()->setData(self::$_sImagesCacheKey, self::$_aImages, self::$_iImagesCacheTTL);
+    }
+
+    protected function getInjectionsData ()
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $aInjections = $oDb->getAll("SELECT `page_index`, `name`, `key`, `type`, `data`, `replace` FROM `" . $this->_sInjectionsTable . "` WHERE `active`='1'");
+        if (!$aInjections)
+            return array();
+
+        foreach ($aInjections as $aInjection)
+            $aInjections['page_' . $aInjection['page_index']][$aInjection['key']][] = $aInjection;
+
+        return $aInjections;
+    }
+
+    /**
+     * Set page name index
+     * @param int $i name index
+     */
+    function setPageNameIndex($i)
+    {
+        $this->aPage['name_index'] = $i;
+    }
+
+    /**
+     * Get page name index
+     * @return int $i name index
+     */
+    function getPageNameIndex()
+    {
+        return isset($this->aPage['name_index']) ? (int)$this->aPage['name_index'] : 0;
+    }
+
+	/**
+     * Set page type
+     * @param int $i page type
+     */
+    function setPageType($i)
+    {
+        $this->aPage['type'] = $i;
+    }
+    
+    /**
+     * Set page url
+     * @param string $s page url
+     */
+    function setPageUrl($s)
+    {
+        $this->aPage['url'] = $s;
+    }
+
+    /**
+     * Get page type
+     * @return int $i page type
+     */
+    function getPageType()
+    {
+        $iType = BX_PAGE_TYPE_DEFAULT;
+        if(isset($this->aPage['type']))
+            $iType = (int)$this->aPage['type'];
+
+        if($iType == BX_PAGE_TYPE_DEFAULT) 
+            $iType = (int)getParam('sys_pt_default_' . (isLogged() ? 'member' : 'visitor'));
+
+        return $iType;
+    }
+
+    /**
+     * Set page header
+     * @param string $s page header
+     */
+    function setPageHeader($s)
+    {
+        $this->aPage['header'] = $s;
+    }
+
+    /**
+     * Get page header
+     * @return string $s page header
+     */
+    function getPageHeader()
+    {
+        return $this->aPage['header'];
+    }
+
+    /**
+     * Set page params. Available page params are: name_index, header
+     * @param array $a page params
+     */
+    function setPageParams($a)
+    {
+        if (!empty($this->aPage))
+            $this->aPage = array_merge($this->aPage, $a);
+        else
+            $this->aPage = $a;
+    }
+
+    /**
+     * Get page params.
+     * @return array $a page params
+     */
+    function getPageParams()
+    {
+        return $this->aPage;
+    }
+
+    /**
+     * Set page 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() : '';
+    }
+    /**
+     * Add image in JS output.
+     *
+     * @param array $aImages an array of image descriptors.
+     *                       The descriptor is a key/value pear in the array of descriptors.
+     */
+    function addJsImage($aImages)
+    {
+        if(!is_array($aImages))
+            return;
+
+        foreach($aImages as $sKey => $sFile) {
+            $sUrl = $this->getImageUrl($sFile);
+            if(empty($sUrl))
+                continue;
+
+            $this->aPage['js_images'][$sKey] = $sUrl;
+        }
+    }
+    /**
+     * Add icon in JS output.
+     *
+     * @param array $aIcons an array of icons descriptors.
+     *                      The descriptor is a key/value pear in the array of descriptors.
+     */
+    function addJsIcon($aIcons)
+    {
+        if(!is_array($aIcons))
+            return;
+
+        foreach($aIcons as $sKey => $sFile) {
+            $sUrl = $this->getIconUrl($sFile);
+            if(empty($sUrl))
+                continue;
+
+            $this->aPage['js_images'][$sKey] = $sUrl;
+        }
+    }
+	/**
+	 * Add CSS style.
+	 *
+	 * @param string $sName CSS class name.
+	 * @param string $sContent CSS class styles.
+	 */
+	function addCssStyle($sName, $sContent)
+	{
+		$this->aPage['css_styles'][$sName] = $sContent;
+	}
+    /**
+     * Set page keywords.
+     *
+     * @param mixed  $mixedKeywords necessary page keywords(string - single keyword, array - an array of keywords).
+     * @param string $sDevider      - string devider.
+     */
+    function addPageKeywords($mixedKeywords, $sDevider = ',')
+    {
+        if(is_string($mixedKeywords))
+            $mixedKeywords = strpos($mixedKeywords, $sDevider) !== false ? explode($sDevider, $mixedKeywords) : array($mixedKeywords);
+
+        foreach($mixedKeywords as $iKey => $sValue)
+            $mixedKeywords[$iKey] = trim($sValue);
+
+        $this->aPage['keywords'] = isset($this->aPage['keywords']) && is_array($this->aPage['keywords']) ? array_merge($this->aPage['keywords'], $mixedKeywords) : $mixedKeywords;
+    }
+    /**
+     * Set page locatoin coordinates.
+     *
+     * @param $fLat latitude
+     * @param $fLng longitude
+     */
+    function addPageMetaLocation($fLat, $fLng, $sCountryCode)
+    {
+        $this->aPage['location'] = array('lat' => $fLat, 'lng' => $fLng, 'country' => $sCountryCode);
+    }
+    /**
+     * Set page meta image.
+     *
+     * @param $sImageUrl meta image url
+     */
+    function addPageMetaImage($sImageUrl)
+    {
+        $this->aPage['image'] = $sImageUrl;
+    }
+    /**
+     * Set page rss link.
+     *
+     * @param $sTitle - rss feed title
+     * @param $sUrl - rss feed URL
+     */
+    function addPageRssLink($sTitle, $sUrl)
+    {
+        if (!isset($this->aPage['rss']))
+            $this->aPage['rss'] = array('title' => $sTitle, 'url' => $sUrl);
+        else
+            $this->aPage['rss'] = false;
+    }
+    /**
+     * Returns page meta info, like meta keyword, meta description, location, etc
+     */
+    function getMetaInfo()
+    {
+        $sRet = '';
+
+        $oPage = BxDolPage::getObjectInstanceByURI();
+        $bPage = $oPage !== false;
+
+        // general meta tags
+        if (!empty($this->aPage['keywords']) && is_array($this->aPage['keywords']))
+            $sRet .= '<meta name="keywords" content="' . bx_html_attribute(implode(',', $this->aPage['keywords'])) . '" />';
+
+        $sDescription = '';
+        if(!empty($this->aPage['description']) && is_string($this->aPage['description']))
+            $sDescription = $this->aPage['description'];
+        if(!$sDescription && $bPage)
+            $sDescription = $oPage->getMetaDescription();
+        $bDescription = !empty($sDescription);
+
+        if ($bDescription)
+            $sRet .= '<meta name="description" content="' . bx_html_attribute($sDescription) . '" />';
+
+        // location
+        if (!empty($this->aPage['location']) && isset($this->aPage['location']['lat']) && isset($this->aPage['location']['lng']) && isset($this->aPage['location']['country']))
+            $sRet .= '
+                <meta name="ICBM" content="' . $this->aPage['location']['lat'] . ';' . $this->aPage['location']['lng'] . '" />
+                <meta name="geo.position" content="' . $this->aPage['location']['lat'] . ';' . $this->aPage['location']['lng'] . '" />
+                <meta name="geo.region" content="' . bx_html_attribute($this->aPage['location']['country']) . '" />';
+
+        //set meta[image] value
+        if(empty($this->aPage['image'])) {
+            // use cover image if exists
+            if($bPage && ($aCover = $oPage->getPageCoverImage()))
+                $this->aPage['image'] = BxDolCover::getInstance($this)->getCoverImageUrl($aCover);
+
+            // use system Apple/Android icons if exists
+            if(empty($this->aPage['image'])) {
+                $sImgUrl = '';
+                $iImgSquare = 0;
+                $oImgStorage = BxDolStorage::getObjectInstance(BX_DOL_STORAGE_OBJ_IMAGES);
+                foreach(['icon_apple', 'icon_android', 'icon_android_splash'] as $sIcon) {
+                    $iIcon = (int)getParam('sys_site_' . $sIcon);
+                    if(!$iIcon)
+                        continue;
+
+                    $sUrl = $oImgStorage->getFileUrlById($iIcon);
+                    if(!$sUrl)
+                        continue;
+
+                    if(($aSize = BxDolImageResize::getImageSize($sUrl)) !== false && ($iSquare = (int)$aSize['w'] * (int)$aSize['h']) > $iImgSquare) {
+                        $sImgUrl = $sUrl;
+                        $iImgSquare = $iSquare;
+                    }
+                }
+
+                if(!empty($sImgUrl))
+                    $this->aPage['image'] = $sImgUrl;
+            }
+        }
+
+        // facebook / twitter
+        $bPageImage = !empty($this->aPage['image']);
+        $sRet .= '<meta name="twitter:card" content="' . ($bPageImage ? 'summary_large_image' : 'summary') . '" />';
+        if ($bPageImage)
+            $sRet .= '<meta property="og:image" content="' . $this->aPage['image'] . '" />';
+        $sRet .= '<meta property="og:title" content="' . (isset($this->aPage['header']) ? bx_html_attribute(strip_tags($this->aPage['header'])) : '') . '" />';
+        $sRet .= '<meta property="og:description" content="' . ($bDescription ? bx_html_attribute($sDescription) : '') . '" />';
+
+        // Smart App Banner
+        if (getParam('smart_app_banner') && false === strpos($_SERVER['HTTP_USER_AGENT'], 'UNAMobileApp')) {
+            if ($sAppIdIOS = getParam('smart_app_banner_ios_app_id'))
+                $sRet .= '<meta name="apple-itunes-app" content="app-id=' . $sAppIdIOS . '" />';
+        }
+
+        // RSS
+        $oFunctions = BxTemplFunctions::getInstance();
+        $sRet .= $oFunctions->getManifests();
+        $sRet .= $oFunctions->getMetaIcons();
+        
+        if (!empty($this->aPage['rss']) && !empty($this->aPage['rss']['url']))
+            $sRet .= '<link rel="alternate" type="application/rss+xml" title="' . bx_html_attribute($this->aPage['rss']['title'], BX_ESCAPE_STR_QUOTE) . '" href="' . $this->aPage['rss']['url'] . '" />';
+        
+        $sRet .= "<link rel=\"alternate\" type=\"application/json+oembed\" href=\"" . BX_DOL_URL_ROOT ."em.php?url=" . urlencode($_SERVER["REQUEST_URI"]) . "&format=json\" title=\"". (isset($this->aPage['header']) ? bx_html_attribute(strip_tags($this->aPage['header'])) : '') . "\" />";
+        
+        if (!empty($this->aPage['url'])){
+            $sRet .= '<link rel="canonical" href="' . bx_absolute_url(BxDolPermalinks::getInstance()->permalink($this->aPage['url'])) . '" />';
+        }
+        
+        return $sRet;
+    }
+    /**
+     * Get template, which was loaded earlier.
+     * @see method this->loadTemplates and field this->_aTemplates
+     *
+     * @param  string $sName - template name.
+     * @return string template's content.
+     */
+    public function getTemplate($sName)
+    {
+        return $this->_aTemplates[$sName];
+    }
+
+    /**
+     * Get template functions object.
+     * @see BxBaseFunctions
+     */
+    public function getTemplateFunctions()
+    {
+        return $this->_oTemplateFunctions;
+    }
+
+    /**
+     * Get image MIME type.
+     * 
+     * @param string $sExtension - image file's extension.
+     * @return string with MIME type. 
+     */
+    function getImageMimeType($sExtension)
+    {
+    	$sExtension = strtolower($sExtension);
+    
+    	$sResult = '';
+    	switch($sExtension) {
+    		case 'svg':
+    			$sResult = 'svg+xml';
+    			break;
+    
+    		default:
+    			$sResult = $sExtension;
+    	}
+    
+    	return 'data:image/' . $sResult;
+    }
+
+    /**
+     * Get icon template in dependence of a value, provided in $mixedId.
+     * 
+     * @param  mixed $mixedId numeric id from Storage, string with template's file name or string with font icon.
+     */
+	public function getIcon($mixedId, $aParams = array())
+    {
+        return $this->_getImage('icon', $mixedId, $aParams);
+    }
+
+    /**
+     * Get image template in dependence of a value, provided in $mixedId.
+     * 
+     * @param  mixed $mixedId numeric id from Storage, string with template's file name or string with font icon.
+     */
+    public function getImage($mixedId, $aParams = array())
+    {
+        return $this->_getImage('image', $mixedId, $aParams);
+    }
+
+	protected function _getImage($sType, $mixedId, $aParams = array())
+    {
+        $sUrl = "";
+        $aType2Method = array('image' => 'getImageUrl', 'icon' => 'getIconUrl');
+
+        //--- Check in System Storage.
+        if(is_numeric($mixedId) && (int)$mixedId > 0) {
+        	$sStorage = BX_DOL_STORAGE_OBJ_IMAGES;
+        	if(!empty($aParams['storage'])) {
+        		$sStorage = $aParams['storage'];
+        		unset($aParams['storage']);
+        	}
+
+            if(($sResult = BxDolStorage::getObjectInstance($sStorage)->getFileUrlById((int)$mixedId)) !== false)
+                $sUrl = $sResult;
+        }
+
+        //--- Check in template folders.
+        if($sUrl == "" && is_string($mixedId) && strpos($mixedId, '.') !== false)
+            $sUrl = $this->{$aType2Method[$sType]}($mixedId);
+
+        if($sUrl != "")
+            return $this->parseImage($sUrl, array(
+                'class' => isset($aParams['class']) && !empty($aParams['class']) ? $aParams['class'] : '',
+            	'alt' => isset($aParams['alt']) && !empty($aParams['alt']) ? $aParams['alt'] : ''
+            ));
+
+        //--- Use iconic font.
+        return $this->parseIcon($mixedId, $aParams);
+    }
+
+    /**
+     * Get full URL for the icon.
+     *
+     * @param  string $sName    icon's file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string full URL.
+     */
+    function getIconUrl($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        $sContent = "";
+        if(($sContent = $this->_getInlineData('icon', $sName, $sCheckIn)) !== false)
+            return $sContent;
+
+        return $this->_getAbsoluteLocation('url', $this->_sFolderIcons, $sName, $sCheckIn);
+    }
+    /**
+     * Get absolute Path for the icon.
+     *
+     * @param  string $sName    - icon's file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string absolute path.
+     */
+    function getIconPath($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        return $this->_getAbsoluteLocation('path', $this->_sFolderIcons, $sName, $sCheckIn);
+    }
+
+    /**
+     * Get image/icon by name automatically. Cache item description:
+     * [
+     *      'v' => value (name, url or source),
+     *      'c' => classes list divided with comma (,)
+     *      't' => parse type (ic - icon, im - image, sc - source)
+     * ]
+     * Cached images can be overwritten by listening 'system' - 'get_layout_images' alert.
+     * 
+     * @param string $sName unique name. The following format can be used: name|classes. 
+     * Where name and classes can consists of multiple parts (divided with comma (,) in HTML variant). For example, 
+     *     in PHP: $this->getImageAuto('far star|class1 class2');
+     *     in HTML: <bx_image_auto:far,star|class1,class2 />
+     * @param boolean $bWrapped wrap in HTML tag or not.
+     * @param string $sCheckIn where the content would be searched(base, template, both)
+     * @return string icon/image value (name, url or source) or final HTML code.
+     */
+    function getImageAuto($sName, $bWrapped = true, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        $sDivParts = '|';
+        $sDivItems = ',';
+
+        $sKey = md5($sName);
+
+        $sClasses = '';
+        if(strpos($sName, $sDivParts) !== false)
+            list($sName, $sClasses) = explode($sDivParts, $sName);
+
+        if(strpos($sName, $sDivItems) !== false)
+            $sName = implode(' ', explode($sDivItems, $sName));
+
+        if(!isset(self::$_aImages[$sKey])) {
+            $aResult = [
+                'v' => $sName,
+                't' => 'ic',
+                'c' => $sClasses
+            ];
+
+            $sUrl = '';
+            foreach(['Image', 'Icon'] as $sType) 
+                foreach(['svg', 'png', 'jpg', 'gif'] as $sExt)
+                    if(($sUrl = $this->{'get' . $sType . 'Url'}($sName . '.' . $sExt, $sCheckIn)) != '') {
+                        $aResult = [
+                            'v' => $sUrl,
+                            't' => 'im',
+                            'c' => $sClasses
+                        ];
+                        break 2;
+                    }
+
+            self::$_aImages[$sKey] = $aResult;
+
+            $this->saveImages();
+        }
+
+        if(!self::$_aImages[$sKey]['v'])
+            return '';
+
+        if(!$bWrapped || self::$_aImages[$sKey]['t'] == 'sc')
+            return self::$_aImages[$sKey]['v'];       
+
+        $aAttrs = [];
+        if(self::$_aImages[$sKey]['c'] != '')
+            $aAttrs['class'] = implode(' ', explode($sDivItems, self::$_aImages[$sKey]['c']));
+
+        $aType2Method = ['ic' => 'parseIcon', 'im' => 'parseImage'];
+        return $this->{$aType2Method[self::$_aImages[$sKey]['t']]}(self::$_aImages[$sKey]['v'], $aAttrs);
+    }
+
+    /**
+     * Get full URL for the image.
+     *
+     * @param  string $sName    - images's file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string full URL.
+     */
+    function getImageUrl($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        $sContent = "";
+        if(($sContent = $this->_getInlineData('image', $sName, $sCheckIn)) !== false)
+            return $sContent;
+
+        return $this->_getAbsoluteLocation('url', $this->_sFolderImages, $sName, $sCheckIn);
+    }
+    /**
+     * Get absolute Path for the image.
+     *
+     * @param  string $sName    - image's file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string absolute path.
+     */
+    function getImagePath($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        return $this->_getAbsoluteLocation('path', $this->_sFolderImages, $sName, $sCheckIn);
+    }
+    /**
+     * Get full URL of CSS file.
+     *
+     * @param  string $sName    - CSS file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string full URL.
+     */
+    function getCssUrl($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        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_name();
+                break;
+            case 'lang_direction':
+                $sRet = bx_lang_direction();
+                break;
+            case 'lang_country':
+                if (!($sRet = BxDolLanguages::getInstance()->getLangCountryCode()))
+                    $sRet = bx_lang_name();
+                break;                
+            case 'main_logo':
+                $sRet = BxTemplFunctions::getInstance()->getMainLogo();
+                break;
+            case 'informer':
+                $oInformer = BxDolInformer::getInstance($this);
+                $sRet = $oInformer ? $oInformer->display() : '';
+                break;
+            case 'cover':
+            	$oCover = BxDolCover::getInstance($this);
+
+            	$bCover = $oCover->isEnabled();
+            	if($bCover && ($oPage = BxDolPage::getObjectInstanceByURI()) !== false) {
+                    $bCover = $oPage->isPageCover();
+                    if($bCover && !$oCover->isCover()) {
+                        $aCover = $oPage->getPageCoverImage();
+                        if(!empty($aCover))
+                            $oCover->setCoverImageUrl($aCover);
+                    }
+            	}
+
+                $sRet = $bCover ? $oCover->display() : $oCover->displayEmpty();
+                break;
+            case 'site_submenu_class':
+                $oMenu = BxDolMenu::getObjectInstance('sys_site_submenu');
+                if($oMenu)
+                    $sRet = $oMenu->getClass();
+                break;
+            case 'site_submenu_hidden':
+                $sClass = 'bx-menu-main-bar-hidden';
+
+                $oPage = BxDolPage::getObjectInstanceByURI();
+                if($oPage !== false && !$oPage->isVisiblePageSubmenu()) {
+                    $sRet = $sClass;
+                    break;
+                }
+
+                $oMenu = BxDolMenu::getObjectInstance('sys_site_submenu');
+                if($oMenu !== false && !$oMenu->isVisible()) {
+                    $sRet = $sClass;
+                    break;
+                }
+
+                break;
+            case 'dol_images':
+                $sRet = $this->_processJsImages();
+                break;
+            case 'dol_lang':
+                $sRet = $this->_processJsTranslations();
+                break;
+            case 'dol_options':
+                $sRet = $this->_processJsOptions();
+                break;
+            case 'copyright':
+                $sRet = _t( '_copyright', date('Y') ) . getVersionComment();
+                break;
+            case 'copyright_attr':
+                $sRet = bx_html_attribute(_t('_copyright', date('Y')));
+                break;
+            case 'extra_js':
+                $sRet = empty($this->aPage['extra_js']) ? '' : $this->aPage['extra_js'];
+                break;
+            case 'is_profile_page':
+                $sRet = (defined('BX_PROFILE_PAGE')) ? 'true' : 'false';
+                break;
+			case 'system_js_requred':
+                $sRet = _t('_sys_javascript_requred');
+                break;
+            case 'included_css':
+                $sRet = json_encode($this->getIncludedUrls('css_compiled'));
+                break;
+            case 'included_js':
+                $sRet = json_encode($this->getIncludedUrls('js_compiled'));
+                break;
+            case 'base':
+                $sRet = bx_convert_array2attrs($this->aPage['base']);
+                break;
+            case 'class_name':
+                $sRet = $this->getCssClassName();
+    
+                if (preg_match('/^[A-Za-z0-9_\-]+$/', bx_get('i')))
+                    $sRet .= ' bx-page-' . bx_get('i');
+
+                if(!empty($this->_iMix)) {
+                    $aMix = BxDolDb::getInstance()->getParamsMix($this->_iMix);
+                    if(isset($aMix['dark']) && (int)$aMix['dark'] == 1)
+                        $sRet .= ' dark';
+                }
+                break;
+			case 'css_media_phone':
+			case 'css_media_phone2':
+			case 'css_media_tablet':
+			case 'css_media_tablet2':
+			case 'css_media_desktop':
+                $aData = json_decode(getParam('sys_css_media_classes'), true);
+				$sKey = str_replace('css_media_', '', $sKey);
+                $sRet = $aData[$sKey];
+                break;
+            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;
+            default:
+                $sRet = ($sTemplAdd = BxTemplFunctions::getInstance()->TemplPageAddComponent($sKey)) !== false ? $sTemplAdd : $aKeyWrappers['left'] . $sKey . $aKeyWrappers['right'];
+        }
+
+        if($bProcessInjection)
+            $sRet = $this->processInjection($this->getPageNameIndex(), $sKey, $sRet);
+
+        return $sRet;
+    }
+
+    /**
+     * Parse tag <A>
+     * 
+     * @param string $sLink link URL
+     * @param string $sContent link content
+     * @param array $aAttrs an array of key => value pairs
+     */
+    function parseLink($sLink, $sContent, $aAttrs = array())
+    {
+        $sAttrs = '';
+        foreach($aAttrs as $sKey => $sValue)
+            $sAttrs .= ' ' . $sKey . '="' . bx_html_attribute($sValue) . '"';
+
+        return '<a href="' . $sLink . '"' . $sAttrs . '>' . $sContent . '</a>';
+    }
+
+    /**
+     * Parse tag <A> using provided HTML template
+     * 
+     * @param string $sName template's file name
+     * @param string $sLink link URL
+     * @param string $sContent link content
+     * @param array $aAttrs an array of key => value pairs
+     */
+    function parseLinkByName($sName, $sLink, $sContent, $aAttrs = array())
+    {
+        $sAttrs = '';
+        foreach($aAttrs as $sKey => $sValue)
+            $sAttrs .= ' ' . $sKey . '="' . bx_html_attribute($sValue) . '"';
+
+        return $this->parseHtmlByName($sName, array(
+            'href' => $sLink,
+            'attrs' => $sAttrs,
+            'content' => $sContent
+        ));
+    }
+
+    /**
+     * Parse tag <BUTTON>
+     * 
+     * @param string $sContent link content
+     * @param array $aAttrs an array of key => value pairs
+     */
+    function parseButton($sContent, $aAttrs = array())
+    {
+        $sAttrs = '';
+        foreach($aAttrs as $sKey => $sValue)
+            $sAttrs .= ' ' . $sKey . '="' . bx_html_attribute($sValue) . '"';
+
+        return '<button' . $sAttrs . '>' . $sContent . '</button>';
+    }
+
+    /**
+     * Parse tag <IMG>
+     * 
+     * @param string $sLink URL to image source 
+     * @param array $aAttrs an array of key => value pairs
+     */
+    function parseImage($sLink, $aAttrs = array())
+    {
+        $sAttrs = '';
+        foreach($aAttrs as $sKey => $sValue)
+            $sAttrs .= ' ' . $sKey . '="' . bx_html_attribute($sValue) . '"';
+
+        return '<img src="' . $sLink . '"' . $sAttrs . ' />';
+    }
+
+    /**
+     * Parse font based icon in <I> tag
+     * 
+     * @param string $sName font icon name
+     * @param array $aAttrs an array of key => value pairs
+     */
+    function parseIcon($sName, $aAttrs = array())
+    {
+        $aIcons = BxTemplFunctions::getInstance()->getIcon($sName, $aAttrs);
+        if($aIcons[0] != '')
+            $aIcons[0] = '';
+
+        return implode($aIcons);
+    }
+
+    function getCacheFilePrefix($sType)
+    {
+    	$sResult = '';
+    	switch($sType) {
+            case 'template':
+                $sResult = $this->_sCacheFilePrefix;
+                break;
+
+            case 'less':
+                $sResult = $this->_sCssLessPrefix;
+                break;
+
+            case 'css':
+                $sResult = $this->_sCssCachePrefix;
+                break;
+
+            case 'js':
+                $sResult = $this->_sJsCachePrefix;
+                break;
+    	}
+
+    	return $sResult;
+    }
+    /**
+     * Get cache object for templates
+     * @return cache class instance
+     */
+    function getTemplatesCacheObject ()
+    {
+        $sCacheEngine = getParam('sys_template_cache_engine');
+        $oCacheEngine = bx_instance('BxDolCache' . $sCacheEngine);
+        if(!$oCacheEngine->isAvailable())
+            $oCacheEngine = bx_instance('BxDolCacheFileHtml');
+        return $oCacheEngine;
+    }
+    /**
+     * Get template from cache if it's enabled.
+     *
+     * @param  string  $sName               template name
+     * @param  string  $aVariables          key/value pairs. key should be the same as template's key, but without prefix and postfix.
+     * @param  mixed   $mixedKeyWrapperHtml - key wrapper(string value if left and right parts are the same, array(0 => left, 1 => right) otherwise).
+     * @param  string  $sCheckIn            where the content would be searched(base, template, both)
+     * @param  boolean $bEvaluate           need to evaluate the template or not.
+     * @return string  result of operation or false on failure.
+     */
+    function getCached($sName, &$aVariables, $mixedKeyWrapperHtml = null, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH, $bEvaluate = true)
+    {
+        // initialization
+
+        if(!$this->_bCacheEnable)
+            return false;
+
+        if (in_array($sName, $this->_aCacheExceptions))
+           return false;
+        
+        $sAbsolutePath = $this->_getAbsoluteLocation('path', $this->_sFolderHtml, $sName, $sCheckIn);
+        if(empty($sAbsolutePath))
+            return false;
+
+        $oCacheEngine = $this->getTemplatesCacheObject ();
+        $isFileBasedEngine = $bEvaluate && method_exists($oCacheEngine, 'getDataFilePath');
+
+        // try to get cached content
+
+        $sCacheVariableName = "a";
+        $sCacheKey = $this->_getCacheFileName('html', $sAbsolutePath) . '.php';
+        if ($isFileBasedEngine)
+            $sCacheContent = $oCacheEngine->getDataFilePath($sCacheKey);
+        else
+            $sCacheContent = $oCacheEngine->getData($sCacheKey);
+
+
+        // recreate cache if it is empty
+
+        if ($sCacheContent === null && ($sContent = file_get_contents($sAbsolutePath)) !== false && ($sContent = $this->_compileContent($sContent, "\$" . $sCacheVariableName, 1, $aVariables, $mixedKeyWrapperHtml)) !== false) {
+            if (false === $oCacheEngine->setData($sCacheKey, trim($sContent)))
+                return false;
+
+            if ($isFileBasedEngine)
+                $sCacheContent = $oCacheEngine->getDataFilePath($sCacheKey);
+            else
+                $sCacheContent = $sContent;
+        }
+
+        if ($sCacheContent === null)
+            return false;
+
+        // return simple cache content
+
+        if(!$bEvaluate)
+            return $sCacheContent;
+
+        // return evaluated cache content
+
+        ob_start();
+
+        $$sCacheVariableName = &$aVariables;
+
+        if ($isFileBasedEngine)
+            include($sCacheContent);
+        else
+            eval('?'.'>' . $sCacheContent);
+
+        $sContent = ob_get_clean();
+
+        return $sContent;
+    }
+
+    /**
+     * Add JS file(s) to global output.
+     *
+     * @param  mixed          $mixedFiles string value represents a single JS file name. An array - array of JS file names.
+     * @param  boolean        $bDynamic   in the dynamic mode JS file(s) are not included to global output, but are returned from the function directly.
+     * @return boolean/string result of operation.
+     */
+    function addJs($mixedFiles, $bDynamic = false)
+    {
+        return $this->_processFiles('js', 'add', $mixedFiles, $bDynamic);
+    }
+
+    function addJsPreloaded($aFiles, $sCallback = false, $sCondition = false, $sConditionElseCallback = false)
+    {
+        if(!$aFiles)
+            return '';
+
+        if(!is_array($aFiles))
+            $aFiles = [$aFiles];
+
+        $sMaskLoad = "bx_get_scripts(%s);";
+        $sMaskLoadWithCallback = "bx_get_scripts(%s, function() {%s});";
+
+        $sMaskCondition = "if(%s) {%s}";
+        $sMaskConditionWithElse = "if(%s) {%s} else {setTimeout(function() {%s}, 10);}";
+
+        $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)
+    {
+        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':
+                    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++) {
+            if(strpos($aKeys[$i], 'bx_repeat:') === 0) {
+                $sKey = "'<" . $aKeys[$i] . ">(.*)<\/" . $aKeys[$i] . ">'s";
+
+                $aMatches = array();
+                preg_match($sKey, $sContent, $aMatches);
+
+                $sValue = '';
+                if(isset($aMatches[1]) && !empty($aMatches[1])) {
+                    if(empty($aValues[$i]) || !is_array($aValues[$i]))
+                        return false;
+
+                    $sIndex = "\$" . str_repeat("i", $iVarDepth);
+                    $sValue .= "<?php if(is_array(" . $aVarName . "['" . $aKeys[$i] . "'])) for(" . $sIndex . "=0; " . $sIndex . "<count(" . $aVarName . "['" . $aKeys[$i] . "']); " . $sIndex . "++){ ?>";
+                    if(($sInnerValue = $this->_compileContent($aMatches[1], $aVarName . "['" . $aKeys[$i] . "'][" . $sIndex . "]", $iVarDepth + 1, current($aValues[$i]), $mixedKeyWrapperHtml)) === false)
+                        return false;
+                    $sValue .= $sInnerValue;
+                    $sValue .= "<?php } else if(is_string(" . $aVarName . "['" . $aKeys[$i] . "'])) echo " . $aVarName . "['" . $aKeys[$i] . "']; ?>";
+                }
+            } else if(strpos($aKeys[$i], 'bx_if:') === 0) {
+                $sKey = "'<" . $aKeys[$i] . ">(.*)<\/" . $aKeys[$i] . ">'s";
+
+                $aMatches = array();
+                preg_match($sKey, $sContent, $aMatches);
+
+                $sValue = '';
+                if(isset($aMatches[1]) && !empty($aMatches[1])) {
+                    if(!is_array($aValues[$i]) || !isset($aValues[$i]['content']) || empty($aValues[$i]['content']) || !is_array($aValues[$i]['content']))
+                        return false;
+
+                    $sValue .= "<?php if(" . $aVarName . "['" . $aKeys[$i] . "']['condition']){ ?>";
+                    if(($sInnerValue = $this->_compileContent($aMatches[1], $aVarName . "['" . $aKeys[$i] . "']['content']", $iVarDepth, $aValues[$i]['content'], $mixedKeyWrapperHtml)) === false)
+                        return false;
+                    $sValue .= $sInnerValue;
+                    $sValue .= "<?php } ?>";
+                }
+            } else {
+                $sKey = "'" . $aKeyWrappers['left'] . $aKeys[$i] . $aKeyWrappers['right'] . "'s";
+                $sValue = "<?php echo " . $aVarName . "['" . $aKeys[$i] . "'];?>";
+            }
+
+            $aKeys[$i] = $sKey;
+            $aValues[$i] = $sValue;
+        }
+
+        try {
+            $oTemplate = &$this;
+
+            $aCallbackPatterns = array(
+                "'<bx_include_auto:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_BOTH,
+                "'<bx_include_auto_mod_general:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_general'),
+                "'<bx_include_auto_mod_profile:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_profile'),
+                "'<bx_include_auto_mod_group:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_group'),
+                "'<bx_include_auto_mod_text:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BOTH, 'sub' => 'mod_text'),
+                "'<bx_include_base:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_BASE,
+                "'<bx_include_base_mod_general:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_general'),
+                "'<bx_include_base_mod_profile:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_profile'),
+                "'<bx_include_base_mod_group:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_group'),
+                "'<bx_include_base_mod_text:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_BASE, 'sub' => 'mod_text'),
+                "'<bx_include_tmpl:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_TMPL,
+                "'<bx_include_tmpl_mod_general:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_general'),
+                "'<bx_include_tmpl_mod_profile:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_profile'),
+                "'<bx_include_tmpl_mod_group:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_group'),
+                "'<bx_include_tmpl_mod_text:([^\s]+) \/>'s" => array('in' => BX_DOL_TEMPLATE_CHECK_IN_TMPL, 'sub' => 'mod_text')
+            );
+            foreach($aCallbackPatterns as $sPattern => $sCheckIn)
+                $sContent = preg_replace_callback($sPattern, function($aMatches) use($oTemplate, $aVarValues, $mixedKeyWrapperHtml, $sCheckIn) {
+                    $mixedResult = $oTemplate->getCached($aMatches[1], $aVarValues, $mixedKeyWrapperHtml, $sCheckIn, false);
+                    if($mixedResult === false)
+                        throw new Exception("Unable to create cache file ({$aMatches[1]}).", 1);
+
+                    return $mixedResult;
+                }, $sContent);
+
+            $sContent = $this->_parseContentKeys($sContent);
+        }
+        catch(Exception $oException) {
+            if(($iCode = $oException->getCode()) != 1)
+                bx_log('sys_template', "Error in _compileContent method. Cannot parse template insertion (<bx_include... />).\n" . 
+                    "  Error ({$iCode}): {$oException->getMessage()}\n" . 
+                    (getLoggedId() ? "  Account ID: " . getLoggedId() . "\n" : "")
+                );
+
+            return false;
+        }
+
+        $aKeys = array_merge($aKeys, array(
+            "'<bx_menu:([^\s]+) \/>'s",
+            "'<bx_url_root />'",
+            "'<bx_url_studio />'"
+        ));
+        $aValues = array_merge($aValues, array(
+            "<?php echo \$this->getMenu('\\1'); ?>",
+            BX_DOL_URL_ROOT,
+            BX_DOL_URL_STUDIO
+        ));
+
+        //--- Parse Predefined Keys ---//
+        $sContent = preg_replace($aKeys, $aValues, $sContent);
+
+        //--- Parse System Keys ---//
+        $sContent = preg_replace( "'" . $aKeyWrappers['left'] . "([a-zA-Z0-9_-]+)" . $aKeyWrappers['right'] . "'", "<?php echo \$this->parseSystemKey('\\1', \$mixedKeyWrapperHtml);?>", $sContent);
+
+        return $sContent;
+    }
+    protected function _parseContentKeys($sContent, $aCallbackPatterns = array())
+    {
+    	$oTemplate = &$this;
+
+    	$aCallbackPatterns = array_merge($aCallbackPatterns, array(
+            "'<bx_image_auto:([^\s]+) \/>'s" => "get_image_auto",
+            "'<bx_image_url:([^\s]+) \/>'s" => "get_image_url",
+            "'<bx_icon_url:([^\s]+) \/>'s" => "get_icon_url",
+            "'<bx_text:([_\{\}\w\d\s]+[^\s]{1}) \/>'s" => "get_text",
+            "'<bx_text_js:([^\s]+) \/>'s" => "get_text_js",
+            "'<bx_text_attribute:([^\s]+) \/>'s" => "get_text_attribute",
+            "'<bx_page:([^\s]+) \/>'s" => "get_page",
+        ));
+
+        foreach($aCallbackPatterns as $sPattern => $sAction)
+            $sContent = preg_replace_callback($sPattern, function($aMatches) use($oTemplate, $sAction) {
+            $sResult = '';
+
+            switch($sAction) {
+                case 'get_image_auto':
+                    $sResult = $oTemplate->getImageAuto($aMatches[1]);
+                    break;
+                case 'get_image_url':
+                    $sResult = $oTemplate->getImageUrl($aMatches[1]);
+                    break;
+                case 'get_icon_url':
+                    $sResult = $oTemplate->getIconUrl($aMatches[1]);
+                    break;
+                case 'get_text':
+                    $sResult = _t($aMatches[1]);
+                    break;
+                case 'get_text_js':
+                    $sResult = bx_js_string(_t($aMatches[1]));
+                    break;
+                case 'get_text_attribute':
+                    $sResult = bx_html_attribute(_t($aMatches[1]));
+                    break;
+                case 'get_injection':
+                    $sResult = $oTemplate->processInjection($oTemplate->getPageNameIndex(), $aMatches[1]);
+                    break;
+                case 'get_menu':
+                    $sResult = $oTemplate->getMenu($aMatches[1]);
+                    break;
+                case 'get_page':
+                    $oPage = BxDolPage::getObjectInstanceByURI($aMatches[1], false, true);
+                    $oPage->setSubPage(true);
+                    $sResult = $oPage ? $oPage->getCode() : '';
+                    break;
+            }
+
+            return $sResult;
+        }, $sContent);
+
+        return $sContent;
+    }
+
+    /**
+     * Get absolute location of some template's part.
+     *
+     * @param  string $sType    - result type. Available values 'url' and 'path'.
+     * @param  string $sFolder  - folders to be searched in. @see $_sFolderHtml, $_sFolderCss, $_sFolderImages and $_sFolderIcons
+     * @param  string $sName    - requested part name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string absolute location (path/url) of the part.
+     */
+    function _getAbsoluteLocation($sType, $sFolder, $sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        $sDirectory = $this->getPath();
+
+        if($sType == 'path') {
+            $sDivider = DIRECTORY_SEPARATOR;
+            $sRoot = BX_DIRECTORY_PATH_ROOT;
+        } else if($sType == 'url') {
+            $sDivider = '/';
+            $sRoot = BX_DOL_URL_ROOT;
+        }
+
+        if(strpos($sName,'|') !== false) {
+            $aParts = explode('|', $sName);
+            $sName = $aParts[1];
+
+            if(strpos($aParts[0],'@') !== false) {
+                $aLocationParts = explode('@', $aParts[0]);
+                $sLocationKey = $this->addLocation($aLocationParts[0], BX_DIRECTORY_PATH_ROOT . $aLocationParts[1], BX_DOL_URL_ROOT . $aLocationParts[1]);
+            }
+        }
+
+        /** 
+         * Module(mod) related locations will be checked first in TMPL and BASE,
+         * then system(sys) location(s) will be checked in TMPL and BASE.
+         */
+        $aLocationsList = array_reverse($this->_aLocations, true);
+        $aLocationsGrouped = ['mod' => [], 'sys' => []];
+        foreach($aLocationsList as $sLocation => $aLocation) {
+            if($sLocation == 'system')
+                $aLocationsGrouped['sys'][$sLocation] = $aLocation;
+            else
+                $aLocationsGrouped['mod'][$sLocation] = $aLocation;
+        }
+
+        $sResult = '';
+        foreach($aLocationsGrouped as $aLocations) {
+            //--- Check it Template.
+            $bInSub = false;
+            $aCheckIn = [BX_DOL_TEMPLATE_CHECK_IN_BOTH, BX_DOL_TEMPLATE_CHECK_IN_TMPL];
+            if(in_array($sCheckIn, $aCheckIn) || $bInSub = (isset($sCheckIn['in'], $sCheckIn['sub']) && in_array($sCheckIn['in'], $aCheckIn)))
+                foreach($aLocations as $sKey => $aLocation)
+                    if((!$bInSub || $sCheckIn['sub'] == $sKey) && extFileExists(BX_DIRECTORY_PATH_MODULES . $this->getPath(). 'data' . DIRECTORY_SEPARATOR . BX_DOL_TEMPLATE_FOLDER_ROOT . DIRECTORY_SEPARATOR . $sKey . DIRECTORY_SEPARATOR . $sFolder . $sName)) {
+                        $sResult = $sRoot . 'modules' . $sDivider . $sDirectory. 'data' . $sDivider . BX_DOL_TEMPLATE_FOLDER_ROOT . $sDivider . $sKey . $sDivider . $sFolder . $sName;
+                        break 2;
+                    }
+
+            //--- Check it Base.
+            $bInSub = false;
+            $aCheckIn = [BX_DOL_TEMPLATE_CHECK_IN_BOTH, BX_DOL_TEMPLATE_CHECK_IN_BASE];
+            if(empty($sResult) && (in_array($sCheckIn, $aCheckIn) || $bInSub = (isset($sCheckIn['in'], $sCheckIn['sub']) && in_array($sCheckIn['in'], $aCheckIn))))
+                foreach($aLocations as $sKey => $aLocation)
+                    if((!$bInSub || $sCheckIn['sub'] == $sKey) && extFileExists($aLocation['path'] . BX_DOL_TEMPLATE_FOLDER_ROOT . DIRECTORY_SEPARATOR . $sFolder . $sName)) {
+                        $sResult = $aLocation[$sType] . BX_DOL_TEMPLATE_FOLDER_ROOT . $sDivider . $sFolder . $sName;
+                        break 2;
+                    }
+        }
+
+        /**
+         * try to find from received path
+         */
+        if(!$sResult && @is_file(BX_DIRECTORY_PATH_ROOT . $aParts[0] . DIRECTORY_SEPARATOR . $aParts[1])) {
+            $sResult = $sRoot . $aParts[0] . $sDivider . $aParts[1];
+        }
+
+        if(isset($sLocationKey))
+           $this->removeLocation($sLocationKey);
+
+        return $sType == 'path' && !empty($sResult) ? realpath($sResult) : $sResult;
+    }
+    /**
+     * Get absolute location of some template's part.
+     *
+     * @param  string $sType result type. Available values 'url' and 'path'.
+     * @param  string $sName requested part name.
+     * @return string absolute location (path/url) of the part.
+     */
+    function _getAbsoluteLocationJs($sType, $sName)
+    {
+        $sResult = '';
+        $aLocations = array_reverse($this->_aLocationsJs, true);
+        foreach($aLocations as $sKey => $aLocation) {
+            if(extFileExists($aLocation['path'] . $sName))
+                $sResult = $aLocation[$sType] . $sName;
+            else
+                continue;
+            break;
+        }
+        return $sType == 'path' && !empty($sResult) ? realpath($sResult) : $sResult;
+    }
+    function _getAbsoluteLocationCss($sType, $sName)
+    {
+    	$sNameLess = str_replace('.css', '.less', $sName);
+
+    	$sResult = $this->_getAbsoluteLocation($sType, $this->_sFolderCss, $sNameLess);
+    	if(!empty($sResult))
+    		return $sResult;
+
+        return $this->_getAbsoluteLocation($sType, $this->_sFolderCss, $sName);
+    }
+    /**
+     * Get inline data for Images and Icons.
+     *
+     * @param  string  $sType    image/icon
+     * @param  string  $sName    file name
+     * @param  string  $sCheckIn where the content would be searched(base, template, both)
+     * @return unknown
+     */
+    function _getInlineData($sType, $sName, $sCheckIn)
+    {
+        switch($sType) {
+            case 'image':
+                $sFolder = $this->_sFolderImages;
+                break;
+            case 'icon':
+                $sFolder = $this->_sFolderIcons;
+                break;
+        }
+        $sPath = $this->_getAbsoluteLocation('path', $sFolder, $sName, $sCheckIn);
+
+        $iFileSize = 0;
+        if($this->_bImagesInline && ($iFileSize = filesize($sPath)) !== false && $iFileSize < $this->_iImagesMaxSize) {
+            $aFileInfo = pathinfo($sPath);
+            return $this->getImageMimeType($aFileInfo['extension']) . ";base64," . base64_encode(file_get_contents($sPath));
+        }
+
+        return false;
+    }
+
+    /**
+     * Get file name where the template would be cached.
+     *
+     * @param  string $sAbsolutePath template's real path.
+     * @return string the result of operation.
+     */
+    function _getCacheFileName($sType, $sAbsolutePath)
+    {
+        $sResult = bx_site_hash($sAbsolutePath);
+        switch($sType) {
+            case 'html':
+                $sResult = $this->_sCacheFilePrefix . bx_lang_name() . '_' . $this->_sCode .  '_' . $sResult;
+                break;
+            case 'css':
+                $sResult = $this->_sCssCachePrefix . (!empty($this->_iMix) ? $this->_iMix . '_' : '') .  $sResult;
+                break;
+            case 'js':
+                $sResult = $this->_sJsCachePrefix . $sResult;
+                break;
+        }
+
+        return $sResult;
+    }
+    /**
+     * Get template key wrappers(left, right)
+     *
+     * @param  mixed $mixedKeyWrapperHtml key wrapper(string value if left and right parts are the same, array(0 => left, 1 => right) otherwise).
+     * @return array result of operation.
+     */
+    function _getKeyWrappers($mixedKeyWrapperHtml)
+    {
+        $aResult = array();
+        if(!empty($mixedKeyWrapperHtml) && is_string($mixedKeyWrapperHtml))
+            $aResult = array('left' => $mixedKeyWrapperHtml, 'right' => $mixedKeyWrapperHtml);
+        else if(!empty($mixedKeyWrapperHtml) && is_array($mixedKeyWrapperHtml))
+            $aResult = array('left' => $mixedKeyWrapperHtml[0], 'right' => $mixedKeyWrapperHtml[1]);
+        else
+            $aResult = array('left' => $this->_sKeyWrapperHtml, 'right' => $this->_sKeyWrapperHtml);
+        return $aResult;
+    }
+
+    /**
+     * Process all added language translations and return them as a string.
+     *
+     * @return string with JS code.
+     */
+    function _processJsTranslations()
+    {
+        $sReturn = '';
+        if(isset($this->aPage['js_translations']) && is_array($this->aPage['js_translations'])) {
+            foreach($this->aPage['js_translations'] as $sKey => $sString)
+                $sReturn .= "'" .  bx_js_string($sKey) . "': '" . bx_js_string($sString) . "',";
+
+            $sReturn = substr($sReturn, 0, -1);
+        }
+
+        return '
+<script language="javascript">
+    if (\'undefined\' === typeof(aDolLang)) 
+        var aDolLang = {' . $sReturn . '};
+    else
+        $.extend(aDolLang, {' . $sReturn . '});
+</script>';
+    }
+    /**
+     * Process all added options and return them as a string.
+     *
+     * @return string with JS code.
+     */
+    function _processJsOptions()
+    {
+        $sReturn = '';
+        if(isset($this->aPage['js_options']) && is_array($this->aPage['js_options'])) {
+            foreach($this->aPage['js_options'] as $sName => $mixedValue)
+                $sReturn .= "'" .  bx_js_string($sName) . "': '" . bx_js_string($mixedValue) . "',";
+
+            $sReturn = substr($sReturn, 0, -1);
+        }
+
+        return '<script language="javascript">var aDolOptions = {' . $sReturn . '};</script>';
+    }
+    /**
+     * Process all added images and return them as a string.
+     *
+     * @return string with JS code.
+     */
+    function _processJsImages()
+    {
+        $sReturn = '';
+        if(isset($this->aPage['js_images']) && is_array($this->aPage['js_images'])) {
+            foreach($this->aPage['js_images'] as $sKey => $sUrl)
+                $sReturn .= "'" .  bx_js_string($sKey) . "': '" . bx_js_string($sUrl) . "',";
+
+            $sReturn = substr($sReturn, 0, -1);
+        }
+
+        return '<script language="javascript">var aDolImages = {' . $sReturn . '};</script>';
+    }
+
+    /**
+     * Get Gzip loader URL.
+     *
+     * @param $sType content type CSS/JS
+     * @param $sName file name.
+     * @return string with URL
+     */
+    function _getLoaderUrl($sType, $sName)
+    {
+        return BX_DOL_URL_ROOT . 'gzip_loader.php?file=' . $sName . '.' . $sType;
+    }
+
+    /**
+     * Get current revision number.
+     * 
+     * @return integer number
+     */
+    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');
+        $this->displayMsg($sMsg ? $sMsg : _t('_Access denied'), false, $iPage, $iDesignBox);
+    }
+
+    function displayNoData ($sMsg = '', $iPage = BX_PAGE_DEFAULT, $iDesignBox = BX_DB_PADDING_DEF)
+    {
+        bx_import('BxDolLanguages');
+        header('HTTP/1.0 204 No Content');
+        header('Status: 204 No Content');
+        $this->displayMsg($sMsg ? $sMsg : _t('_Empty'), false, $iPage, $iDesignBox);
+    }
+
+    function displayErrorOccured ($sMsg = '', $iPage = BX_PAGE_DEFAULT, $iDesignBox = BX_DB_PADDING_DEF)
+    {
+        bx_import('BxDolLanguages');
+        header('HTTP/1.0 500 Internal Server Error');
+        header('Status: 500 Internal Server Error');
+        $this->displayMsg($sMsg ? $sMsg : _t('_error occured'), false, $iPage, $iDesignBox);
+    }
+
+    function displayPageNotFound ($sMsg = '', $iPage = BX_PAGE_DEFAULT, $iDesignBox = BX_DB_PADDING_DEF)
+    {
+        bx_import('BxDolLanguages');
+        header('HTTP/1.0 404 Not Found');
+        header('Status: 404 Not Found');
+        $this->displayMsg($sMsg ? $sMsg : _t('_sys_request_page_not_found_cpt'), false, $iPage, $iDesignBox);
+    }
+
+    function displayMsg ($s, $bTranslate = false, $iPage = BX_PAGE_DEFAULT, $iDesignBox = BX_DB_PADDING_DEF)
+    {
+        $sError = '_Error';
+        $bArray = is_array($s);
+
+        $sTitle = $bArray ? $s['title'] : ($bTranslate ? $sError : _t($sError));
+        $sContent = $bArray ? $s['content'] : $s;
+
+        if($bTranslate) {
+            $sTitle = _t($sTitle);
+            $sContent = _t($sContent);
+        }
+
+        $sContent = MsgBox($sContent);
+        if($iPage == BX_PAGE_DEFAULT)
+            $sContent = DesignBoxContent($sTitle, $sContent, $iDesignBox);
+
+        $oTemplate = BxDolTemplate::getInstance();
+        $oTemplate->setPageNameIndex ($iPage);
+        $oTemplate->setPageHeader ($sTitle);
+        $oTemplate->setPageContent ('page_main_code', $sContent);
+        $oTemplate->getPageCode();
+        exit;
+    }
+    
+    /**
+     * * * * Static methods for work with template injections * * *
+     *
+     * Static method is used to add/replace the content of some key in the template.
+     * It's usefull when you don't want to modify existing template but need to add some data to existing template key.
+     *
+     * @param  integer $iPageIndex - page index where injections would processed. Use 0 if you want it to be done on all the pages.
+     * @param  string  $sKey       - template key.
+     * @param  string  $sValue     - the data to be added.
+     * @return string  the result of operation.
+     */
+    function processInjection($iPageIndex, $sKey, $sValue = "")
+    {
+        if($iPageIndex != 0 && isset($this->aPage['injections']['page_0'][$sKey]) && isset($this->aPage['injections']['page_' . $iPageIndex][$sKey]))
+           $aSelection = @array_merge($this->aPage['injections']['page_0'][$sKey], $this->aPage['injections']['page_' . $iPageIndex][$sKey]);
+        else if(isset($this->aPage['injections']['page_0'][$sKey]))
+           $aSelection = $this->aPage['injections']['page_0'][$sKey];
+        else if(isset($this->aPage['injections']['page_' . $iPageIndex][$sKey]))
+            $aSelection = $this->aPage['injections']['page_' . $iPageIndex][$sKey];
+        else
+            $aSelection = array();
+
+        if(is_array($aSelection))
+            foreach($aSelection as $aInjection) {
+
+                if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->beginInjection($sRand = time().rand());
+
+                $sInjData = '';
+                switch($aInjection['type']) {
+                    case 'text':
+                        $sInjData = $aInjection['data'];
+                        break;
+
+                    case 'service':
+                    	if(BxDolService::isSerializedService($aInjection['data']))
+                    		$sInjData = BxDolService::callSerialized($aInjection['data']);                    	
+                        break;
+                }
+
+                if((int)$aInjection['replace'] == 1)
+                    $sValue = $sInjData;
+                else
+                    $sValue .= $sInjData;
+
+                if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->endInjection($sRand, $aInjection);
+
+            }
+
+        return $sValue != '__' . $sKey . '__' ? str_replace('__' . $sKey . '__', '', $sValue) : $sValue;
+    }
+    /**
+     * Static method to add ingection available on the current page only.
+     *
+     * @param string  $sKey     - template's key.
+     * @param string  $sType    - injection type(text, php).
+     * @param string  $sData    - the data to be added.
+     * @param integer $iReplace - replace already existed data or not.
+     */
+    function addInjection($sKey, $sType, $sData, $iReplace = 0)
+    {
+        $this->aPage['injections']['page_0'][$sKey][] = array(
+            'page_index' => 0,
+            'key' => $sKey,
+            'type' => $sType,
+            'data' => $sData,
+            'replace' => $iReplace
+        );
+    }
+
+    function getPageCode($oTemplate = null)
+    {
+        if (empty($oTemplate))
+           $oTemplate = $this;
+
+        bx_alert('system', 'design_before_output', 0, 0, ['page' => &$this->aPage, 'page_content' => &$this->aPageContent]);
+
+        header( 'Content-type: text/html; charset=utf-8' );
+        header( 'X-Frame-Options: sameorigin' );
+        if (BX_PAGE_EMBED == $oTemplate->getPageNameIndex())
+            header('Content-Security-Policy: frame-ancestors ' . getParam('sys_csp_frame_ancestors'));
+
+        $sResult = $oTemplate->parsePageByName('page_' . $oTemplate->getPageNameIndex() . '.html', $oTemplate->getPageContent());
+        
+        bx_alert('system', 'design_after_output', 0, false, ['override_result' => &$sResult]);
+        
+        echo $sResult;
+    }
+}
+
+/** @} */

+ 1130 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/js/classes/BxDolCmts.js

@@ -0,0 +1,1130 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function BxDolCmts (options) {
+    this._sObjName = undefined == options.sObjName ? 'oCmts' : options.sObjName;    // javascript object name, to run current object instance from onTimer
+    this._sActionsUrl = options.sRootUrl + 'cmts.php'; // actions url address
+
+    this._sSystem = options.sSystem; // current comment system
+    this._iAuthorId = options.iAuthorId; // this comment's author ID.
+    this._iObjId = options.iObjId; // this object id comments
+    this._sBaseUrl = options.sBaseUrl; // base url to view comment's listing.
+    this._sSocket = options.sSocket === undefined ? this._sSystem : options.sSocket;
+
+    this._iMinPostForm = undefined == options.iMinPostForm ? 0 : options.iMinPostForm;
+    this._sPostFormPosition = undefined == options.sPostFormPosition ? 'top' : options.sPostFormPosition;
+    this._sDisplayType = undefined == options.sDisplayType ? 'threaded' : options.sDisplayType;
+    this._iDisplayStructure = undefined == options.iDisplayStructure ? 0 : options.iDisplayStructure;
+
+    this._sBrowseType = undefined == options.sBrowseType ? 'tail' : options.sBrowseType;
+    this._sBrowseFilter = undefined == options.sBrowseFilter ? 'all' : options.sBrowseFilter;
+
+    this._sSP = options.sStylePrefix === undefined ? 'cmt' : options.sStylePrefix;
+    this._aHtmlIds = options.aHtmlIds;
+    this._sAnimationEffect = 'fade';
+    this._iAnimationSpeed = 'slow';
+
+    this._oSavedTexts = {};
+    this._sRootId = '#cmts-box-' + this._sSystem + '-' + this._iObjId;
+
+    this._bLiveUpdatePaused = false;
+}
+
+/*--- Main layout functionality ---*/
+BxDolCmts.prototype.cmtInit = function()
+{
+    var $this = this;
+
+    $(document).ready(function() {
+        // init socket
+        if(window.oBxDolSockets !== undefined && $this._sSocket)
+            oBxDolSockets.subscribe($this._sSocket, $this._iObjId, 'comment_added', function(oData) {
+                $this.cmtUpdateCounterAs(oData);
+                $this.showLiveUpdateForSocket(oData);
+            });
+
+        // init post comment form    
+        var sFormId = $this._sRootId + ' .cmt-post-reply form';
+        if ($(sFormId).length) {
+            $(sFormId).each(function() {
+                $this.cmtInitFormPost($(this));
+            });
+        }
+
+        // blink (highlight) necessary comments
+        $this._cmtsBlink($($this._sRootId));
+
+        // check content to show 'See More'
+        $this._cmtsInitSeeMore($($this._sRootId));
+    });
+};
+
+BxDolCmts.prototype.cmtInitFormPost = function(oCmtForm)
+{
+    var $this = this;
+
+    oCmtForm.ajaxForm({
+        dataType: "json",
+        beforeSubmit: function (formData, jqForm, options) {
+            window[$this._sObjName].cmtBeforePostSubmit(oCmtForm);
+        },
+        success: function (oData) {
+            window[$this._sObjName].cmtAfterPostSubmit(oCmtForm, oData);
+        }
+    });
+};
+
+BxDolCmts.prototype.cmtShowForm = function(oElement)
+{
+    var oForm = $(oElement).parents('.cmt-reply.cmt-reply-min:first');
+    oForm.find('.cmt-body-min').bx_anim('hide', this._sAnimationEffect, this._iAnimationSpeed, function() {
+        oForm.find('.cmt-body').show(function() {
+            var oTextarea = oForm.find("[name='cmt_text']");
+            if(oTextarea && oTextarea.hasClass('bx-form-input-html') && typeof bx_editor_get_htmleditable === 'function')
+                oTextarea = bx_editor_get_htmleditable(oTextarea);
+
+            if(!oTextarea || oTextarea.length == 0)
+                return;
+            
+            if (oTextarea.attr('object_editor'))
+                eval('bx_editor_activate(' + oTextarea.attr('object_editor') + ')');
+            else
+                oTextarea.focus();
+        });
+    });
+};
+
+BxDolCmts.prototype.cmtBeforePostSubmit = function(oCmtForm)
+{
+    this._loadingInButton($(oCmtForm).children().find(':submit'), true);
+};
+
+BxDolCmts.prototype.cmtAfterPostSubmit = function (oCmtForm, oData)
+{
+    var $this = this;
+    var fContinue = function() {
+        var oParent = oCmtForm.parents('.cmt-reply:first');
+
+        if(oData && oData.id != undefined) {
+            var iCmtId = parseInt(oData.id);
+            if(iCmtId > 0) {
+                $this._getCmt(oCmtForm, iCmtId);
+
+                //--- Update form
+                var iCmtParentId = parseInt(oData.parent_id);
+                if(iCmtParentId == 0)
+                    $this._getForm(undefined, {CmtParent: iCmtParentId}, function(sFormWrp) {
+                        if(sFormWrp && sFormWrp.length > 0)
+                            sFormWrp = $(sFormWrp).html();
+
+                        oParent.hide().html(sFormWrp).bxProcessHtml().show();
+
+                        $this.cmtInitFormPost(oParent.find('form'));
+                    });
+                else
+                    oParent.remove();
+
+                //--- Update counter
+                $this.cmtUpdateCounter(oCmtForm, oData);
+            }
+
+            return;
+        }
+
+        if(oData && oData.form != undefined && oData.form_id != undefined) {
+            oParent.find('form').replaceWith(oData.form);
+
+            $this.cmtInitFormPost(oParent.find('form'));
+
+            return;
+        }
+    };
+
+    this._loadingInButton($(oCmtForm).children().find(':submit'), false);
+
+    if(oData && oData.msg != undefined)
+        bx_alert(oData.msg, fContinue);
+    else 
+        fContinue();
+};
+
+BxDolCmts.prototype.cmtInitFormEdit = function(sCmtFormId)
+{
+	var $this = this;
+	var oCmtForm = $('#' + sCmtFormId);
+
+	oCmtForm.ajaxForm({
+        dataType: "json",
+        beforeSubmit: function (formData, jqForm, options) {
+            window[$this._sObjName].cmtBeforeEditSubmit(oCmtForm);
+        },
+        success: function (oData) {
+            window[$this._sObjName].cmtAfterEditSubmit(oCmtForm, oData);
+        }
+    });
+};
+
+BxDolCmts.prototype.cmtBeforeEditSubmit = function(oCmtForm)
+{
+    this._loadingInButton($(oCmtForm).children().find(':submit'), true);
+};
+
+BxDolCmts.prototype.cmtAfterEditSubmit = function (oCmtForm, oData, onComplete)
+{
+    var $this = this;
+    var fContinue = function() {
+        if(oData && oData.id != undefined && oData.content != undefined) {
+            var iCmtId = parseInt(oData.id);
+            if(iCmtId > 0) {
+                $('#cmt' + iCmtId + ' .cmt-cont-cnt:first').bx_anim('hide', $this._sAnimationEffect, $this._iAnimationSpeed, function() {
+                    $(this).html(oData.content).bxProcessHtml().bx_anim('show', $this._sAnimationEffect, $this._iAnimationSpeed);
+                });
+            }
+        }
+
+        if(oData && oData.form != undefined && oData.form_id != undefined) {
+            $('#' + oData.form_id).replaceWith(oData.form);
+            $this.cmtInitFormEdit(oData.form_id);
+        }
+
+        if(typeof onComplete == 'function')
+            onComplete(oCmtForm, oData);
+    };
+
+    this._loadingInButton($(oCmtForm).children().find(':submit'), false);
+
+    if(oData && oData.msg != undefined)
+        bx_alert(oData.msg, fContinue);
+    else 
+        fContinue();
+};
+
+BxDolCmts.prototype.cmtUpdateCounter = function (oElement, oData)
+{
+    if(oData && (oData.count == undefined || parseInt(oData.count) == 0))
+        return;
+
+    if(oData.countf != undefined && oData.countf.length != 0) {
+        var oCounter = this._getCounter(oElement);
+        if(oCounter && oCounter.length > 0)
+            oCounter.replaceWith(oData.countf);
+    }
+
+    var oCounter = this._getCounter(oElement, true);
+    if(oCounter && oCounter.length > 0)
+        oCounter.html(oData.count);
+
+    var sClassHidden = 'sys-ac-hidden';
+    if(!oCounter.is('.' + sClassHidden))
+        oCounter = oCounter.parents('.' + sClassHidden + ':first');
+
+    oCounter.toggleClass(sClassHidden);
+};
+
+BxDolCmts.prototype.cmtUpdateCounterAs = function (oData)
+{
+    this.cmtUpdateCounter(null, JSON.parse(oData));
+};
+
+BxDolCmts.prototype.cmtPin = function(oLink, iCmtId, iWay, bHideMenu) {
+    var $this = this;
+
+    if(bHideMenu == undefined || bHideMenu)
+        $(oLink).parents('.bx-popup-applied:first:visible').dolPopupHide();
+
+    var oParams = this._getDefaultActions();
+    oParams['action'] = 'Pin';
+    oParams['Cmt'] = iCmtId;
+    oParams['way'] = iWay;
+
+    var oCmt = $(this._sRootId + ' #cmt' + iCmtId);
+    this._loadingInBlock(oCmt, true);
+
+    jQuery.post (
+        this._sActionsUrl,
+        oParams,
+        function (oData) {
+            var fContinue = function() {
+                if(oData && oData.parent_id != undefined) {
+                    $this._getCmts(oLink, {
+                        CmtParent: oData.parent_id,
+                        CmtBrowse: $this._sBrowseType,
+                        CmtFilter: $this._sBrowseFilter,
+                        CmtDisplay: $this._sDisplayType,
+                        CmtPinned: 1
+                    }, function(sListId, sContent) {
+                        $this._loadingInBlock(oCmt, false);
+
+                        $this._sDisplayType = $this._sDisplayType;
+                        $this._cmtsReplaceContent($(sListId), sContent);
+
+                        var oDivider = $(sListId).siblings('.cmts-divider');
+                        if(oDivider.length > 0) {
+                            if(oDivider.is(':hidden') && sContent.length != 0)
+                                oDivider.show();
+                            else if(!oDivider.is(':hidden') && sContent.length == 0)
+                                oDivider.hide();
+                        }
+                    });
+            	}
+            };
+
+            if(oData && oData.msg != undefined)
+                bx_alert(oData.msg, fContinue);
+            else
+                fContinue();
+        },
+        'json'
+    );
+};
+
+BxDolCmts.prototype.cmtEdit = function(oLink, iCmtId, bHideMenu) {
+    var $this = this;
+
+    if(bHideMenu == undefined || bHideMenu)
+        $(oLink).parents('.bx-popup-applied:first:visible').dolPopupHide();
+
+    var sContentId = this._sRootId + ' #cmt' + iCmtId + ' .cmt-cont-cnt:first';
+    if ($(sContentId + ' > form').length) {
+        $(sContentId).bx_anim('hide', $this._sAnimationEffect, $this._iAnimationSpeed, function() {
+            $(this).html($this._oSavedTexts[iCmtId]).bxProcessHtml().bx_anim('show', $this._sAnimationEffect, $this._iAnimationSpeed);
+        });
+
+        return;
+    }
+
+    var oParams = this._getDefaultActions();
+    oParams['action'] = 'GetFormEdit';
+    oParams['Cmt'] = iCmtId;
+
+    this._oSavedTexts[iCmtId] = $(sContentId).html();
+
+    this._loadingInContent(oLink, true);
+
+    jQuery.post (
+        this._sActionsUrl,
+        oParams,
+        function (oData) {
+            var fContinue = function() {
+                if(oData && oData.form != undefined && oData.form_id != undefined) {
+                    $(sContentId).bx_anim('hide', $this._sAnimationEffect, $this._iAnimationSpeed, function() {
+                        $(this).html(oData.form).bxProcessHtml().bx_anim('show', $this._sAnimationEffect, $this._iAnimationSpeed, function() {
+                            $this.cmtInitFormEdit(oData.form_id);
+                        });
+                    });
+            	}
+            };
+
+            $this._loadingInContent(oLink, false);
+
+            if(oData && oData.msg != undefined)
+                bx_alert(oData.msg, fContinue);
+            else
+                fContinue();
+        },
+        'json'
+    );
+};
+
+BxDolCmts.prototype.cmtRemove = function(e, iCmtId) {
+	var $this = this;
+
+	$(e).parents('.bx-popup-active:first').dolPopupHide();
+
+	bx_confirm('', function() {
+	    var oParams = $this._getDefaultActions();
+	    oParams['action'] = 'Remove';
+	    oParams['Cmt'] = iCmtId;
+
+	    $this._loadingInContent(e, true);
+
+	    jQuery.post (
+	        $this._sActionsUrl,
+	        oParams,
+	        function(oData) {
+	            var fContinue = function() {
+	            	if(oData && oData.id != undefined) {
+                            $(e).parents('.bx-popup-applied:first:visible').dolPopupHide();
+
+                            $('#cmt' + oData.id).bx_anim('hide', $this._sAnimationEffect, $this._iAnimationSpeed, function() {
+                                    //--- Update counter
+                                    if(oData && oData.count != undefined && parseInt(oData.count) >= 0) {
+                                        var oSource = $(this);
+
+                                        var oCounter = $this._getCounter(oSource);
+                                        if(oCounter && oCounter.length > 0) {
+                                            if(oData.countf != undefined && oData.countf.length != 0)
+                                                oCounter.html(oData.countf);
+
+                                            if(parseInt(oData.count) == 0)
+                                                oCounter.toggleClass('sys-ac-hidden');
+                                        }
+
+                                        var oCounter = $this._getCounter(oSource, true);
+                                        if(oCounter && oCounter.length > 0)
+                                            oCounter.html(oData.count);
+                                    }
+
+                                    $(this).remove();
+                            });
+                        }
+	            };
+
+	            $this._loadingInContent(e, false);
+
+	            if(oData && oData.msg != undefined)
+	                bx_alert(oData.msg, fContinue);	            
+	            else
+	            	fContinue();
+	        },
+	        'json'
+	    );
+	});
+};
+
+BxDolCmts.prototype.cmtLoad = function(oLink, iCmtParentId, iStart, iPerView)
+{
+	var $this = this;
+	var bButton = $(oLink).hasClass('bx-btn');
+
+	if(bButton)
+		this._loadingInButton(oLink, true);
+	else 
+		this._loading($(oLink).parents('ul.cmts:first'), true);
+
+	this._getCmts(oLink, {
+		CmtParent: iCmtParentId,
+		CmtStart: iStart,
+		CmtPerView: iPerView,
+		CmtBrowse: this._sBrowseType,
+		CmtFilter: this._sBrowseFilter,
+		CmtDisplay: this._sDisplayType,
+                CmtDisplayStructure: this._iDisplayStructure
+	}, function(sListId, sContent) {
+		if(bButton)
+			$this._loadingInButton(oLink, false);
+		else 
+			$this._loading($(oLink).parents('ul.cmts:first'), false);
+
+		$this._cmtsReplace($(oLink).parents('li:first'), sContent);
+	});
+};
+
+BxDolCmts.prototype.cmtChangeDisplay = function(oLink, sType)
+{
+	var $this = this;
+	this._getCmts(oLink, {
+		CmtParent: 0,
+		CmtBrowse: this._sBrowseType,
+		CmtFilter: this._sBrowseFilter,
+		CmtDisplay: sType
+	}, function(sListId, sContent) {
+		$this._sDisplayType = sType;
+		$this._cmtsReplaceContent($(sListId), sContent);
+	});
+};
+
+BxDolCmts.prototype.cmtChangeBrowse = function(oLink, sType)
+{
+	var $this = this;
+	this._getCmts(oLink, {
+		CmtParent: 0,
+		CmtBrowse: sType,
+		CmtFilter: this._sBrowseFilter,
+		CmtDisplay: this._sDisplayType
+	}, function(sListId, sContent) {
+		$this._sBrowseType = sType;
+		$this._cmtsReplaceContent($(sListId), sContent);
+	});
+};
+
+BxDolCmts.prototype.cmtChangeFilter = function(oLink, sType)
+{
+	var $this = this;
+	this._getCmts(oLink, {
+		CmtParent: 0,
+		CmtBrowse: this._sBrowseType,
+		CmtFilter: sType,
+		CmtDisplay: this._sDisplayType
+	}, function(sListId, sContent) {
+		$this._sBrowseFilter = sType;
+		$this._cmtsReplaceContent($(sListId), sContent);
+	});
+};
+
+BxDolCmts.prototype.showReplacement = function(iCmtId)
+{
+    $('#cmt' + iCmtId + '-hidden').bx_anim('hide', this._sAnimationEffect, this._iAnimationSpeed, function(){
+        $(this).next('#cmt' + iCmtId).bx_anim('show', this._sAnimationEffect, this._iAnimationSpeed);
+    });
+};
+
+BxDolCmts.prototype.showImage = function(eve, oLink)
+{
+    var $this = this;
+    var ePswp = document.querySelectorAll('.pswp.cmts')[0];
+
+    var aItems = []; // more items are added dynamically
+    var oItem = {};
+    var iCount = 0;
+    var options = {
+        shareEl: false,
+        counterEl: false,
+        history: false,
+        loop: false,
+        showHideOpacity: true,
+        index: 0,
+        getThumbBoundsFn: function(index) {
+            var e = $('a[data-file-id] img');
+            if (!e.length)
+                return false;
+
+            return {x:e.offset().left, y:e.offset().top, w:e.width()};
+        }
+    };
+
+    var fnProcessRetina = function (o) {
+        var dpr = ((window.glBxDisableRetina !== undefined && window.glBxDisableRetina) || window.devicePixelRatio === undefined ? 1 : window.devicePixelRatio);
+        if (dpr < 2)
+            return o;
+        o.w *= 2;
+        o.h *= 2;
+        return o;
+    };
+
+    eve.preventDefault ? eve.preventDefault() : eve.returnValue = false;
+
+    // get data for initial item from the attributes of the item which was clicked
+    $.each(oLink.attributes, function(i, attr) {
+        var sName = attr.name;
+        if (sName.indexOf('data-') !== 0)
+            return;
+
+        sName = sName.replace('data-', '');
+        oItem[sName] = attr.value;        
+
+        ++iCount;
+    });
+
+    oItem.msrc = oItem.src;
+    oItem = fnProcessRetina(oItem);
+
+    if(!iCount)
+        return false;
+
+    aItems.push(oItem);
+
+    var fnConverMedia = function (oMedia) {
+        var oMap = {
+            'id': 'file-id',
+            'file' : 'src',
+            'w': 'w',
+            'h': 'h'
+        };
+
+        var o = {};
+        for (var i in oMap)
+            o[oMap[i]] = oMedia[i];
+
+        return fnProcessRetina(o);
+    };
+
+    var fnIndexOfMediaObject = function (oArray, oItem) {
+        var iLength = oArray.length;
+        for (var i=0 ; i < iLength ; ++i)
+            if (oItem['file-id'] == oArray[i]['file-id'])
+                return i;
+
+        return -1;
+    };
+
+    var fnDisableArrows = function (oItem) {
+
+        // disable prev item and action if we are on the first item
+        if (0 == fnIndexOfMediaObject(aItems, oItem)) {
+            $('.pswp.cmts .pswp__button--arrow--left').hide();
+            glSysCmtsPrevFn = glSysCmtsGallery.prev;
+            glSysCmtsGallery.prev = function () { };
+        } 
+        else {
+            $('.pswp.cmts .pswp__button--arrow--left').show();
+            if ('undefined' !== typeof(glSysCmtsPrevFn))
+                glSysCmtsGallery.prev = glSysCmtsPrevFn;
+        }
+
+        // disable next item and action when we are in the last item
+        if ((aItems.length-1) == fnIndexOfMediaObject(aItems, oItem)) {
+            $('.pswp.cmts .pswp__button--arrow--right').hide();
+            glSysCmtsNextFn = glSysCmtsGallery.next;
+            glSysCmtsGallery.next = function () { };
+        } 
+        else {
+            $('.pswp.cmts .pswp__button--arrow--right').show();
+            if ('undefined' !== typeof(glSysCmtsNextFn))
+                glSysCmtsGallery.next = glSysCmtsNextFn;
+        }
+    };
+
+    var fnLoadMoreItem = function (oItemCurrent, bFirstLoad) {
+
+        // load addutional items only for items on the border
+        if (0 != fnIndexOfMediaObject(aItems, oItemCurrent) && (aItems.length-1) != fnIndexOfMediaObject(aItems, oItemCurrent))
+            return;
+
+        var sUrl = bx_append_url_params($this._sActionsUrl, {sys: $this._sSystem, id: $this._iObjId, action: 'GetSiblingFiles', file_id: oItemCurrent['file-id']});
+        $.getJSON(sUrl, function(oData) {
+
+            if ('undefined' !== typeof(oData.error)) {
+                if ('undefined' !== typeof(console))
+                    console.log(oData.error);
+
+                return;
+            }
+
+            var iGoTo = -1;
+            var iLength = aItems.length;
+
+            if (0 == fnIndexOfMediaObject(aItems, oItemCurrent) && 'undefined' !== typeof(oData.prev.file)) {
+                aItems.unshift(fnConverMedia(oData.prev));
+                if (bFirstLoad)
+                    options.index = 1;
+                else
+                    iGoTo = glSysCmtsGallery.getCurrentIndex() + 1;
+            }
+    
+            if ((aItems.length - 1) == fnIndexOfMediaObject(aItems, oItemCurrent) && 'undefined' !== typeof(oData.next.file))
+                aItems.push(fnConverMedia(oData.next));
+
+            if (!bFirstLoad && iLength != aItems.length) {
+                glSysCmtsGallery.invalidateCurrItems();
+                glSysCmtsGallery.updateSize(true);
+
+                if(iGoTo >= 0)
+                    glSysCmtsGallery.goTo(iGoTo);
+            }
+
+            if (bFirstLoad) {
+                glSysCmtsGallery = new PhotoSwipe(ePswp, PhotoSwipeUI_Default, aItems, options);
+                glSysCmtsGallery.init();
+
+                glSysCmtsGallery.listen('beforeChange', function() { 
+                    // load more items if we are on first or on the last item
+                    fnLoadMoreItem(glSysCmtsGallery.currItem, false);
+                });
+
+                glSysCmtsGallery.listen('afterChange', function() {
+                    fnDisableArrows(glSysCmtsGallery.currItem);
+                });
+
+                glSysCmtsGallery.listen('close', function() {
+                });
+
+                fnDisableArrows(glSysCmtsGallery.currItem);
+            }
+        });
+    };
+
+    fnLoadMoreItem(oItem, true);
+
+    return false;
+};
+
+BxDolCmts.prototype.toggleReply = function(oElement, iCmtParentId, iQuote)
+{
+    var $this = this;
+    var aParams = {
+        CmtParent: iCmtParentId,
+        CmtQuote: iQuote != undefined ? parseInt(iQuote) : 0
+    };
+    var fOnShow = function() {
+        $(this).find('textarea:first').focus();
+    };
+
+    var oParentId = null;
+    if(oElement)
+        oParentId = $(oElement).parents('#cmt' + iCmtParentId);
+    else
+        oParentId = $(this._sRootId + ' #cmt' + iCmtParentId);
+
+    var sReplyQuoteId = '.cmt-reply-quote';
+    var sReplyQuoteIdOpst = ':not(' + sReplyQuoteId + ')';
+
+    var sReplyId = '';
+    var sReplyIdOpst = '';
+    if(aParams['CmtQuote']) {
+        sReplyId = '.cmt-reply' + sReplyQuoteId;
+        sReplyIdOpst = '.cmt-reply' + sReplyQuoteIdOpst;
+    }
+    else {
+        sReplyId = '.cmt-reply' + sReplyQuoteIdOpst;
+        sReplyIdOpst = '.cmt-reply' + sReplyQuoteId;
+    }
+
+    //--- Hide opposite form.
+    if (oParentId.find('> ' + sReplyIdOpst + ':visible').length)
+        oParentId.find('> ' + sReplyIdOpst).hide();
+
+    if (oParentId.find('> ' + sReplyId).length) {
+        oParentId.find('> ' + sReplyId).bx_anim('toggle', this._sAnimationEffect, this._iAnimationSpeed, fOnShow);
+        return;
+    }
+
+    this._getForm(oElement, aParams, function(sForm) {
+        var oForm = $(sForm).hide().addClass('cmt-reply-' + $this._sPostFormPosition).addClass('cmt-reply-margin');
+        var oFormSibling = oParentId.find('> ul.cmts:first');
+        switch($this._sPostFormPosition) {
+            case 'top':
+                oFormSibling.before(oForm);
+                break;
+
+            case 'bottom':
+                oFormSibling.after(oForm);
+                break;
+        }
+
+        $this.cmtInitFormPost(oForm.find('form'));
+
+        oParentId.children(sReplyId).bx_anim('toggle', $this._sAnimationEffect, $this._iAnimationSpeed, fOnShow);
+    });
+};
+
+BxDolCmts.prototype.toggleQuote = function(e, iCmtParentId)
+{
+    this.toggleReply(e, iCmtParentId, 1);
+};
+
+BxDolCmts.prototype.goTo = function(oLink, sGoToId, sBlinkIds, onLoad)
+{
+    $(oLink).parents('.bx-popup-applied:first:visible').dolPopupHide();
+
+    var $this = this;
+    this._getCmts(null, {
+        CmtParent: 0,
+        CmtBrowse: this._sBrowseType,
+        CmtFilter: this._sBrowseFilter,
+        CmtDisplay: this._sDisplayType,
+        CmtBlink: sBlinkIds
+    }, function(sListId, sContent) {
+        $this._cmtsReplaceContent($(sListId), sContent, function() {
+            location.hash = sGoToId;
+
+            if(typeof onLoad == 'function')
+                onLoad();
+        });
+    });
+};
+
+BxDolCmts.prototype.goToBtn = function(oLink, sGoToId, sBlinkIds, onLoad)
+{
+    var $this = this;
+
+    this._loadingInButton(oLink, true);
+
+    this._getCmts(null, {
+        CmtParent: 0,
+        CmtBrowse: this._sBrowseType,
+        CmtFilter: this._sBrowseFilter,
+        CmtDisplay: this._sDisplayType,
+        CmtBlink: sBlinkIds
+    }, function(sListId, sContent) {
+        $this._cmtsReplaceContent($(sListId), sContent, function() {
+            location.hash = sGoToId;
+
+            if(typeof onLoad == 'function')
+                onLoad();
+
+            $(oLink).parents('.cmt-lu-button:first').remove();
+
+            $this.resumeLiveUpdates();
+        });
+    });
+};
+
+BxDolCmts.prototype.goToAndReply = function(oLink, sGoToId, sBlinkIds)
+{
+    var $this = this;
+    this.goTo(oLink, sGoToId, sBlinkIds, function() {
+        var aBlinkIds = sBlinkIds.split(",");
+        $.each(aBlinkIds, function(iIndex, iValue) {
+            $this.toggleReply(null, iValue);
+        });
+    });
+};
+
+/*----------------------------*/
+/*--- Live Updates methods ---*/
+/*----------------------------*/
+BxDolCmts.prototype.showLiveUpdate = function(oData)
+{
+    if(!oData.code)
+        return;
+
+    var oButton = $(oData.code);
+    var sId = oButton.attr('id');
+    $('#' + sId).remove();
+
+    oButton.prependTo(this._sRootId);
+};
+
+BxDolCmts.prototype.showLiveUpdateForSocket = function(sData)
+{
+    var oData = JSON.parse(sData);
+    if(this._iAuthorId == oData.author_id)
+        return;
+
+    var sSelector = this._sRootId + ' .' + this._sSP + '-lu-button';
+    if($(sSelector + ':visible').length != 0)
+        return;
+
+    var oElement = $(sSelector + ':hidden').clone();
+    if(!oElement || oElement.length == 0)
+        return;
+
+    var sOnClick = oElement.find('.bx-btn').attr('onclick');
+    oElement.find('.bx-btn').attr('onclick', sOnClick.replace('{cmt_id}', oData.id).replace('{cmt_anchor}', oData.anchor));
+    oElement.prependTo(this._sRootId).show();
+};
+
+BxDolCmts.prototype.showLiveUpdates = function(oData)
+{
+	/*
+	 * Note. oData.count_old and oData.count_new are also available and can be checked or used in notification popup.  
+	 */
+	if(!oData.code)
+            return;
+
+	var $this = this;
+
+	var oNotifs = $(oData.code);
+	var sId = oNotifs.attr('id');
+	$('#' + sId).remove();
+
+	oNotifs.prependTo('body').dolPopup({
+    	position: 'fixed',
+    	left: '1rem',
+    	top: 'auto',
+    	bottom: '1rem',
+    	fog: false,
+    	onBeforeShow: function() {
+    	},
+    	onBeforeHide: function() {
+    	},
+        onShow: function() {
+            setTimeout(function() {
+                $('.bx-popup-chain.bx-popup-applied:visible:first').dolPopupHide();
+            }, 5000);
+        },
+    	onHide: function() {
+            $this.resumeLiveUpdates();
+    	}
+    });
+};
+
+BxDolCmts.prototype.previousLiveUpdate = function(oLink)
+{
+	var fPrevious = function() {
+		var sClass = 'bx-popup-chain-item';
+		$(oLink).parents('.' + sClass + ':first').hide().prev('.' + sClass).show();
+	};
+
+	if(!this.pauseLiveUpdates(fPrevious));
+		fPrevious();
+};
+
+BxDolCmts.prototype.hideLiveUpdate = function(oLink)
+{
+	$(oLink).parents('.bx-popup-applied:visible:first').dolPopupHide();
+};
+
+BxDolCmts.prototype.resumeLiveUpdates = function(onLoad)
+{
+	if(!this._bLiveUpdatePaused)
+		return false;
+
+	var $this = this;
+	this.changeLiveUpdates('ResumeLiveUpdate', function() {
+		$this._bLiveUpdatePaused = false;
+
+		if(typeof onLoad == 'function')
+			onLoad();
+	});
+
+	return true;
+};
+
+BxDolCmts.prototype.pauseLiveUpdates = function(onLoad)
+{
+	if(this._bLiveUpdatePaused)
+		return false;
+
+	var $this = this;
+	this.changeLiveUpdates('PauseLiveUpdate', function() {
+		$this._bLiveUpdatePaused = true;
+
+		if(typeof onLoad == 'function')
+			onLoad();
+	});
+
+	return true;
+};
+
+BxDolCmts.prototype.changeLiveUpdates = function(sAction, onLoad)
+{
+	var $this = this;
+    var oParams = this._getDefaultActions();
+    oParams['action'] = sAction;
+
+	jQuery.get(
+	    this._sActionsUrl,
+	    oParams,
+	    function() {
+	    	if(typeof onLoad == 'function')
+				onLoad();
+	    }
+	);
+};
+
+BxDolCmts.prototype.cmtShowMore = function(oLink)
+{
+    var oShowMore = $(oLink).parents('.' + this._sSP + '-body-show-more:first');
+    if(!oShowMore.length)
+        return;
+
+    var sClassOverflow = 'bx-overflow';
+
+    oShowMore.siblings('.' + sClassOverflow).css('max-height', 'none').removeClass(sClassOverflow);
+    oShowMore.remove();
+};
+
+BxDolCmts.prototype.cmtOnFindOverflow = function(oElement) {
+    $(oElement).after($(oElement).parents(this._sRootId + ':first').find('.' + this._sSP + '-body-show-more:hidden:first').clone().show());
+};
+
+/*----------------------------------*/
+/*--- Methods for internal usage ---*/
+/*----------------------------------*/
+BxDolCmts.prototype._getCmt = function (e, iCmtId)
+{
+    var $this = this;
+    var oData = this._getDefaultActions();
+    oData['action'] = 'GetCmt';
+    oData['Cmt'] = iCmtId;
+    oData['CmtBrowse'] = this._sBrowseType;
+    oData['CmtDisplay'] = this._sDisplayType;
+
+    this._loadingInBlock (e);
+
+    jQuery.post (
+        this._sActionsUrl,
+        oData,
+        function (oData) {
+            $this._loadingInBlock (e, false);
+
+            var sListId = $this._sRootId + ' #cmt' + oData.vparent_id + ' > ul.cmts-all';
+            var sReplyFormId = $this._sRootId + ' #cmt' + oData.parent_id + ' > .cmt-reply';
+
+            //--- Hide reply form ---//
+            if($(sReplyFormId).length)
+            	$(sReplyFormId).bx_anim('hide', $this._sAnimationEffect, $this._iAnimationSpeed);
+
+            $(sListId).each(function() {
+                //--- Some number of comments already loaded ---//
+                if($(this).children('li.cmt').length) {
+                    var oAdded = null;
+                    switch($this._sPostFormPosition) {
+                        case 'top':
+                            oAdded = $(this).children('li.cmt:first').before($(oData.content).hide()).prev('li.cmt:hidden');
+                            break;
+
+                        case 'bottom':
+                            oAdded = $(this).children('li.cmt:last').after($(oData.content).hide()).next('li.cmt:hidden');
+                            break;
+                    }
+
+                    oAdded.bxProcessHtml().bx_anim('toggle', $this._sAnimationEffect, $this._iAnimationSpeed);
+                }
+                //-- There is no comments at all ---//
+                else
+                    $(this).hide().html(oData.content).bxProcessHtml().bx_anim('show', $this._sAnimationEffect, $this._iAnimationSpeed);
+
+                $this._cmtsInitSeeMore($(this));
+            });
+        },
+        'json'
+    );
+};
+
+BxDolCmts.prototype._getCmts = function (oElement, oRequestParams, onLoad)
+{
+    var $this = this;
+    var oData = this._getDefaultActions();    
+    oData['action'] = 'GetCmts';
+    oData = $.extend({}, oData, oRequestParams);
+
+    var sListId =  this._sRootId + ' #cmt' + oData['CmtParent'] + ' > ul.cmts-' + (oRequestParams.CmtPinned ? 'pinned' : 'all') + ':first';
+
+    if(oElement)
+        this._loadingInBlock(oElement, true);
+
+    jQuery.post (
+        this._sActionsUrl,
+        oData,
+        function(s) {
+            if(oElement)
+                $this._loadingInBlock(oElement, false);
+
+            if(typeof onLoad == 'function')
+                onLoad(sListId, s);
+            else
+                $this._cmtsAppend(sListId, s);
+        }
+    );
+};
+
+BxDolCmts.prototype._getForm = function (e, oParams, onLoad)
+{
+    var $this = this;
+    var oData = $.extend({}, this._getDefaultActions(), {
+        action: 'GetFormPost',
+        CmtType: 'reply',
+        CmtBrowse: this._sBrowseType, 
+        CmtDisplay: this._sDisplayType, 
+        CmtMinPostForm: this._iMinPostForm
+    }, oParams);
+
+    if(e)
+    	this._loadingInContent(e, true);
+
+    jQuery.post (
+        this._sActionsUrl,
+        oData,
+        function (s) {
+            if(e)
+                $this._loadingInContent(e, false);
+
+            if(typeof onLoad == 'function')
+                onLoad(s);
+        }
+    );
+};
+
+BxDolCmts.prototype._cmtsAppend = function(sIdTo, sContent)
+{
+	$(sIdTo).append($(sContent).hide()).children(':hidden').bxProcessHtml().bx_anim('show', this._sAnimationEffect, this._iAnimationSpeed);
+};
+
+BxDolCmts.prototype._cmtsPrepend = function(sIdTo, sContent)
+{
+	$(sIdTo).prepend($(sContent).hide()).children(':hidden').bxProcessHtml().bx_anim('show', this._sAnimationEffect, this._iAnimationSpeed);
+};
+
+BxDolCmts.prototype._cmtsReplace = function(oReplace, sContent, onLoad)
+{
+    var $this = this;
+    $(oReplace).bx_anim('hide', this._sAnimationEffect, this._iAnimationSpeed, function() {
+        $(this).after($(sContent).hide()).nextAll(':hidden').bxProcessHtml().bx_anim('show', $this._sAnimationEffect, $this._iAnimationSpeed, function() {
+            $this._cmtsBlink($(this));
+
+            $this._cmtsInitSeeMore($(this));
+
+            if(typeof onLoad == 'function')
+                onLoad();
+        });
+        $(this).remove();
+    });
+};
+BxDolCmts.prototype._cmtsReplaceContent = function(oParent, sContent, onLoad)
+{
+    var $this = this;
+    $(oParent).bx_anim('hide', this._sAnimationEffect, this._iAnimationSpeed, function() {
+        $(this).html(sContent).bxProcessHtml().bx_anim('show', $this._sAnimationEffect, $this._iAnimationSpeed, function() {
+            $this._cmtsBlink($(this));
+
+            $this._cmtsInitSeeMore($(this));
+
+            if(typeof onLoad == 'function')
+            onLoad();
+        });
+    });
+};
+
+BxDolCmts.prototype._cmtsBlink = function(oParent)
+{
+    var sBlinkClass = 'cmt-blink';
+
+    oParent.find('.' + sBlinkClass + '-plate:visible').animate({
+        opacity: 0
+    }, 5000, function() {
+        oParent.find('.' + sBlinkClass).removeClass(sBlinkClass);
+    });
+};
+
+BxDolCmts.prototype._cmtsInitSeeMore = function(oParent)
+{
+    var $this = this;
+
+    oParent.find('.cmt-body.bx-overflow-ready').bxCheckOverflowHeight('', function(oElement) {
+        $this.cmtOnFindOverflow(oElement);
+    });
+};
+        
+        
+        
+BxDolCmts.prototype._getCounter = function(oElement, bText)
+{
+    var oCounter = null;
+    var sSelector = '.' + this._sSP + '-counter' + (bText ? '-text' : '');
+
+    if(oElement) {
+        if($(oElement).hasClass(this._sSP))
+            oCounter = $(oElement).find(sSelector);
+        else 
+            oCounter = $(oElement).parents('.' + this._sSP + ':first').find(sSelector);
+    }
+
+    if((!oCounter || !oCounter.length) && this._aHtmlIds['counter'] != undefined) {
+        oCounter = $('#' + this._aHtmlIds['counter']);
+        if(!oCounter.is(sSelector))
+            oCounter = oCounter.find(sSelector);
+    }
+
+    return oCounter;
+};
+
+BxDolCmts.prototype._getDefaultActions = function() {
+    var oDate = new Date();
+    return {
+        sys: this._sSystem,
+        id: this._iObjId,
+        _t: oDate.getTime()
+    };
+};
+
+BxDolCmts.prototype._loading = function(e, bShow) {
+    var oParent = $(e).length ? $(e) : $('body'); 
+    bx_loading(oParent, bShow);
+};
+
+BxDolCmts.prototype._loadingInBlock = function(e, bShow) {
+    var oParent = $(e).length ? $(e).parents('.bx-db-content:first') : $('body'); 
+    bx_loading(oParent, bShow);
+};
+
+BxDolCmts.prototype._loadingInContent = function(e, bShow) {
+    var oParent = $(e).length ? $(e).parents('li.cmt:first,.cmt-reply:first') : $('body'); 
+    bx_loading(oParent, bShow);
+};
+
+BxDolCmts.prototype._loadingInButton = function(e, bShow) {
+    if($(e).length)
+        bx_loading_btn($(e), bShow);
+    else
+        bx_loading($('body'), bShow);
+};
+
+/** @} */

+ 2488 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/utils.inc.php

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

+ 12 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/inc/version.inc.php

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

+ 138 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/modules/base/general/classes/BxBaseModGeneralCmtsSearchResult.php

@@ -0,0 +1,138 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModGeneralCmtsSearchResult extends BxBaseModGeneralSearchResult
+{
+    protected $sModule;
+    protected $sModuleObjectComments;
+    protected $aCommentsAddons;
+
+    function __construct($sMode = '', $aParams = array())
+    {
+        parent::__construct($sMode, $aParams);
+
+        $this->oModule = BxDolModule::getInstance($this->sModule);
+        $this->sModuleObjectComments = $this->oModule->_oConfig->CNF['OBJECT_COMMENTS'];
+        $this->aCommentsAddons = array();        
+
+        $this->aCurrent = array(
+            'name' => $this->sModule . '_cmts',
+            'module_name' => $this->sModule,
+            'object_metatags' => 'sys_cmts',
+            'title' => '',
+            'table' => $this->oModule->_oConfig->getDbPrefix() . 'cmts',
+            'ownFields' => array('cmt_id', 'cmt_object_id', 'cmt_author_id', 'cmt_text', 'cmt_time'),
+            'searchFields' => array('cmt_text'),
+            'restriction' => array(
+                'author' => array('value' => '', 'field' => 'cmt_author_id', 'operator' => '='),
+            ),
+            'paginate' => array('start' => 0),
+            'sorting' => 'last',
+            'ident' => 'cmt_id',
+            'added' => 'cmt_time',
+        );
+
+        $this->_joinTableUniqueIds();
+
+        $this->sBrowseUrl = BX_DOL_SEARCH_KEYWORD_PAGE;
+    }
+
+    function displaySearchUnit($aData)
+    {
+        $oCmts = BxDolCmts::getObjectInstance($this->sModuleObjectComments, $aData['object_id']);
+        $oCmts->addCssJs();
+
+        if($this->_bLiveSearch) {
+            $CNF = &$this->oModule->_oConfig->CNF;
+
+            return $oCmts->getCommentLiveSearch($aData['id'], array(
+                'txt_sample_single' => isset($CNF['T']['txt_sample_comment_single']) ? $CNF['T']['txt_sample_comment_single'] : ''
+            ));
+        }
+        else {
+            if(!isset($this->aCommentsAddons[$aData['object_id']]))
+                $this->aCommentsAddons[$aData['object_id']] = '';
+
+            return $oCmts->getCommentSearch(isset($aData['cmt_id']) ? $aData['cmt_id'] : $aData['id'], $this->aCommentsAddons[$aData['object_id']]);
+        }
+    }
+
+    function displayResultBlock ()
+    {
+        $sCode = '<ul class="cmts">' . parent::displayResultBlock() . '</ul>';
+
+        if(!empty($this->aCommentsAddons) && is_array($this->aCommentsAddons))
+            foreach($this->aCommentsAddons as $sCommentsAddon)
+                $sCode .= $sCommentsAddon;
+
+        return $sCode;
+    }
+
+    function getAlterOrder()
+    {
+        if($this->aCurrent['sorting'] == 'last')
+            return array(
+            	'order' => " ORDER BY `" . $this->aCurrent['table'] . "`.`cmt_time` DESC"
+            );
+
+        return array();
+    }
+
+    function decodeDataAPI ($a)
+    {
+        $oContentInfo = $this->getContentInfoObject();
+
+        foreach($a as $i => $r)
+            $a[$i] = $oContentInfo->getContentInfoAPI($r['id']);
+
+        return $a;
+    }
+
+    function _getPseud ()
+    {
+        return array(
+            'id' => 'cmt_id',
+        	'object_id' => 'cmt_object_id',
+            'author' => 'cmt_author_id',
+            'text' => 'cmt_text',
+            'added' => 'cmt_time'
+        );
+    }
+
+    protected function _joinTableUniqueIds()
+    {
+        $aCommentsSystem = BxDolCmtsQuery::getSystemBy(['type' => 'name', 'name' => $this->sModuleObjectComments]);
+        if(empty($aCommentsSystem) || !is_array($aCommentsSystem))
+            return;
+
+        $sTableMain = $this->aCurrent['table'];
+        $sTableUniqueIds = 'sys_cmts_ids';
+        $sTableUniqueIdsAlias = 'tci';
+
+        $this->aCurrent['restriction']['status_admin'] = [
+            'value' => 'active', 
+            'field' => 'status_admin', 
+            'operator' => '=', 
+            'table' => $sTableUniqueIdsAlias
+        ];
+
+        $this->aCurrent['join']['unique_ids'] = [
+            'type' => 'INNER',
+            'table' => $sTableUniqueIds,
+            'table_alias' => $sTableUniqueIdsAlias,
+            'mainField' => 'cmt_id',
+            'on_sql' => "`{$sTableMain}`.`cmt_id`=`{$sTableUniqueIdsAlias}`.`cmt_id` AND `{$sTableUniqueIdsAlias}`.`system_id`='{$aCommentsSystem['ID']}' ",
+            'joinFields' => array('status_admin'),
+        ];
+    }
+}
+
+/** @} */

+ 599 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/modules/base/general/classes/BxBaseModGeneralMenuViewActions.php

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

+ 3814 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/modules/base/general/classes/BxBaseModGeneralModule.php

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

+ 232 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/modules/base/general/classes/BxBaseModGeneralSearchResult.php

@@ -0,0 +1,232 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+define('BX_SYS_PER_PAGE_BROWSE_SHOWCASE', 32);
+
+class BxBaseModGeneralSearchResult extends BxTemplSearchResult
+{
+    protected $oModule;
+    protected $sFilterName;
+    protected $bShowcaseView = false;
+    protected $aUnitViews = array();
+    protected $sUnitViewDefault = 'gallery';
+
+    function __construct($sMode = '', $aParams = array())
+    {
+        $this->_sMode = $sMode;
+        $this->_aParams = $aParams;
+
+        parent::__construct();
+    }
+
+    function getMain()
+    {
+        if(!$this->oModule)
+            $this->oModule = BxDolModule::getInstance($this->getModuleName());
+
+        return $this->oModule;
+    }
+
+    function getContentInfoObject()
+    {
+        return BxDolContentInfo::getObjectInstance($this->getContentInfoName());
+    }
+
+    function getRssUnitLink (&$a)
+    {
+        $CNF = &$this->oModule->_oConfig->CNF;
+
+        return bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $a[$CNF['FIELD_ID']]));
+    }
+
+    function getRssPageUrl ()
+    {
+        if (false === parent::getRssPageUrl())
+            return false;
+
+        $oPermalinks = BxDolPermalinks::getInstance();
+        return bx_absolute_url($oPermalinks->permalink($this->aCurrent['rss']['link']));
+    }
+
+    function rss ()
+    {
+        if (!isset($this->aCurrent['rss']))
+            return '';
+
+        $this->aCurrent['paginate']['perPage'] = empty($this->oModule->_oConfig->CNF['PARAM_NUM_RSS']) ? 10 : getParam($this->oModule->_oConfig->CNF['PARAM_NUM_RSS']);
+
+        return parent::rss();
+    }
+
+    protected function addCustomConditions($CNF, $oProfile, $sMode, $aParams)
+    {
+        $this->addConditionsForAuthorStatus($CNF);
+
+        $this->addConditionsForCf($CNF);
+
+        if(!empty($aParams['filter']) && is_array($aParams['filter']))
+            $this->addConditionsForFilter($CNF, $sMode, $aParams);
+    }
+
+    protected function addConditionsForAuthorStatus($CNF)
+    {
+        if (empty($CNF['FIELD_AUTHOR']))
+            return;
+
+        $this->aCurrent['restriction']['statusAuthor'] = [
+            'value' => 'active',
+            'field' => 'status',
+            'operator' => '=',
+            'table' => 'sys_profiles',
+        ];
+
+        $this->aCurrent['join']['statusAuthor'] = [
+            'type' => 'INNER',
+            'table' => 'sys_profiles',
+            'mainField' => $CNF['FIELD_AUTHOR'],
+            'mainFieldFunc' => 'ABS',
+            'onField' => 'id',
+            'joinFields' => array(),
+        ];
+    }
+
+    protected function addConditionsForCf($CNF)
+    {
+        if(empty($CNF['FIELD_CF']))
+            return;
+
+        $oCf = BxDolContentFilter::getInstance();
+        if(!$oCf->isEnabled()) 
+            return;
+
+        $aConditions = $oCf->getConditions($this->aCurrent['table'], $CNF['FIELD_CF']);
+        if(!empty($aConditions) && is_array($aConditions))
+            $this->aCurrent['restriction'] = array_merge($this->aCurrent['restriction'], $aConditions);
+    }
+    
+    protected function addConditionsForFilter($CNF, $sMode, $aParams)
+    {
+        $aFilter = $aParams['filter'];
+        
+        if(empty($aFilter['field']) || empty($aFilter['value']))
+            return;
+        
+        $aRestriction = [
+            'value' => $aFilter['value'],
+            'field' => $aFilter['field'],
+            'operator' => '=',
+        ];
+
+        if(isset($aFilter['operator']))
+            $aRestriction['operator'] = $aFilter['operator'];
+
+        if(isset($aFilter['table']))
+            switch($aFilter['table']) {
+                case 'table':
+                    $aRestriction['table'] = $this->aCurrent['table'];
+                    break;
+
+                case 'tableSearch':
+                    $aRestriction['table'] = $this->aCurrent['tableSearch'];
+                    break;
+
+                default: 
+                    $aRestriction['table'] = $aFilter['table'];
+            }
+
+        $this->aCurrent['restriction']['filter'] = $aRestriction;
+    }
+
+    /**
+     * Add conditions for private content
+     */
+    protected function addConditionsForPrivateContent($CNF, $oProfile, $aCustomGroup = array()) 
+    {
+        // default is bProcessPrivateContent = 1, 
+        // so private items are shown as empty boxes with "Private" title
+
+        // we can show public content when privacy object is available
+        
+        if(empty($CNF['OBJECT_PRIVACY_VIEW']))
+            return;
+
+        $oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW']);
+        if(!$oPrivacy)
+            return;
+
+        // for posts in some context we need to show all items, not only public content
+        if (!empty($this->aCurrent['restriction']['context']['value']) || !empty($this->aCurrent['restriction']['author']['value'])) {
+            $this->setProcessPrivateContent(true);
+            return;
+        }
+
+        // build condition to show only public content
+        $aCondition = $oPrivacy->getContentPublicAsCondition($oProfile ? $oProfile->id() : 0, $aCustomGroup);
+        if(empty($aCondition) || !is_array($aCondition))
+            return;
+
+        if(isset($aCondition['restriction'])) {
+            $this->aCurrent['restriction'] = array_merge($this->aCurrent['restriction'], $aCondition['restriction']);
+            $this->aPrivateConditionsIndexes['restriction'] = array_keys($aCondition['restriction']);
+        }
+        if(isset($aCondition['join'])) {
+            $this->aCurrent['join'] = array_merge($this->aCurrent['join'], $aCondition['join']);
+            $this->aPrivateConditionsIndexes['join'] = array_keys($aCondition['join']);
+        }
+
+        $this->setProcessPrivateContent(false);
+    }
+
+    function showPagination($bAdmin = false, $bChangePage = true, $bPageReload = true)
+    {
+        if($this->bShowcaseView)
+            return '';
+
+        $sPagination = parent::showPagination ($bAdmin, $bChangePage, $bPageReload);
+        if(empty($sPagination))
+            return '';
+
+        return $sPagination;
+    }
+
+    protected function getItemPerPageInShowCase ()
+    {
+        $iPerPageInShowCase = BX_SYS_PER_PAGE_BROWSE_SHOWCASE;
+        $CNF = &$this->oModule->_oConfig->CNF;
+        if (isset($CNF['PARAM_PER_PAGE_BROWSE_SHOWCASE']))
+            $iPerPageInShowCase = getParam($CNF['PARAM_PER_PAGE_BROWSE_SHOWCASE']);
+        return $iPerPageInShowCase;
+    }
+    
+    function displayResultBlock()
+    {
+        if ($this->bShowcaseView) {
+            $this->addContainerClass(array('bx-base-unit-showcase-wrapper'));
+            $this->aCurrent['paginate']['perPage'] = $this->getItemPerPageInShowCase();
+            $this->oModule->_oTemplate->addCss(array(BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'flickity/|flickity.css'));
+            $this->oModule->_oTemplate->addJs(array('flickity/flickity.pkgd.min.js','modules/base/general/js/|showcase.js'));
+        }
+
+        return parent::displayResultBlock();
+    }
+
+    function decodeDataAPI($a)
+    {
+        $bExtendedUnits = getParam('sys_api_extended_units') == 'on';
+
+        foreach($a as $i => $r)
+            $a[$i] = $this->oModule->getDataAPI($r, ['extended' => $bExtendedUnits]);
+
+        return $a;
+    }
+}
+
+/** @} */

+ 54 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/modules/base/groups/classes/BxBaseModGroupsMenuViewActionsAll.php

@@ -0,0 +1,54 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * View entry all actions menu
+ */
+class BxBaseModGroupsMenuViewActionsAll extends BxBaseModProfileMenuViewActionsAll
+{
+    public function __construct($aObject, $oTemplate = false)
+    {
+        parent::__construct($aObject, $oTemplate);
+        
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        
+        if(isset($CNF['OBJECT_CONNECTIONS']))
+            $this->_aConnectionToFunctionCheck[$CNF['OBJECT_CONNECTIONS']] = [
+                'add' => 'checkAllowedFanAdd', 
+                'remove' => 'checkAllowedFanRemove'
+            ];
+    }
+
+    protected function _getMenuItemProfileFanAdd($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        
+        if($this->_bIsApi && $CNF['OBJECT_CONNECTIONS'])
+            return $this->_getMenuItemConnectionApi($CNF['OBJECT_CONNECTIONS'], 'add', $aItem);
+        return $this->_getMenuItemByNameActions($aItem);
+    }
+
+    protected function _getMenuItemProfileFanAddPaid($aItem)
+    {
+        return $this->_getMenuItemByNameActions($aItem);
+    }
+
+    protected function _getMenuItemProfileFanRemove($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        
+        if($this->_bIsApi && $CNF['OBJECT_CONNECTIONS'])
+            return $this->_getMenuItemConnectionApi($CNF['OBJECT_CONNECTIONS'], 'remove', $aItem);
+        return $this->_getMenuItemByNameActionsMore($aItem);
+    }
+}
+
+/** @} */

+ 2304 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/modules/base/profile/classes/BxBaseModProfileModule.php

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

+ 762 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/modules/base/profile/classes/BxBaseModProfileTemplate.php

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

+ 508 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/akeeba/s3/src/Signature/V4.php

@@ -0,0 +1,508 @@
+<?php
+/**
+ * Akeeba Engine
+ *
+ * @package   akeebaengine
+ * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
+ * @license   GNU General Public License version 3, or later
+ */
+
+namespace Akeeba\S3\Signature;
+
+// Protection against direct access
+defined('AKEEBAENGINE') || die();
+
+use Akeeba\S3\Signature;
+use DateTime;
+
+/**
+ * Implements the Amazon AWS v4 signatures
+ *
+ * @see http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
+ */
+class V4 extends Signature
+{
+	/**
+	 * Pre-process the request headers before we convert them to cURL-compatible format. Used by signature engines to
+	 * add custom headers, e.g. x-amz-content-sha256
+	 *
+	 * @param   array  $headers     The associative array of headers to process
+	 * @param   array  $amzHeaders  The associative array of amz-* headers to process
+	 *
+	 * @return  void
+	 */
+	public function preProcessHeaders(array &$headers, array &$amzHeaders): void
+	{
+		// Do we already have an SHA-256 payload hash?
+		if (isset($amzHeaders['x-amz-content-sha256']))
+		{
+			return;
+		}
+
+		// Set the payload hash header
+		$input = $this->request->getInput();
+
+		if (is_object($input))
+		{
+			$requestPayloadHash = $input->getSha256();
+		}
+		else
+		{
+			$requestPayloadHash = hash('sha256', '', false);
+		}
+
+		$amzHeaders['x-amz-content-sha256'] = $requestPayloadHash;
+	}
+
+	/**
+	 * Get a pre-signed URL for the request. Typically used to pre-sign GET requests to objects, i.e. give shareable
+	 * pre-authorized URLs for downloading files from S3.
+	 *
+	 * @param   integer|null  $lifetime  Lifetime in seconds. NULL for default lifetime.
+	 * @param   bool          $https     Use HTTPS ($hostBucket should be false for SSL verification)?
+	 *
+	 * @return  string  The presigned URL
+	 */
+	public function getAuthenticatedURL(?int $lifetime = null, bool $https = false): string
+	{
+		// Set the Expires header
+		if (is_null($lifetime))
+		{
+			$lifetime = 10;
+		}
+
+		/**
+		 * Authenticated URLs must always go through the generic regional endpoint, not the virtual hosting-style domain
+		 * name. This means that if you have a bucket "example" in the EU West 1 (Ireland) region we have to go through
+		 * http://s3-eu-west-1.amazonaws.com/example instead of http://example.amazonaws.com/ for all authenticated URLs
+		 */
+		$region   = $this->request->getConfiguration()->getRegion();
+		$bucket   = $this->request->getBucket();
+		$hostname = $this->getPresignedHostnameForRegion($region);
+
+		if ($this->isValidBucketName($bucket))
+		{
+			$hostname = $bucket . '.' . $hostname;
+		}
+
+		$this->request->setHeader('Host', $hostname);
+
+		// Set the expiration time in seconds
+		$this->request->setHeader('Expires', (int) $lifetime);
+
+		// Get the query parameters, including the calculated signature
+		$uri              = $this->request->getResource();
+		$headers          = $this->request->getHeaders();
+		$protocol         = $https ? 'https' : 'http';
+		$serialisedParams = $this->getAuthorizationHeader();
+
+		// The query parameters are returned serialized; unserialize them, then build and return the URL.
+		$queryParameters = unserialize($serialisedParams);
+
+		if ($this->isValidBucketName($bucket) && strpos($uri, '/' . $bucket) === 0)
+		{
+			$uri = substr($uri, strlen($bucket) + 1);
+		}
+
+		$query = http_build_query($queryParameters);
+
+		$url = $protocol . '://' . $headers['Host'] . $uri;
+		$url .= (strpos($uri, '?') !== false) ? '&' : '?';
+		$url .= $query;
+
+		return $url;
+	}
+
+	/**
+	 * Returns the authorization header for the request
+	 *
+	 * @return  string
+	 */
+	public function getAuthorizationHeader(): string
+	{
+		$verb           = strtoupper($this->request->getVerb());
+		$resourcePath   = $this->request->getResource();
+		$headers        = $this->request->getHeaders();
+		$amzHeaders     = $this->request->getAmzHeaders();
+		$parameters     = $this->request->getParameters();
+		$bucket         = $this->request->getBucket();
+		$isPresignedURL = false;
+
+		// See the Connector class for the explanation behind this ugly workaround
+		$amazonIsBraindead = isset($headers['workaround-braindead-error-from-amazon']);
+
+		if ($amazonIsBraindead)
+		{
+			unset ($headers['workaround-braindead-error-from-amazon']);
+		}
+
+		// Get the credentials scope
+		$signatureDate = new DateTime($headers['Date']);
+
+		$credentialScope = $signatureDate->format('Ymd') . '/' .
+		                   $this->request->getConfiguration()->getRegion() . '/' .
+		                   's3/aws4_request';
+
+		/**
+		 * If the Expires header is set up we're pre-signing a download URL. The string to sign is a bit
+		 * different in this case and we have to pass certain headers as query string parameters.
+		 *
+		 * @see http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
+		 */
+		if (isset($headers['Expires']))
+		{
+			$gmtDate = clone $signatureDate;
+			$gmtDate->setTimezone(new \DateTimeZone('GMT'));
+
+			$parameters['X-Amz-Algorithm']  = "AWS4-HMAC-SHA256";
+			$parameters['X-Amz-Credential'] = $this->request->getConfiguration()->getAccess() . '/' . $credentialScope;
+			$parameters['X-Amz-Date']       = $gmtDate->format('Ymd\THis\Z');
+			$parameters['X-Amz-Expires']    = sprintf('%u', $headers['Expires']);
+			$token                          = $this->request->getConfiguration()->getToken();
+
+			if (!empty($token))
+			{
+				$parameters['x-amz-security-token'] = $token;
+			}
+
+			unset($headers['Expires']);
+			unset($headers['Date']);
+			unset($headers['Content-MD5']);
+			unset($headers['Content-Type']);
+
+			$isPresignedURL = true;
+		}
+
+		// ========== Step 1: Create a canonical request ==========
+		// See http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
+
+		$canonicalHeaders   = "";
+		$signedHeadersArray = [];
+
+		// Calculate the canonical headers and the signed headers
+		if ($isPresignedURL)
+		{
+			// Presigned URLs use UNSIGNED-PAYLOAD instead
+			unset($amzHeaders['x-amz-content-sha256']);
+		}
+
+		$allHeaders = array_merge($headers, $amzHeaders);
+		ksort($allHeaders);
+
+		foreach ($allHeaders as $k => $v)
+		{
+			$lowercaseHeaderName = strtolower($k);
+
+			if ($amazonIsBraindead && ($lowercaseHeaderName == 'content-length'))
+			{
+				/**
+				 * I know it looks crazy. It is. Somehow Amazon requires me to do this and only on _some_ servers, mind
+				 * you. This is something undocumented and which is not covered by their official SDK. I had to write
+				 * my own library because of that and the official SDK's inability to upload large files without using
+				 * at least as much memory as the file itself (which doesn't fly well for files around 2Gb, let me tell
+				 * you that!).
+				 */
+				$v = "$v,$v";
+			}
+
+			$canonicalHeaders     .= $lowercaseHeaderName . ':' . trim($v) . "\n";
+			$signedHeadersArray[] = $lowercaseHeaderName;
+		}
+
+		$signedHeaders = implode(';', $signedHeadersArray);
+
+		if ($isPresignedURL)
+		{
+			$parameters['X-Amz-SignedHeaders'] = $signedHeaders;
+		}
+
+		// The canonical URI is the resource path
+		$canonicalURI     = $resourcePath;
+		$bucketResource   = '/' . $bucket;
+		$regionalHostname = ($headers['Host'] != 's3.amazonaws.com')
+		                    && ($headers['Host'] != $bucket . '.s3.amazonaws.com');
+
+		// Special case: if the canonical URI ends in /?location the bucket name DOES count as part of the canonical URL
+		// even though the Host is s3.amazonaws.com (in which case it normally shouldn't count). Yeah, I know, it makes
+		// no sense!!!
+		if (!$regionalHostname && ($headers['Host'] == 's3.amazonaws.com')
+		    && (substr($canonicalURI, -10) == '/?location'))
+		{
+			$regionalHostname = true;
+		}
+
+		if (!$regionalHostname && (strpos($canonicalURI, $bucketResource) === 0))
+		{
+			if ($canonicalURI === $bucketResource)
+			{
+				$canonicalURI = '/';
+			}
+			else
+			{
+				$canonicalURI = substr($canonicalURI, strlen($bucketResource));
+			}
+		}
+
+		// If the resource path has a query yank it and parse it into the parameters array
+		$questionMarkPos = strpos($canonicalURI, '?');
+
+		if ($questionMarkPos !== false)
+		{
+			$canonicalURI = substr($canonicalURI, 0, $questionMarkPos);
+			$queryString  = @substr($canonicalURI, $questionMarkPos + 1);
+			@parse_str($queryString, $extraQuery);
+
+			if (count($extraQuery))
+			{
+				$parameters = array_merge($parameters, $extraQuery);
+			}
+		}
+
+		// The canonical query string is the string representation of $parameters, alpha sorted by key
+		ksort($parameters);
+
+		// We build the query the hard way because http_build_query in PHP 5.3 does NOT have the fourth parameter
+		// (encoding type), defaulting to RFC 1738 encoding whereas S3 expects RFC 3986 encoding
+		$canonicalQueryString = '';
+
+		if (!empty($parameters))
+		{
+			$temp = [];
+
+			foreach ($parameters as $k => $v)
+			{
+				$temp[] = $this->urlencode($k) . '=' . $this->urlencode($v);
+			}
+
+			$canonicalQueryString = implode('&', $temp);
+		}
+
+		// Get the payload hash
+		$requestPayloadHash = 'UNSIGNED-PAYLOAD';
+
+		if (isset($amzHeaders['x-amz-content-sha256']))
+		{
+			$requestPayloadHash = $amzHeaders['x-amz-content-sha256'];
+		}
+
+		// Calculate the canonical request
+		$canonicalRequest = $verb . "\n" .
+		                    $canonicalURI . "\n" .
+		                    $canonicalQueryString . "\n" .
+		                    $canonicalHeaders . "\n" .
+		                    $signedHeaders . "\n" .
+		                    $requestPayloadHash;
+
+		$hashedCanonicalRequest = hash('sha256', $canonicalRequest);
+
+		// ========== Step 2: Create a string to sign ==========
+		// See http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
+
+		if (!isset($headers['Date']))
+		{
+			$headers['Date'] = '';
+		}
+
+		/**
+		 * The Date in the String-to-Sign is a messy situation.
+		 *
+		 * Amazon's documentation says it must be in ISO 8601 format: `Ymd\THis\Z`. Unfortunately, Amazon's
+		 * documentation is actually wrong :troll_face: The actual Amazon S3 service expects the date to be formatted as
+		 * per RFC1123.
+		 *
+		 * Most third party implementations have caught up to the fact that Amazon has documented the v4 signatures
+		 * wrongly (naughty AWS!) and accept either format.
+		 *
+		 * Some other third party implementations, which never bothered to validate their implementations against Amazon
+		 * S3 proper, only expect what Amazon has documented as "ISO 8601". Therefore, we detect third party services
+		 * and switch to the as-documented date format.
+		 *
+		 * Some other third party services, like Wasabi, are broken in yet a different way. They will only use the date
+		 * from the x-amz-date header, WITHOUT falling back to the Date header if the former is not present. This is
+		 * the opposite of Amazon S3 proper which does expect the Date header. That's why the Request class sets both
+		 * headers if the request is made to a service _other_ than Amazon S3 proper.
+		 */
+		$dateToSignFor = strpos($headers['Host'], '.amazonaws.com') !== false
+			? $headers['Date']
+			: $signatureDate->format('Ymd\THis\Z');
+
+		$stringToSign = "AWS4-HMAC-SHA256\n" .
+		                $dateToSignFor . "\n" .
+		                $credentialScope . "\n" .
+		                $hashedCanonicalRequest;
+
+		if ($isPresignedURL)
+		{
+			$stringToSign = "AWS4-HMAC-SHA256\n" .
+			                $parameters['X-Amz-Date'] . "\n" .
+			                $credentialScope . "\n" .
+			                $hashedCanonicalRequest;
+		}
+
+		// ========== Step 3: Calculate the signature ==========
+		// See http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
+		$kSigning = $this->getSigningKey($signatureDate);
+
+		$signature = hash_hmac('sha256', $stringToSign, $kSigning, false);
+
+		// ========== Step 4: Add the signing information to the Request ==========
+		// See http://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html
+
+		$authorization = 'AWS4-HMAC-SHA256 Credential=' .
+		                 $this->request->getConfiguration()->getAccess() . '/' . $credentialScope . ', ' .
+		                 'SignedHeaders=' . $signedHeaders . ', ' .
+		                 'Signature=' . $signature;
+
+		// For presigned URLs we only return the Base64-encoded signature without the AWS format specifier and the
+		// public access key.
+		if ($isPresignedURL)
+		{
+			$parameters['X-Amz-Signature'] = $signature;
+
+			return serialize($parameters);
+		}
+
+		return $authorization;
+	}
+
+	/**
+	 * Calculate the AWS4 signing key
+	 *
+	 * @param   DateTime  $signatureDate  The date the signing key is good for
+	 *
+	 * @return  string
+	 */
+	private function getSigningKey(DateTime $signatureDate): string
+	{
+		$kSecret  = $this->request->getConfiguration()->getSecret();
+		$kDate    = hash_hmac('sha256', $signatureDate->format('Ymd'), 'AWS4' . $kSecret, true);
+		$kRegion  = hash_hmac('sha256', $this->request->getConfiguration()->getRegion(), $kDate, true);
+		$kService = hash_hmac('sha256', 's3', $kRegion, true);
+		$kSigning = hash_hmac('sha256', 'aws4_request', $kService, true);
+
+		return $kSigning;
+	}
+
+	private function urlencode(?string $toEncode): string
+	{
+		if (empty($toEncode))
+		{
+			return '';
+		}
+
+		return str_replace('+', '%20', urlencode($toEncode));
+	}
+
+	/**
+	 * Get the correct hostname for the given AWS region
+	 *
+	 * @param   string  $region
+	 *
+	 * @return  string
+	 */
+	private function getPresignedHostnameForRegion(string $region): string
+	{
+		$config   = $this->request->getConfiguration();
+		$endpoint = $config->getEndpoint();
+
+		if (empty($endpoint))
+		{
+			$endpoint = 's3.' . $region . '.amazonaws.com';
+		}
+
+		// As of October 2023, AWS does not consider DualStack signed URLs as valid. Whatever.
+		$dualstackEnabled = false && $this->request->getConfiguration()->getDualstackUrl();
+
+		// If dual-stack URLs are enabled then prepend the endpoint
+		if ($dualstackEnabled)
+		{
+			$endpoint = 's3.dualstack.' . $region . '.amazonaws.com';
+		}
+
+		if ($region == 'cn-north-1')
+		{
+			return $endpoint . '.cn';
+		}
+
+		return $endpoint;
+	}
+
+	/**
+	 * Is this a valid bucket name?
+	 *
+	 * @param   string  $bucketName   The bucket name to check
+	 * @param   bool    $asSubdomain  Should I put additional restrictions for use as a subdomain?
+	 *
+	 * @return  bool
+	 * @since   2.3.1
+	 *
+	 * @see     https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html
+	 */
+	private function isValidBucketName(string $bucketName, bool $asSubdomain = true): bool
+	{
+		/**
+		 * If there are dots in the bucket name I can't use it as a subdomain.
+		 *
+		 * "If you include dots in a bucket's name, you can't use virtual-host-style addressing over HTTPS, unless you
+		 * perform your own certificate validation. This is because the security certificates used for virtual hosting
+		 * of buckets don't work for buckets with dots in their names."
+		 */
+		if ($asSubdomain && strpos($bucketName, '.') !== false)
+		{
+			return false;
+		}
+
+		/**
+		 * - Bucket names must be between 3 (min) and 63 (max) characters long.
+		 * - Bucket names can consist only of lowercase letters, numbers, dots (.), and hyphens (-).
+		 */
+		if (!preg_match('/^[a-z0-9\-.]{3,63}$/', $bucketName))
+		{
+			return false;
+		}
+
+		// Bucket names must begin and end with a letter or number.
+		if (!preg_match('/^[a-z0-9].*[a-z0-9]$/', $bucketName))
+		{
+			return false;
+		}
+
+		// Bucket names must not contain two adjacent periods.
+		if (preg_match('/\.\./', $bucketName))
+		{
+			return false;
+		}
+
+		// Bucket names must not be formatted as an IP address (for example, 192.168.5.4).
+		if (preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/', $bucketName))
+		{
+			return false;
+		}
+
+		// Bucket names must not start with the prefix xn--.
+		if (strpos($bucketName, 'xn--') === 0)
+		{
+			return false;
+		}
+
+		// Bucket names must not start with the prefix sthree- and the prefix sthree-configurator.
+		if (strpos($bucketName, 'sthree-') === 0)
+		{
+			return false;
+		}
+
+		// Bucket names must not end with the suffix -s3alias.
+		if (substr($bucketName, -8) === '-s3alias')
+		{
+			return false;
+		}
+
+		// Bucket names must not end with the suffix --ol-s3.
+		if (substr($bucketName, -7) === '--ol-s3')
+		{
+			return false;
+		}
+
+		return true;
+	}
+}

+ 7 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/autoload.php

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

+ 266 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/chargebee/chargebee-php/lib/ChargeBee/Models/Invoice.php

@@ -0,0 +1,266 @@
+<?php
+
+namespace ChargeBee\ChargeBee\Models;
+
+use ChargeBee\ChargeBee\Model;
+use ChargeBee\ChargeBee\Request;
+use ChargeBee\ChargeBee\Util;
+
+class Invoice extends Model
+{
+
+  protected $allowed = [
+    'id',
+    'poNumber',
+    'customerId',
+    'subscriptionId',
+    'recurring',
+    'status',
+    'vatNumber',
+    'priceType',
+    'date',
+    'dueDate',
+    'netTermDays',
+    'exchangeRate',
+    'currencyCode',
+    'total',
+    'amountPaid',
+    'amountAdjusted',
+    'writeOffAmount',
+    'creditsApplied',
+    'amountDue',
+    'paidAt',
+    'dunningStatus',
+    'nextRetryAt',
+    'voidedAt',
+    'resourceVersion',
+    'updatedAt',
+    'subTotal',
+    'subTotalInLocalCurrency',
+    'totalInLocalCurrency',
+    'localCurrencyCode',
+    'tax',
+    'localCurrencyExchangeRate',
+    'firstInvoice',
+    'newSalesAmount',
+    'hasAdvanceCharges',
+    'termFinalized',
+    'isGifted',
+    'generatedAt',
+    'expectedPaymentDate',
+    'amountToCollect',
+    'roundOffAmount',
+    'lineItems',
+    'discounts',
+    'lineItemDiscounts',
+    'taxes',
+    'lineItemTaxes',
+    'lineItemTiers',
+    'linkedPayments',
+    'dunningAttempts',
+    'appliedCredits',
+    'adjustmentCreditNotes',
+    'issuedCreditNotes',
+    'linkedOrders',
+    'notes',
+    'shippingAddress',
+    'statementDescriptor',
+    'billingAddress',
+    'einvoice',
+    'paymentOwner',
+    'voidReasonCode',
+    'deleted',
+    'taxCategory',
+    'vatNumberPrefix',
+    'channel',
+    'businessEntityId',
+  ];
+
+
+
+  # OPERATIONS
+  #-----------
+
+  public static function create($params = array(), $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices"), $params, $env, $headers);
+  }
+
+  public static function createForChargeItemsAndCharges($params, $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices","create_for_charge_items_and_charges"), $params, $env, $headers);
+  }
+
+  public static function charge($params, $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices","charge"), $params, $env, $headers);
+  }
+
+  public static function chargeAddon($params, $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices","charge_addon"), $params, $env, $headers);
+  }
+
+  public static function createForChargeItem($params, $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices","create_for_charge_item"), $params, $env, $headers);
+  }
+
+  public static function stopDunning($id, $params = array(), $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"stop_dunning"), $params, $env, $headers);
+  }
+
+  public static function importInvoice($params, $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices","import_invoice"), $params, $env, $headers);
+  }
+
+  public static function applyPayments($id, $params = array(), $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"apply_payments"), $params, $env, $headers);
+  }
+
+  public static function syncUsages($id, $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"sync_usages"), array(), $env, $headers);
+  }
+
+  public static function deleteLineItems($id, $params = array(), $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"delete_line_items"), $params, $env, $headers);
+  }
+
+  public static function applyCredits($id, $params = array(), $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"apply_credits"), $params, $env, $headers);
+  }
+
+  public static function all($params = array(), $env = null, $headers = array())
+  {
+    return Request::sendListRequest(Request::GET, Util::encodeURIPath("invoices"), $params, $env, $headers);
+  }
+
+  public static function invoicesForCustomer($id, $params = array(), $env = null, $headers = array())
+  {
+    return Request::send(Request::GET, Util::encodeURIPath("customers",$id,"invoices"), $params, $env, $headers);
+  }
+
+  public static function invoicesForSubscription($id, $params = array(), $env = null, $headers = array())
+  {
+    return Request::send(Request::GET, Util::encodeURIPath("subscriptions",$id,"invoices"), $params, $env, $headers);
+  }
+
+  public static function retrieve($id, $env = null, $headers = array())
+  {
+    return Request::send(Request::GET, Util::encodeURIPath("invoices",$id), array(), $env, $headers);
+  }
+
+  public static function pdf($id, $params = array(), $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"pdf"), $params, $env, $headers);
+  }
+
+  public static function downloadEinvoice($id, $env = null, $headers = array())
+  {
+    return Request::send(Request::GET, Util::encodeURIPath("invoices",$id,"download_einvoice"), array(), $env, $headers);
+  }
+
+  public static function listPaymentReferenceNumbers($params = array(), $env = null, $headers = array())
+  {
+    return Request::send(Request::GET, Util::encodeURIPath("invoices","payment_reference_numbers"), $params, $env, $headers);
+  }
+
+  public static function addCharge($id, $params, $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"add_charge"), $params, $env, $headers);
+  }
+
+  public static function addAddonCharge($id, $params, $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"add_addon_charge"), $params, $env, $headers);
+  }
+
+  public static function addChargeItem($id, $params, $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"add_charge_item"), $params, $env, $headers);
+  }
+
+  public static function close($id, $params = array(), $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"close"), $params, $env, $headers);
+  }
+
+  public static function collectPayment($id, $params = array(), $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"collect_payment"), $params, $env, $headers);
+  }
+
+  public static function recordPayment($id, $params, $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"record_payment"), $params, $env, $headers);
+  }
+
+  public static function recordTaxWithheld($id, $params, $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"record_tax_withheld"), $params, $env, $headers);
+  }
+
+  public static function removeTaxWithheld($id, $params, $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"remove_tax_withheld"), $params, $env, $headers);
+  }
+
+  public static function refund($id, $params = array(), $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"refund"), $params, $env, $headers);
+  }
+
+  public static function recordRefund($id, $params, $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"record_refund"), $params, $env, $headers);
+  }
+
+  public static function removePayment($id, $params, $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"remove_payment"), $params, $env, $headers);
+  }
+
+  public static function removeCreditNote($id, $params, $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"remove_credit_note"), $params, $env, $headers);
+  }
+
+  public static function voidInvoice($id, $params = array(), $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"void"), $params, $env, $headers);
+  }
+
+  public static function writeOff($id, $params = array(), $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"write_off"), $params, $env, $headers);
+  }
+
+  public static function delete($id, $params = array(), $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"delete"), $params, $env, $headers);
+  }
+
+  public static function updateDetails($id, $params = array(), $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"update_details"), $params, $env, $headers);
+  }
+
+  public static function resendEinvoice($id, $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"resend_einvoice"), array(), $env, $headers);
+  }
+
+  public static function sendEinvoice($id, $env = null, $headers = array())
+  {
+    return Request::send(Request::POST, Util::encodeURIPath("invoices",$id,"send_einvoice"), array(), $env, $headers);
+  }
+
+ }
+
+?>

+ 17 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/chargebee/chargebee-php/lib/ChargeBee/Models/InvoiceStatementDescriptor.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace ChargeBee\ChargeBee\Models;
+
+use ChargeBee\ChargeBee\Model;
+
+class InvoiceStatementDescriptor extends Model
+{
+  protected $allowed = [
+    'id',
+    'descriptor',
+    'additionalInfo',
+  ];
+
+}
+
+?>

+ 770 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/chargebee/chargebee-php/lib/ChargeBee/Result.php

@@ -0,0 +1,770 @@
+<?php
+
+namespace ChargeBee\ChargeBee;
+
+use ChargeBee\ChargeBee\Models;
+
+define('IDEMPOTENCY_REPLAY_HEADER', 'chargebee-idempotency-replayed');
+
+class Result
+{
+    private $_response;
+    private $_responseHeaders;
+    private $_responseObj;
+
+    public function __construct($_response, $_responseHeaders = null)
+    {
+            $this->_response = $_response;
+        $this->_responseHeaders = $_responseHeaders;
+            $this->_responseObj = array();
+    }
+
+    public function getResponseHeaders()
+    {
+        return $this->_responseHeaders;
+    }
+
+    public function isIdempotencyReplayed()
+    {   
+        $headers = $this->getResponseHeaders();
+        if (isset($headers[IDEMPOTENCY_REPLAY_HEADER])) {
+            $value = $headers[IDEMPOTENCY_REPLAY_HEADER][0];
+            return  boolval($value);
+        }
+        return false;
+    }
+    
+    public function subscription() 
+    {
+        $subscription = $this->_get('subscription', Models\Subscription::class, 
+        array( 
+			'subscription_items' => Models\SubscriptionSubscriptionItem::class, 
+			'item_tiers' => Models\SubscriptionItemTier::class, 
+			'charged_items' => Models\SubscriptionChargedItem::class, 
+			'addons' => Models\SubscriptionAddon::class, 
+			'event_based_addons' => Models\SubscriptionEventBasedAddon::class, 
+			'charged_event_based_addons' => Models\SubscriptionChargedEventBasedAddon::class, 
+			'coupons' => Models\SubscriptionCoupon::class, 
+			'shipping_address' => Models\SubscriptionShippingAddress::class, 
+			'referral_info' => Models\SubscriptionReferralInfo::class, 
+			'contract_term' => Models\SubscriptionContractTerm::class, 
+			'discounts' => Models\SubscriptionDiscount::class
+		));
+        return $subscription;
+    }
+
+    public function contractTerm() 
+    {
+        $contract_term = $this->_get('contract_term', Models\ContractTerm::class);
+        return $contract_term;
+    }
+
+    public function discount() 
+    {
+        $discount = $this->_get('discount', Models\Discount::class);
+        return $discount;
+    }
+
+    public function advanceInvoiceSchedule() 
+    {
+        $advance_invoice_schedule = $this->_get('advance_invoice_schedule', Models\AdvanceInvoiceSchedule::class, 
+        array( 
+			'fixed_interval_schedule' => Models\AdvanceInvoiceScheduleFixedIntervalSchedule::class, 
+			'specific_dates_schedule' => Models\AdvanceInvoiceScheduleSpecificDatesSchedule::class
+		));
+        return $advance_invoice_schedule;
+    }
+
+    public function customer() 
+    {
+        $customer = $this->_get('customer', Models\Customer::class, 
+        array( 
+			'billing_address' => Models\CustomerBillingAddress::class, 
+			'referral_urls' => Models\CustomerReferralUrl::class, 
+			'contacts' => Models\CustomerContact::class, 
+			'payment_method' => Models\CustomerPaymentMethod::class, 
+			'balances' => Models\CustomerBalance::class, 
+			'entity_identifiers' => Models\CustomerEntityIdentifier::class, 
+			'relationship' => Models\CustomerRelationship::class, 
+			'parent_account_access' => Models\CustomerParentAccountAccess::class, 
+			'child_account_access' => Models\CustomerChildAccountAccess::class
+		));
+        return $customer;
+    }
+
+    public function hierarchy() 
+    {
+        $hierarchy = $this->_get('hierarchy', Models\Hierarchy::class);
+        return $hierarchy;
+    }
+
+    public function contact() 
+    {
+        $contact = $this->_get('contact', Models\Contact::class);
+        return $contact;
+    }
+
+    public function token() 
+    {
+        $token = $this->_get('token', Models\Token::class);
+        return $token;
+    }
+
+    public function paymentSource() 
+    {
+        $payment_source = $this->_get('payment_source', Models\PaymentSource::class, 
+        array( 
+			'card' => Models\PaymentSourceCard::class, 
+			'bank_account' => Models\PaymentSourceBankAccount::class, 
+			'cust_voucher_source' => Models\PaymentSourceCustVoucherSource::class, 
+			'billing_address' => Models\PaymentSourceBillingAddress::class, 
+			'amazon_payment' => Models\PaymentSourceAmazonPayment::class, 
+			'upi' => Models\PaymentSourceUpi::class, 
+			'paypal' => Models\PaymentSourcePaypal::class, 
+			'venmo' => Models\PaymentSourceVenmo::class, 
+			'mandates' => Models\PaymentSourceMandate::class
+		));
+        return $payment_source;
+    }
+
+    public function thirdPartyPaymentMethod() 
+    {
+        $third_party_payment_method = $this->_get('third_party_payment_method', Models\ThirdPartyPaymentMethod::class);
+        return $third_party_payment_method;
+    }
+
+    public function virtualBankAccount() 
+    {
+        $virtual_bank_account = $this->_get('virtual_bank_account', Models\VirtualBankAccount::class);
+        return $virtual_bank_account;
+    }
+
+    public function card() 
+    {
+        $card = $this->_get('card', Models\Card::class);
+        return $card;
+    }
+
+    public function promotionalCredit() 
+    {
+        $promotional_credit = $this->_get('promotional_credit', Models\PromotionalCredit::class);
+        return $promotional_credit;
+    }
+
+    public function invoice() 
+    {
+        $invoice = $this->_get('invoice', Models\Invoice::class, 
+        array( 
+			'line_items' => Models\InvoiceLineItem::class, 
+			'discounts' => Models\InvoiceDiscount::class, 
+			'line_item_discounts' => Models\InvoiceLineItemDiscount::class, 
+			'taxes' => Models\InvoiceTax::class, 
+			'line_item_taxes' => Models\InvoiceLineItemTax::class, 
+			'line_item_tiers' => Models\InvoiceLineItemTier::class, 
+			'linked_payments' => Models\InvoiceLinkedPayment::class, 
+			'dunning_attempts' => Models\InvoiceDunningAttempt::class, 
+			'applied_credits' => Models\InvoiceAppliedCredit::class, 
+			'adjustment_credit_notes' => Models\InvoiceAdjustmentCreditNote::class, 
+			'issued_credit_notes' => Models\InvoiceIssuedCreditNote::class, 
+			'linked_orders' => Models\InvoiceLinkedOrder::class, 
+			'notes' => Models\InvoiceNote::class, 
+			'shipping_address' => Models\InvoiceShippingAddress::class, 
+			'statement_descriptor' => Models\InvoiceStatementDescriptor::class, 
+			'billing_address' => Models\InvoiceBillingAddress::class, 
+			'einvoice' => Models\InvoiceEinvoice::class
+		));
+        return $invoice;
+    }
+
+    public function paymentReferenceNumber() 
+    {
+        $payment_reference_number = $this->_get('payment_reference_number', Models\PaymentReferenceNumber::class);
+        return $payment_reference_number;
+    }
+
+    public function taxWithheld() 
+    {
+        $tax_withheld = $this->_get('tax_withheld', Models\TaxWithheld::class);
+        return $tax_withheld;
+    }
+
+    public function creditNote() 
+    {
+        $credit_note = $this->_get('credit_note', Models\CreditNote::class, 
+        array( 
+			'einvoice' => Models\CreditNoteEinvoice::class, 
+			'line_items' => Models\CreditNoteLineItem::class, 
+			'discounts' => Models\CreditNoteDiscount::class, 
+			'line_item_discounts' => Models\CreditNoteLineItemDiscount::class, 
+			'line_item_tiers' => Models\CreditNoteLineItemTier::class, 
+			'taxes' => Models\CreditNoteTax::class, 
+			'line_item_taxes' => Models\CreditNoteLineItemTax::class, 
+			'linked_refunds' => Models\CreditNoteLinkedRefund::class, 
+			'allocations' => Models\CreditNoteAllocation::class, 
+			'shipping_address' => Models\CreditNoteShippingAddress::class, 
+			'billing_address' => Models\CreditNoteBillingAddress::class
+		));
+        return $credit_note;
+    }
+
+    public function unbilledCharge() 
+    {
+        $unbilled_charge = $this->_get('unbilled_charge', Models\UnbilledCharge::class, 
+        array( 
+			'tiers' => Models\UnbilledChargeTier::class
+		));
+        return $unbilled_charge;
+    }
+
+    public function order() 
+    {
+        $order = $this->_get('order', Models\Order::class, 
+        array( 
+			'order_line_items' => Models\OrderOrderLineItem::class, 
+			'shipping_address' => Models\OrderShippingAddress::class, 
+			'billing_address' => Models\OrderBillingAddress::class, 
+			'line_item_taxes' => Models\OrderLineItemTax::class, 
+			'line_item_discounts' => Models\OrderLineItemDiscount::class, 
+			'linked_credit_notes' => Models\OrderLinkedCreditNote::class, 
+			'resent_orders' => Models\OrderResentOrder::class
+		));
+        return $order;
+    }
+
+    public function gift() 
+    {
+        $gift = $this->_get('gift', Models\Gift::class, 
+        array( 
+			'gifter' => Models\GiftGifter::class, 
+			'gift_receiver' => Models\GiftGiftReceiver::class, 
+			'gift_timelines' => Models\GiftGiftTimeline::class
+		));
+        return $gift;
+    }
+
+    public function transaction() 
+    {
+        $transaction = $this->_get('transaction', Models\Transaction::class, 
+        array( 
+			'linked_invoices' => Models\TransactionLinkedInvoice::class, 
+			'linked_credit_notes' => Models\TransactionLinkedCreditNote::class, 
+			'linked_refunds' => Models\TransactionLinkedRefund::class, 
+			'linked_payments' => Models\TransactionLinkedPayment::class
+		));
+        return $transaction;
+    }
+
+    public function hostedPage() 
+    {
+        $hosted_page = $this->_get('hosted_page', Models\HostedPage::class);
+        return $hosted_page;
+    }
+
+    public function estimate() 
+    {
+        $estimate = $this->_get('estimate', Models\Estimate::class, array(),
+        array( 
+			'subscription_estimate' => Models\SubscriptionEstimate::class, 
+			'subscription_estimates' => Models\SubscriptionEstimate::class, 
+			'invoice_estimate' => Models\InvoiceEstimate::class, 
+			'invoice_estimates' => Models\InvoiceEstimate::class, 
+			'next_invoice_estimate' => Models\InvoiceEstimate::class, 
+			'credit_note_estimates' => Models\CreditNoteEstimate::class, 
+			'unbilled_charge_estimates' => Models\UnbilledCharge::class
+		));
+        $estimate->_initDependant($this->_response['estimate'], 'subscription_estimate', 
+        array( 
+			'shipping_address' => Models\SubscriptionEstimateShippingAddress::class, 
+			'contract_term' => Models\SubscriptionEstimateContractTerm::class
+		));
+        $estimate->_initDependant($this->_response['estimate'], 'invoice_estimate', 
+        array( 
+			'line_items' => Models\InvoiceEstimateLineItem::class, 
+			'discounts' => Models\InvoiceEstimateDiscount::class, 
+			'taxes' => Models\InvoiceEstimateTax::class, 
+			'line_item_taxes' => Models\InvoiceEstimateLineItemTax::class, 
+			'line_item_tiers' => Models\InvoiceEstimateLineItemTier::class, 
+			'line_item_discounts' => Models\InvoiceEstimateLineItemDiscount::class
+		));
+        $estimate->_initDependant($this->_response['estimate'], 'next_invoice_estimate', 
+        array( 
+			'line_items' => Models\InvoiceEstimateLineItem::class, 
+			'discounts' => Models\InvoiceEstimateDiscount::class, 
+			'taxes' => Models\InvoiceEstimateTax::class, 
+			'line_item_taxes' => Models\InvoiceEstimateLineItemTax::class, 
+			'line_item_tiers' => Models\InvoiceEstimateLineItemTier::class, 
+			'line_item_discounts' => Models\InvoiceEstimateLineItemDiscount::class
+		));
+        $estimate->_initDependantList($this->_response['estimate'], 'subscription_estimates', 
+        array( 
+			'shipping_address' => Models\SubscriptionEstimateShippingAddress::class, 
+			'contract_term' => Models\SubscriptionEstimateContractTerm::class
+		));
+        $estimate->_initDependantList($this->_response['estimate'], 'invoice_estimates', 
+        array( 
+			'line_items' => Models\InvoiceEstimateLineItem::class, 
+			'discounts' => Models\InvoiceEstimateDiscount::class, 
+			'taxes' => Models\InvoiceEstimateTax::class, 
+			'line_item_taxes' => Models\InvoiceEstimateLineItemTax::class, 
+			'line_item_tiers' => Models\InvoiceEstimateLineItemTier::class, 
+			'line_item_discounts' => Models\InvoiceEstimateLineItemDiscount::class
+		));
+        $estimate->_initDependantList($this->_response['estimate'], 'credit_note_estimates', 
+        array( 
+			'line_items' => Models\CreditNoteEstimateLineItem::class, 
+			'discounts' => Models\CreditNoteEstimateDiscount::class, 
+			'taxes' => Models\CreditNoteEstimateTax::class, 
+			'line_item_taxes' => Models\CreditNoteEstimateLineItemTax::class, 
+			'line_item_discounts' => Models\CreditNoteEstimateLineItemDiscount::class, 
+			'line_item_tiers' => Models\CreditNoteEstimateLineItemTier::class
+		));
+        $estimate->_initDependantList($this->_response['estimate'], 'unbilled_charge_estimates', 
+        array( 
+			'tiers' => Models\UnbilledChargeTier::class
+		));
+        return $estimate;
+    }
+
+    public function quote() 
+    {
+        $quote = $this->_get('quote', Models\Quote::class, 
+        array( 
+			'line_items' => Models\QuoteLineItem::class, 
+			'discounts' => Models\QuoteDiscount::class, 
+			'line_item_discounts' => Models\QuoteLineItemDiscount::class, 
+			'taxes' => Models\QuoteTax::class, 
+			'line_item_taxes' => Models\QuoteLineItemTax::class, 
+			'line_item_tiers' => Models\QuoteLineItemTier::class, 
+			'shipping_address' => Models\QuoteShippingAddress::class, 
+			'billing_address' => Models\QuoteBillingAddress::class
+		));
+        return $quote;
+    }
+
+    public function quotedSubscription() 
+    {
+        $quoted_subscription = $this->_get('quoted_subscription', Models\QuotedSubscription::class, 
+        array( 
+			'addons' => Models\QuotedSubscriptionAddon::class, 
+			'event_based_addons' => Models\QuotedSubscriptionEventBasedAddon::class, 
+			'coupons' => Models\QuotedSubscriptionCoupon::class, 
+			'subscription_items' => Models\QuotedSubscriptionSubscriptionItem::class, 
+			'item_tiers' => Models\QuotedSubscriptionItemTier::class, 
+			'quoted_contract_term' => Models\QuotedSubscriptionQuotedContractTerm::class
+		));
+        return $quoted_subscription;
+    }
+
+    public function quotedCharge() 
+    {
+        $quoted_charge = $this->_get('quoted_charge', Models\QuotedCharge::class, 
+        array( 
+			'charges' => Models\QuotedChargeCharge::class, 
+			'addons' => Models\QuotedChargeAddon::class, 
+			'invoice_items' => Models\QuotedChargeInvoiceItem::class, 
+			'item_tiers' => Models\QuotedChargeItemTier::class, 
+			'coupons' => Models\QuotedChargeCoupon::class
+		));
+        return $quoted_charge;
+    }
+
+    public function quoteLineGroup() 
+    {
+        $quote_line_group = $this->_get('quote_line_group', Models\QuoteLineGroup::class, 
+        array( 
+			'line_items' => Models\QuoteLineGroupLineItem::class, 
+			'discounts' => Models\QuoteLineGroupDiscount::class, 
+			'line_item_discounts' => Models\QuoteLineGroupLineItemDiscount::class, 
+			'taxes' => Models\QuoteLineGroupTax::class, 
+			'line_item_taxes' => Models\QuoteLineGroupLineItemTax::class
+		));
+        return $quote_line_group;
+    }
+
+    public function plan() 
+    {
+        $plan = $this->_get('plan', Models\Plan::class, 
+        array( 
+			'tiers' => Models\PlanTier::class, 
+			'applicable_addons' => Models\PlanApplicableAddon::class, 
+			'attached_addons' => Models\PlanAttachedAddon::class, 
+			'event_based_addons' => Models\PlanEventBasedAddon::class
+		));
+        return $plan;
+    }
+
+    public function addon() 
+    {
+        $addon = $this->_get('addon', Models\Addon::class, 
+        array( 
+			'tiers' => Models\AddonTier::class
+		));
+        return $addon;
+    }
+
+    public function coupon() 
+    {
+        $coupon = $this->_get('coupon', Models\Coupon::class, 
+        array( 
+			'item_constraints' => Models\CouponItemConstraint::class, 
+			'item_constraint_criteria' => Models\CouponItemConstraintCriteria::class, 
+			'coupon_constraints' => Models\CouponCouponConstraint::class
+		));
+        return $coupon;
+    }
+
+    public function couponSet() 
+    {
+        $coupon_set = $this->_get('coupon_set', Models\CouponSet::class);
+        return $coupon_set;
+    }
+
+    public function couponCode() 
+    {
+        $coupon_code = $this->_get('coupon_code', Models\CouponCode::class);
+        return $coupon_code;
+    }
+
+    public function address() 
+    {
+        $address = $this->_get('address', Models\Address::class);
+        return $address;
+    }
+
+    public function usage() 
+    {
+        $usage = $this->_get('usage', Models\Usage::class);
+        return $usage;
+    }
+
+    public function event() 
+    {
+        $event = $this->_get('event', Models\Event::class, 
+        array( 
+			'webhooks' => Models\EventWebhook::class
+		));
+        return $event;
+    }
+
+    public function comment() 
+    {
+        $comment = $this->_get('comment', Models\Comment::class);
+        return $comment;
+    }
+
+    public function download() 
+    {
+        $download = $this->_get('download', Models\Download::class);
+        return $download;
+    }
+
+    public function portalSession() 
+    {
+        $portal_session = $this->_get('portal_session', Models\PortalSession::class, 
+        array( 
+			'linked_customers' => Models\PortalSessionLinkedCustomer::class
+		));
+        return $portal_session;
+    }
+
+    public function siteMigrationDetail() 
+    {
+        $site_migration_detail = $this->_get('site_migration_detail', Models\SiteMigrationDetail::class);
+        return $site_migration_detail;
+    }
+
+    public function resourceMigration() 
+    {
+        $resource_migration = $this->_get('resource_migration', Models\ResourceMigration::class);
+        return $resource_migration;
+    }
+
+    public function timeMachine() 
+    {
+        $time_machine = $this->_get('time_machine', Models\TimeMachine::class);
+        return $time_machine;
+    }
+
+    public function export() 
+    {
+        $export = $this->_get('export', Models\Export::class, 
+        array( 
+			'download' => Models\ExportDownload::class
+		));
+        return $export;
+    }
+
+    public function paymentIntent() 
+    {
+        $payment_intent = $this->_get('payment_intent', Models\PaymentIntent::class, 
+        array( 
+			'payment_attempt' => Models\PaymentIntentPaymentAttempt::class
+		));
+        return $payment_intent;
+    }
+
+    public function itemFamily() 
+    {
+        $item_family = $this->_get('item_family', Models\ItemFamily::class);
+        return $item_family;
+    }
+
+    public function item() 
+    {
+        $item = $this->_get('item', Models\Item::class, 
+        array( 
+			'applicable_items' => Models\ItemApplicableItem::class
+		));
+        return $item;
+    }
+
+    public function itemPrice() 
+    {
+        $item_price = $this->_get('item_price', Models\ItemPrice::class, 
+        array( 
+			'tiers' => Models\ItemPriceTier::class, 
+			'tax_detail' => Models\ItemPriceTaxDetail::class, 
+			'accounting_detail' => Models\ItemPriceAccountingDetail::class
+		));
+        return $item_price;
+    }
+
+    public function attachedItem() 
+    {
+        $attached_item = $this->_get('attached_item', Models\AttachedItem::class);
+        return $attached_item;
+    }
+
+    public function differentialPrice() 
+    {
+        $differential_price = $this->_get('differential_price', Models\DifferentialPrice::class, 
+        array( 
+			'tiers' => Models\DifferentialPriceTier::class, 
+			'parent_periods' => Models\DifferentialPriceParentPeriod::class
+		));
+        return $differential_price;
+    }
+
+    public function feature() 
+    {
+        $feature = $this->_get('feature', Models\Feature::class, 
+        array( 
+			'levels' => Models\FeatureLevel::class
+		));
+        return $feature;
+    }
+
+    public function impactedSubscription() 
+    {
+        $impacted_subscription = $this->_get('impacted_subscription', Models\ImpactedSubscription::class, 
+        array( 
+			'download' => Models\ImpactedSubscriptionDownload::class
+		));
+        return $impacted_subscription;
+    }
+
+    public function impactedItem() 
+    {
+        $impacted_item = $this->_get('impacted_item', Models\ImpactedItem::class, 
+        array( 
+			'download' => Models\ImpactedItemDownload::class
+		));
+        return $impacted_item;
+    }
+
+    public function subscriptionEntitlement() 
+    {
+        $subscription_entitlement = $this->_get('subscription_entitlement', Models\SubscriptionEntitlement::class, 
+        array( 
+			'component' => Models\SubscriptionEntitlementComponent::class
+		));
+        return $subscription_entitlement;
+    }
+
+    public function itemEntitlement() 
+    {
+        $item_entitlement = $this->_get('item_entitlement', Models\ItemEntitlement::class);
+        return $item_entitlement;
+    }
+
+    public function inAppSubscription() 
+    {
+        $in_app_subscription = $this->_get('in_app_subscription', Models\InAppSubscription::class);
+        return $in_app_subscription;
+    }
+
+    public function nonSubscription() 
+    {
+        $non_subscription = $this->_get('non_subscription', Models\NonSubscription::class);
+        return $non_subscription;
+    }
+
+    public function entitlementOverride() 
+    {
+        $entitlement_override = $this->_get('entitlement_override', Models\EntitlementOverride::class);
+        return $entitlement_override;
+    }
+
+    public function purchase() 
+    {
+        $purchase = $this->_get('purchase', Models\Purchase::class);
+        return $purchase;
+    }
+
+
+    public function paymentVoucher() 
+    {
+        $payment_voucher = $this->_get('payment_voucher', Models\PaymentVoucher::class, 
+        array( 
+			'linked_invoices' => Models\PaymentVoucherLinkedInvoice::class
+		));
+        return $payment_voucher;
+    }
+
+
+
+    public function unbilledCharges() 
+    {
+        $unbilled_charges = $this->_getList('unbilled_charges', Models\UnbilledCharge::class,
+        array( 
+			'tiers' => Models\UnbilledChargeTier::class
+		));
+        return $unbilled_charges;
+    }
+    
+    public function creditNotes() 
+    {
+        $credit_notes = $this->_getList('credit_notes', Models\CreditNote::class,
+        array( 
+			'einvoice' => Models\CreditNoteEinvoice::class, 
+			'line_items' => Models\CreditNoteLineItem::class, 
+			'discounts' => Models\CreditNoteDiscount::class, 
+			'line_item_discounts' => Models\CreditNoteLineItemDiscount::class, 
+			'line_item_tiers' => Models\CreditNoteLineItemTier::class, 
+			'taxes' => Models\CreditNoteTax::class, 
+			'line_item_taxes' => Models\CreditNoteLineItemTax::class, 
+			'linked_refunds' => Models\CreditNoteLinkedRefund::class, 
+			'allocations' => Models\CreditNoteAllocation::class, 
+			'shipping_address' => Models\CreditNoteShippingAddress::class, 
+			'billing_address' => Models\CreditNoteBillingAddress::class
+		));
+        return $credit_notes;
+    }
+    
+    public function advanceInvoiceSchedules() 
+    {
+        $advance_invoice_schedules = $this->_getList('advance_invoice_schedules', Models\AdvanceInvoiceSchedule::class,
+        array( 
+			'fixed_interval_schedule' => Models\AdvanceInvoiceScheduleFixedIntervalSchedule::class, 
+			'specific_dates_schedule' => Models\AdvanceInvoiceScheduleSpecificDatesSchedule::class
+		));
+        return $advance_invoice_schedules;
+    }
+    
+    public function hierarchies() 
+    {
+        $hierarchies = $this->_getList('hierarchies', Models\Hierarchy::class,
+        array( 
+			
+		));
+        return $hierarchies;
+    }
+    
+    public function downloads() 
+    {
+        $downloads = $this->_getList('downloads', Models\Download::class,
+        array( 
+			
+		));
+        return $downloads;
+    }
+    
+    public function invoices() 
+    {
+        $invoices = $this->_getList('invoices', Models\Invoice::class,
+        array( 
+			'line_items' => Models\InvoiceLineItem::class, 
+			'discounts' => Models\InvoiceDiscount::class, 
+			'line_item_discounts' => Models\InvoiceLineItemDiscount::class, 
+			'taxes' => Models\InvoiceTax::class, 
+			'line_item_taxes' => Models\InvoiceLineItemTax::class, 
+			'line_item_tiers' => Models\InvoiceLineItemTier::class, 
+			'linked_payments' => Models\InvoiceLinkedPayment::class, 
+			'dunning_attempts' => Models\InvoiceDunningAttempt::class, 
+			'applied_credits' => Models\InvoiceAppliedCredit::class, 
+			'adjustment_credit_notes' => Models\InvoiceAdjustmentCreditNote::class, 
+			'issued_credit_notes' => Models\InvoiceIssuedCreditNote::class, 
+			'linked_orders' => Models\InvoiceLinkedOrder::class, 
+			'notes' => Models\InvoiceNote::class, 
+			'shipping_address' => Models\InvoiceShippingAddress::class, 
+			'statement_descriptor' => Models\InvoiceStatementDescriptor::class, 
+			'billing_address' => Models\InvoiceBillingAddress::class, 
+			'einvoice' => Models\InvoiceEinvoice::class
+		));
+        return $invoices;
+    }
+    
+    public function differentialPrices() 
+    {
+        $differential_prices = $this->_getList('differential_prices', Models\DifferentialPrice::class,
+        array( 
+			'tiers' => Models\DifferentialPriceTier::class, 
+			'parent_periods' => Models\DifferentialPriceParentPeriod::class
+		));
+        return $differential_prices;
+    }
+    
+    public function inAppSubscriptions() 
+    {
+        $in_app_subscriptions = $this->_getList('in_app_subscriptions', Models\InAppSubscription::class,
+        array( 
+			
+		));
+        return $in_app_subscriptions;
+    }
+    
+
+    public function toJson() 
+    {
+        return json_encode($this->_response);
+    } 
+    
+    private function _getList($type, $class, $subTypes = array(), $dependantTypes = array(),  $dependantSubTypes = array())
+    {
+        if(!array_key_exists($type, $this->_response))
+        {
+            return null;
+        }
+        if(!array_key_exists($type, $this->_responseObj))
+        {
+            $setVal = array();
+            foreach($this->_response[$type] as $stV)
+            {
+                $obj = new $class($stV, $subTypes, $dependantTypes);
+                foreach($dependantSubTypes as $k => $v)
+                {
+                    $obj->_initDependant($stV, $k, $v);
+                }
+                array_push($setVal, $obj);
+            }
+            $this->_responseObj[$type] = $setVal;
+        }
+        return $this->_responseObj[$type];        
+    }
+    
+    private function _get($type, $class, $subTypes = array(), $dependantTypes = array())
+    {
+        if(!array_key_exists($type, $this->_response))
+        {
+                return null;
+        }
+        if(!array_key_exists($type, $this->_responseObj))
+        {
+                $this->_responseObj[$type] = new $class($this->_response[$type], $subTypes, $dependantTypes);
+        }
+        return $this->_responseObj[$type];
+    }
+
+}
+
+?>

+ 10 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/chargebee/chargebee-php/lib/ChargeBee/Version.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace ChargeBee\ChargeBee;
+
+final class Version
+{
+	  const VERSION = '3.23.0';
+}
+
+?>

+ 222 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/chargebee/chargebee-php/lib/init.php

@@ -0,0 +1,222 @@
+<?php
+	
+	
+require(dirname(__FILE__) . '/AbstractChargeBee.php');
+require(dirname(__FILE__) . '/ChargeBee.php');
+require(dirname(__FILE__) . '/ChargeBee/Version.php');
+require(dirname(__FILE__) . '/ChargeBee/Environment.php');
+require(dirname(__FILE__) . '/ChargeBee/Util.php');
+
+require(dirname(__FILE__) . '/ChargeBee/Exceptions/IOException.php');
+require(dirname(__FILE__) . '/ChargeBee/Exceptions/APIError.php');
+require(dirname(__FILE__) . '/ChargeBee/Exceptions/PaymentException.php');
+require(dirname(__FILE__) . '/ChargeBee/Exceptions/OperationFailedException.php');
+require(dirname(__FILE__) . '/ChargeBee/Exceptions/InvalidRequestException.php');
+
+require(dirname(__FILE__) . '/ChargeBee/Request.php');
+require(dirname(__FILE__) . '/ChargeBee/Guzzle.php');
+
+require(dirname(__FILE__) . '/ChargeBee/Result.php');
+require(dirname(__FILE__) . '/ChargeBee/ListResult.php');
+
+require(dirname(__FILE__) . '/ChargeBee/Model.php');
+
+require(dirname(__FILE__) . '/ChargeBee/Models/Addon.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/AddonTier.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Address.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/AdvanceInvoiceSchedule.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/AdvanceInvoiceScheduleFixedIntervalSchedule.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/AdvanceInvoiceScheduleSpecificDatesSchedule.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/AttachedItem.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Card.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Comment.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Contact.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/ContractTerm.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Coupon.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CouponCode.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CouponCouponConstraint.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CouponItemConstraint.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CouponItemConstraintCriteria.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CouponSet.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNote.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteAllocation.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteBillingAddress.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteDiscount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteEinvoice.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteEstimate.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteEstimateDiscount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteEstimateLineItem.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteEstimateLineItemDiscount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteEstimateLineItemTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteEstimateLineItemTier.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteEstimateTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteLineItem.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteLineItemDiscount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteLineItemTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteLineItemTier.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteLinkedRefund.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteShippingAddress.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CreditNoteTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Customer.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CustomerBalance.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CustomerBillingAddress.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CustomerChildAccountAccess.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CustomerContact.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CustomerEntityIdentifier.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CustomerParentAccountAccess.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CustomerPaymentMethod.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CustomerReferralUrl.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/CustomerRelationship.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/DifferentialPrice.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/DifferentialPriceParentPeriod.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/DifferentialPriceTier.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Discount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Download.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/EntitlementOverride.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Estimate.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Event.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/EventWebhook.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Export.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/ExportDownload.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Feature.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/FeatureLevel.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Gift.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/GiftGiftReceiver.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/GiftGiftTimeline.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/GiftGifter.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Hierarchy.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/HostedPage.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/ImpactedItem.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/ImpactedItemDownload.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/ImpactedSubscription.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/ImpactedSubscriptionDownload.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InAppSubscription.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Invoice.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceAdjustmentCreditNote.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceAppliedCredit.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceBillingAddress.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceDiscount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceDunningAttempt.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceEinvoice.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceEstimate.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceEstimateDiscount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceEstimateLineItem.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceEstimateLineItemDiscount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceEstimateLineItemTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceEstimateLineItemTier.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceEstimateTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceIssuedCreditNote.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceLineItem.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceLineItemDiscount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceLineItemTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceLineItemTier.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceLinkedOrder.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceLinkedPayment.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceNote.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceShippingAddress.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceStatementDescriptor.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/InvoiceTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Item.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/ItemApplicableItem.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/ItemEntitlement.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/ItemFamily.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/ItemPrice.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/ItemPriceAccountingDetail.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/ItemPriceTaxDetail.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/ItemPriceTier.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/NonSubscription.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Order.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/OrderBillingAddress.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/OrderLineItemDiscount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/OrderLineItemTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/OrderLinkedCreditNote.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/OrderOrderLineItem.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/OrderResentOrder.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/OrderShippingAddress.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PaymentIntent.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PaymentIntentPaymentAttempt.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PaymentReferenceNumber.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PaymentSource.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PaymentSourceAmazonPayment.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PaymentSourceBankAccount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PaymentSourceBillingAddress.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PaymentSourceCard.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PaymentSourceCustVoucherSource.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PaymentSourceMandate.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PaymentSourcePaypal.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PaymentSourceUpi.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PaymentSourceVenmo.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PaymentVoucher.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PaymentVoucherLinkedInvoice.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Plan.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PlanApplicableAddon.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PlanAttachedAddon.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PlanEventBasedAddon.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PlanTier.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PortalSession.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PortalSessionLinkedCustomer.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/PromotionalCredit.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Purchase.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Quote.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuoteBillingAddress.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuoteDiscount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuoteLineGroup.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuoteLineGroupDiscount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuoteLineGroupLineItem.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuoteLineGroupLineItemDiscount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuoteLineGroupLineItemTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuoteLineGroupTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuoteLineItem.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuoteLineItemDiscount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuoteLineItemTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuoteLineItemTier.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuoteShippingAddress.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuoteTax.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuotedCharge.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuotedChargeAddon.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuotedChargeCharge.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuotedChargeCoupon.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuotedChargeInvoiceItem.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuotedChargeItemTier.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuotedSubscription.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuotedSubscriptionAddon.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuotedSubscriptionCoupon.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuotedSubscriptionEventBasedAddon.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuotedSubscriptionItemTier.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuotedSubscriptionQuotedContractTerm.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/QuotedSubscriptionSubscriptionItem.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/ResourceMigration.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SiteMigrationDetail.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Subscription.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionAddon.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionChargedEventBasedAddon.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionChargedItem.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionContractTerm.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionCoupon.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionDiscount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionEntitlement.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionEntitlementComponent.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionEstimate.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionEstimateContractTerm.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionEstimateShippingAddress.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionEventBasedAddon.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionItemTier.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionReferralInfo.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionShippingAddress.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/SubscriptionSubscriptionItem.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/TaxWithheld.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/ThirdPartyPaymentMethod.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/TimeMachine.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Token.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Transaction.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/TransactionLinkedCreditNote.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/TransactionLinkedInvoice.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/TransactionLinkedPayment.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/TransactionLinkedRefund.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/UnbilledCharge.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/UnbilledChargeTier.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Usage.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/VirtualBankAccount.php');
+require(dirname(__FILE__) . '/ChargeBee/Models/Content.php');
+
+?>

+ 75 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/composer/autoload_real.php

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

+ 226 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/composer/autoload_static.php

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

+ 2219 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/composer/installed.json

@@ -0,0 +1,2219 @@
+{
+    "packages": [
+        {
+            "name": "achingbrain/php5-akismet",
+            "version": "0.5",
+            "version_normalized": "0.5.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/achingbrain/php5-akismet",
+                "reference": "master"
+            },
+            "type": "library",
+            "installation-source": "source",
+            "install-path": "../achingbrain/php5-akismet"
+        },
+        {
+            "name": "akeeba/s3",
+            "version": "dev-development",
+            "version_normalized": "dev-development",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/akeeba/s3.git",
+                "reference": "982d78d9f35c15bbfeff42a5f2a6e6ac4fd60f48"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/akeeba/s3/zipball/982d78d9f35c15bbfeff42a5f2a6e6ac4fd60f48",
+                "reference": "982d78d9f35c15bbfeff42a5f2a6e6ac4fd60f48",
+                "shasum": ""
+            },
+            "require": {
+                "ext-curl": "*",
+                "ext-simplexml": "*",
+                "php": ">=7.1.0 <8.4"
+            },
+            "time": "2023-10-30T15:02:15+00:00",
+            "default-branch": true,
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "files": [
+                    "src/aliasing.php"
+                ],
+                "psr-4": {
+                    "Akeeba\\S3\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "GPL-3.0-or-later"
+            ],
+            "authors": [
+                {
+                    "name": "Nicholas K. Dionysopoulos",
+                    "email": "nicholas_NO_SPAM_PLEASE@akeeba.com",
+                    "homepage": "http://www.dionysopoulos.me",
+                    "role": "Lead Developer"
+                }
+            ],
+            "description": "A compact, dependency-less Amazon S3 API client implementing the most commonly used features",
+            "homepage": "https://github.com/akeeba/s3",
+            "keywords": [
+                "s3"
+            ],
+            "support": {
+                "issues": "https://github.com/akeeba/s3/issues",
+                "source": "https://github.com/akeeba/s3/tree/development"
+            },
+            "install-path": "../akeeba/s3"
+        },
+        {
+            "name": "avto-dev/composer-cleanup-plugin",
+            "version": "dev-master",
+            "version_normalized": "dev-master",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/unaio/composer-cleanup-plugin.git",
+                "reference": "75187ca4854265174021765e3b67cb973dc37723"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/unaio/composer-cleanup-plugin/zipball/75187ca4854265174021765e3b67cb973dc37723",
+                "reference": "75187ca4854265174021765e3b67cb973dc37723",
+                "shasum": ""
+            },
+            "require": {
+                "composer-plugin-api": "^1.0 || ^2.0",
+                "php": "^7.2 || ^8.0"
+            },
+            "require-dev": {
+                "composer/composer": "^1.8.6 || ^2.0",
+                "phpstan/phpstan": "~0.12.34",
+                "phpunit/phpunit": "^8.5.4 || ^9.3"
+            },
+            "time": "2021-09-04T13:12:22+00:00",
+            "default-branch": true,
+            "type": "composer-plugin",
+            "extra": {
+                "class": "AvtoDev\\Composer\\Cleanup\\Plugin"
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "AvtoDev\\Composer\\Cleanup\\": "src/"
+                }
+            },
+            "autoload-dev": {
+                "psr-4": {
+                    "AvtoDev\\Composer\\Cleanup\\Tests\\": "tests/"
+                }
+            },
+            "scripts": {
+                "post-update-cmd": [
+                    "AvtoDev\\Composer\\Cleanup\\Plugin::cleanupAllPackages"
+                ],
+                "post-install-cmd": [
+                    "AvtoDev\\Composer\\Cleanup\\Plugin::cleanupAllPackages"
+                ],
+                "phpunit": [
+                    "@php ./vendor/bin/phpunit --no-coverage --colors=always"
+                ],
+                "phpunit-cover": [
+                    "@php ./vendor/bin/phpunit --coverage-html='./coverage/html'"
+                ],
+                "phpstan": [
+                    "@php ./vendor/bin/phpstan analyze -c ./phpstan.neon.dist --no-progress --ansi"
+                ],
+                "test": [
+                    "@phpstan",
+                    "@phpunit"
+                ],
+                "test-cover": [
+                    "@phpstan",
+                    "@phpunit-cover"
+                ]
+            },
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "avto-dev",
+                    "homepage": "https://github.com/avto-dev"
+                },
+                {
+                    "name": "Barry vd. Heuvel",
+                    "email": "barryvdh@gmail.com"
+                }
+            ],
+            "description": "A composer cleanup plugin, to remove tests and documentation to save space",
+            "support": {
+                "issues": "https://github.com/avto-dev/composer-cleanup-plugin/issues",
+                "source": "https://github.com/avto-dev/composer-cleanup-plugin"
+            },
+            "install-path": "../avto-dev/composer-cleanup-plugin"
+        },
+        {
+            "name": "bshaffer/oauth2-server-php",
+            "version": "v1.14.1",
+            "version_normalized": "1.14.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/bshaffer/oauth2-server-php.git",
+                "reference": "096db2c86a7d67a2ba45e72be7d208c342694542"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/bshaffer/oauth2-server-php/zipball/096db2c86a7d67a2ba45e72be7d208c342694542",
+                "reference": "096db2c86a7d67a2ba45e72be7d208c342694542",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2"
+            },
+            "require-dev": {
+                "aws/aws-sdk-php": "^2.8",
+                "firebase/php-jwt": "^6.4",
+                "phpunit/phpunit": "^7.5||^8.0",
+                "predis/predis": "^1.1",
+                "thobbs/phpcassa": "dev-master",
+                "yoast/phpunit-polyfills": "^1.0"
+            },
+            "suggest": {
+                "aws/aws-sdk-php": "~2.8 is required to use DynamoDB storage",
+                "firebase/php-jwt": "~v6.4 is required to use JWT features",
+                "mongodb/mongodb": "^1.1 is required to use MongoDB storage",
+                "predis/predis": "Required to use Redis storage",
+                "thobbs/phpcassa": "Required to use Cassandra storage"
+            },
+            "time": "2023-07-07T17:53:54+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-0": {
+                    "OAuth2": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Brent Shaffer",
+                    "email": "bshafs@gmail.com",
+                    "homepage": "http://brentertainment.com"
+                }
+            ],
+            "description": "OAuth2 Server for PHP",
+            "homepage": "http://github.com/bshaffer/oauth2-server-php",
+            "keywords": [
+                "auth",
+                "oauth",
+                "oauth2"
+            ],
+            "support": {
+                "issues": "https://github.com/bshaffer/oauth2-server-php/issues",
+                "source": "https://github.com/bshaffer/oauth2-server-php/tree/v1.14.1"
+            },
+            "install-path": "../bshaffer/oauth2-server-php"
+        },
+        {
+            "name": "chargebee/chargebee-php",
+            "version": "v3.23.0",
+            "version_normalized": "3.23.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/chargebee/chargebee-php.git",
+                "reference": "e821b81adf4b6334c0705f1e13471abbe41e94db"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/chargebee/chargebee-php/zipball/e821b81adf4b6334c0705f1e13471abbe41e94db",
+                "reference": "e821b81adf4b6334c0705f1e13471abbe41e94db",
+                "shasum": ""
+            },
+            "require": {
+                "ext-curl": "*",
+                "guzzlehttp/guzzle": ">=6.5",
+                "php": ">=5.6.0"
+            },
+            "require-dev": {
+                "simpletest/simpletest": "^1.1"
+            },
+            "time": "2023-10-31T09:19:32+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-0": {
+                    "ChargeBee": "lib/"
+                },
+                "psr-4": {
+                    "ChargeBee\\": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "ChargeBee API client implementation for PHP",
+            "homepage": "https://github.com/chargebee/chargebee-php",
+            "keywords": [
+                "chargebee",
+                "payments",
+                "recurring billing",
+                "subscription billing"
+            ],
+            "support": {
+                "issues": "https://github.com/chargebee/chargebee-php/issues",
+                "source": "https://github.com/chargebee/chargebee-php/tree/v3.23.0"
+            },
+            "install-path": "../chargebee/chargebee-php"
+        },
+        {
+            "name": "curl/cacert",
+            "version": "0.0.1",
+            "version_normalized": "0.0.1.0",
+            "dist": {
+                "type": "file",
+                "url": "https://curl.se/ca/cacert.pem"
+            },
+            "type": "library",
+            "installation-source": "dist",
+            "install-path": "../curl/cacert"
+        },
+        {
+            "name": "erusev/parsedown",
+            "version": "dev-master",
+            "version_normalized": "dev-master",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/erusev/parsedown.git",
+                "reference": "77947eda2fdaf06b181c63a7db13e38968306aee"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/erusev/parsedown/zipball/77947eda2fdaf06b181c63a7db13e38968306aee",
+                "reference": "77947eda2fdaf06b181c63a7db13e38968306aee",
+                "shasum": ""
+            },
+            "require": {
+                "ext-mbstring": "*",
+                "php": ">=5.3.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.8.35"
+            },
+            "time": "2023-10-13T08:10:28+00:00",
+            "default-branch": true,
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-0": {
+                    "Parsedown": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Emanuil Rusev",
+                    "email": "hello@erusev.com",
+                    "homepage": "http://erusev.com"
+                }
+            ],
+            "description": "Parser for Markdown.",
+            "homepage": "http://parsedown.org",
+            "keywords": [
+                "markdown",
+                "parser"
+            ],
+            "support": {
+                "issues": "https://github.com/erusev/parsedown/issues",
+                "source": "https://github.com/erusev/parsedown/tree/master"
+            },
+            "install-path": "../erusev/parsedown"
+        },
+        {
+            "name": "ezyang/htmlpurifier",
+            "version": "v4.16.0",
+            "version_normalized": "4.16.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ezyang/htmlpurifier.git",
+                "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/523407fb06eb9e5f3d59889b3978d5bfe94299c8",
+                "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8",
+                "shasum": ""
+            },
+            "require": {
+                "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0"
+            },
+            "require-dev": {
+                "cerdic/css-tidy": "^1.7 || ^2.0",
+                "simpletest/simpletest": "dev-master"
+            },
+            "suggest": {
+                "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
+                "ext-bcmath": "Used for unit conversion and imagecrash protection",
+                "ext-iconv": "Converts text to and from non-UTF-8 encodings",
+                "ext-tidy": "Used for pretty-printing HTML"
+            },
+            "time": "2022-09-18T07:06:19+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "files": [
+                    "library/HTMLPurifier.composer.php"
+                ],
+                "psr-0": {
+                    "HTMLPurifier": "library/"
+                },
+                "exclude-from-classmap": [
+                    "/library/HTMLPurifier/Language/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-2.1-or-later"
+            ],
+            "authors": [
+                {
+                    "name": "Edward Z. Yang",
+                    "email": "admin@htmlpurifier.org",
+                    "homepage": "http://ezyang.com"
+                }
+            ],
+            "description": "Standards compliant HTML filter written in PHP",
+            "homepage": "http://htmlpurifier.org/",
+            "keywords": [
+                "html"
+            ],
+            "support": {
+                "issues": "https://github.com/ezyang/htmlpurifier/issues",
+                "source": "https://github.com/ezyang/htmlpurifier/tree/v4.16.0"
+            },
+            "install-path": "../ezyang/htmlpurifier"
+        },
+        {
+            "name": "ffmpeg/ffmpeg",
+            "version": "4.4.0",
+            "version_normalized": "4.4.0.0",
+            "dist": {
+                "type": "file",
+                "url": "https://github.com/eugeneware/ffmpeg-static/releases/download/b4.4/linux-x64"
+            },
+            "type": "library",
+            "installation-source": "dist",
+            "install-path": "../ffmpeg/ffmpeg"
+        },
+        {
+            "name": "guzzlehttp/guzzle",
+            "version": "7.8.0",
+            "version_normalized": "7.8.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/guzzle.git",
+                "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/1110f66a6530a40fe7aea0378fe608ee2b2248f9",
+                "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "guzzlehttp/promises": "^1.5.3 || ^2.0.1",
+                "guzzlehttp/psr7": "^1.9.1 || ^2.5.1",
+                "php": "^7.2.5 || ^8.0",
+                "psr/http-client": "^1.0",
+                "symfony/deprecation-contracts": "^2.2 || ^3.0"
+            },
+            "provide": {
+                "psr/http-client-implementation": "1.0"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.8.1",
+                "ext-curl": "*",
+                "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999",
+                "php-http/message-factory": "^1.1",
+                "phpunit/phpunit": "^8.5.29 || ^9.5.23",
+                "psr/log": "^1.1 || ^2.0 || ^3.0"
+            },
+            "suggest": {
+                "ext-curl": "Required for CURL handler support",
+                "ext-intl": "Required for Internationalized Domain Name (IDN) support",
+                "psr/log": "Required for using the Log middleware"
+            },
+            "time": "2023-08-27T10:20:53+00:00",
+            "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": false
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "files": [
+                    "src/functions_include.php"
+                ],
+                "psr-4": {
+                    "GuzzleHttp\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Jeremy Lindblom",
+                    "email": "jeremeamia@gmail.com",
+                    "homepage": "https://github.com/jeremeamia"
+                },
+                {
+                    "name": "George Mponos",
+                    "email": "gmponos@gmail.com",
+                    "homepage": "https://github.com/gmponos"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Márk Sági-Kazár",
+                    "email": "mark.sagikazar@gmail.com",
+                    "homepage": "https://github.com/sagikazarmark"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                }
+            ],
+            "description": "Guzzle is a PHP HTTP client library",
+            "keywords": [
+                "client",
+                "curl",
+                "framework",
+                "http",
+                "http client",
+                "psr-18",
+                "psr-7",
+                "rest",
+                "web service"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/guzzle/issues",
+                "source": "https://github.com/guzzle/guzzle/tree/7.8.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/Nyholm",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../guzzlehttp/guzzle"
+        },
+        {
+            "name": "guzzlehttp/promises",
+            "version": "2.0.1",
+            "version_normalized": "2.0.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/promises.git",
+                "reference": "111166291a0f8130081195ac4556a5587d7f1b5d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/promises/zipball/111166291a0f8130081195ac4556a5587d7f1b5d",
+                "reference": "111166291a0f8130081195ac4556a5587d7f1b5d",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5 || ^8.0"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.8.1",
+                "phpunit/phpunit": "^8.5.29 || ^9.5.23"
+            },
+            "time": "2023-08-03T15:11:55+00:00",
+            "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": false
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\Promise\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                }
+            ],
+            "description": "Guzzle promises library",
+            "keywords": [
+                "promise"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/promises/issues",
+                "source": "https://github.com/guzzle/promises/tree/2.0.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/Nyholm",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../guzzlehttp/promises"
+        },
+        {
+            "name": "guzzlehttp/psr7",
+            "version": "2.6.1",
+            "version_normalized": "2.6.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/psr7.git",
+                "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/psr7/zipball/be45764272e8873c72dbe3d2edcfdfcc3bc9f727",
+                "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5 || ^8.0",
+                "psr/http-factory": "^1.0",
+                "psr/http-message": "^1.1 || ^2.0",
+                "ralouphie/getallheaders": "^3.0"
+            },
+            "provide": {
+                "psr/http-factory-implementation": "1.0",
+                "psr/http-message-implementation": "1.0"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.8.1",
+                "http-interop/http-factory-tests": "^0.9",
+                "phpunit/phpunit": "^8.5.29 || ^9.5.23"
+            },
+            "suggest": {
+                "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+            },
+            "time": "2023-08-27T10:13:57+00:00",
+            "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": false
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\Psr7\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "George Mponos",
+                    "email": "gmponos@gmail.com",
+                    "homepage": "https://github.com/gmponos"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Márk Sági-Kazár",
+                    "email": "mark.sagikazar@gmail.com",
+                    "homepage": "https://github.com/sagikazarmark"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                },
+                {
+                    "name": "Márk Sági-Kazár",
+                    "email": "mark.sagikazar@gmail.com",
+                    "homepage": "https://sagikazarmark.hu"
+                }
+            ],
+            "description": "PSR-7 message implementation that also provides common utility methods",
+            "keywords": [
+                "http",
+                "message",
+                "psr-7",
+                "request",
+                "response",
+                "stream",
+                "uri",
+                "url"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/psr7/issues",
+                "source": "https://github.com/guzzle/psr7/tree/2.6.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/Nyholm",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../guzzlehttp/psr7"
+        },
+        {
+            "name": "intervention/image",
+            "version": "2.7.2",
+            "version_normalized": "2.7.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Intervention/image.git",
+                "reference": "04be355f8d6734c826045d02a1079ad658322dad"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Intervention/image/zipball/04be355f8d6734c826045d02a1079ad658322dad",
+                "reference": "04be355f8d6734c826045d02a1079ad658322dad",
+                "shasum": ""
+            },
+            "require": {
+                "ext-fileinfo": "*",
+                "guzzlehttp/psr7": "~1.1 || ^2.0",
+                "php": ">=5.4.0"
+            },
+            "require-dev": {
+                "mockery/mockery": "~0.9.2",
+                "phpunit/phpunit": "^4.8 || ^5.7 || ^7.5.15"
+            },
+            "suggest": {
+                "ext-gd": "to use GD library based image processing.",
+                "ext-imagick": "to use Imagick based image processing.",
+                "intervention/imagecache": "Caching extension for the Intervention Image library"
+            },
+            "time": "2022-05-21T17:30:32+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.4-dev"
+                },
+                "laravel": {
+                    "providers": [
+                        "Intervention\\Image\\ImageServiceProvider"
+                    ],
+                    "aliases": {
+                        "Image": "Intervention\\Image\\Facades\\Image"
+                    }
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Intervention\\Image\\": "src/Intervention/Image"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Oliver Vogel",
+                    "email": "oliver@intervention.io",
+                    "homepage": "https://intervention.io/"
+                }
+            ],
+            "description": "Image handling and manipulation library with support for Laravel integration",
+            "homepage": "http://image.intervention.io/",
+            "keywords": [
+                "gd",
+                "image",
+                "imagick",
+                "laravel",
+                "thumbnail",
+                "watermark"
+            ],
+            "support": {
+                "issues": "https://github.com/Intervention/image/issues",
+                "source": "https://github.com/Intervention/image/tree/2.7.2"
+            },
+            "funding": [
+                {
+                    "url": "https://paypal.me/interventionio",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/Intervention",
+                    "type": "github"
+                }
+            ],
+            "install-path": "../intervention/image"
+        },
+        {
+            "name": "lusitanian/oauth",
+            "version": "v0.8.11",
+            "version_normalized": "0.8.11.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Lusitanian/PHPoAuthLib.git",
+                "reference": "fc11a53db4b66da555a6a11fce294f574a8374f9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Lusitanian/PHPoAuthLib/zipball/fc11a53db4b66da555a6a11fce294f574a8374f9",
+                "reference": "fc11a53db4b66da555a6a11fce294f574a8374f9",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "3.7.*",
+                "predis/predis": "0.8.*@dev",
+                "squizlabs/php_codesniffer": "2.*",
+                "symfony/http-foundation": "~2.1"
+            },
+            "suggest": {
+                "ext-openssl": "Allows for usage of secure connections with the stream-based HTTP client.",
+                "predis/predis": "Allows using the Redis storage backend.",
+                "symfony/http-foundation": "Allows using the Symfony Session storage backend."
+            },
+            "time": "2018-02-14T22:37:14+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "0.1-dev"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-0": {
+                    "OAuth": "src",
+                    "OAuth\\Unit": "tests"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "David Desberg",
+                    "email": "david@daviddesberg.com"
+                },
+                {
+                    "name": "Elliot Chance",
+                    "email": "elliotchance@gmail.com"
+                },
+                {
+                    "name": "Pieter Hordijk",
+                    "email": "info@pieterhordijk.com"
+                }
+            ],
+            "description": "PHP 5.3+ oAuth 1/2 Library",
+            "keywords": [
+                "Authentication",
+                "authorization",
+                "oauth",
+                "security"
+            ],
+            "support": {
+                "issues": "https://github.com/Lusitanian/PHPoAuthLib/issues",
+                "source": "https://github.com/Lusitanian/PHPoAuthLib/tree/master"
+            },
+            "install-path": "../lusitanian/oauth"
+        },
+        {
+            "name": "matthiasmullie/minify",
+            "version": "1.3.71",
+            "version_normalized": "1.3.71.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/matthiasmullie/minify.git",
+                "reference": "ae42a47d7fecc1fbb7277b2f2d84c37a33edc3b1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/matthiasmullie/minify/zipball/ae42a47d7fecc1fbb7277b2f2d84c37a33edc3b1",
+                "reference": "ae42a47d7fecc1fbb7277b2f2d84c37a33edc3b1",
+                "shasum": ""
+            },
+            "require": {
+                "ext-pcre": "*",
+                "matthiasmullie/path-converter": "~1.1",
+                "php": ">=5.3.0"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": ">=2.0",
+                "matthiasmullie/scrapbook": ">=1.3",
+                "phpunit/phpunit": ">=4.8",
+                "squizlabs/php_codesniffer": ">=3.0"
+            },
+            "suggest": {
+                "psr/cache-implementation": "Cache implementation to use with Minify::cache"
+            },
+            "time": "2023-04-25T20:33:03+00:00",
+            "bin": [
+                "bin/minifycss",
+                "bin/minifyjs"
+            ],
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "MatthiasMullie\\Minify\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Matthias Mullie",
+                    "email": "minify@mullie.eu",
+                    "homepage": "https://www.mullie.eu",
+                    "role": "Developer"
+                }
+            ],
+            "description": "CSS & JavaScript minifier, in PHP. Removes whitespace, strips comments, combines files (incl. @import statements and small assets in CSS files), and optimizes/shortens a few common programming patterns.",
+            "homepage": "https://github.com/matthiasmullie/minify",
+            "keywords": [
+                "JS",
+                "css",
+                "javascript",
+                "minifier",
+                "minify"
+            ],
+            "support": {
+                "issues": "https://github.com/matthiasmullie/minify/issues",
+                "source": "https://github.com/matthiasmullie/minify/tree/1.3.71"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/matthiasmullie",
+                    "type": "github"
+                }
+            ],
+            "install-path": "../matthiasmullie/minify"
+        },
+        {
+            "name": "matthiasmullie/path-converter",
+            "version": "1.1.3",
+            "version_normalized": "1.1.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/matthiasmullie/path-converter.git",
+                "reference": "e7d13b2c7e2f2268e1424aaed02085518afa02d9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/matthiasmullie/path-converter/zipball/e7d13b2c7e2f2268e1424aaed02085518afa02d9",
+                "reference": "e7d13b2c7e2f2268e1424aaed02085518afa02d9",
+                "shasum": ""
+            },
+            "require": {
+                "ext-pcre": "*",
+                "php": ">=5.3.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.8"
+            },
+            "time": "2019-02-05T23:41:09+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "MatthiasMullie\\PathConverter\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Matthias Mullie",
+                    "email": "pathconverter@mullie.eu",
+                    "homepage": "http://www.mullie.eu",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Relative path converter",
+            "homepage": "http://github.com/matthiasmullie/path-converter",
+            "keywords": [
+                "converter",
+                "path",
+                "paths",
+                "relative"
+            ],
+            "support": {
+                "issues": "https://github.com/matthiasmullie/path-converter/issues",
+                "source": "https://github.com/matthiasmullie/path-converter/tree/1.1.3"
+            },
+            "install-path": "../matthiasmullie/path-converter"
+        },
+        {
+            "name": "mpratt/embera",
+            "version": "2.0.35",
+            "version_normalized": "2.0.35.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/mpratt/Embera.git",
+                "reference": "840035cbccd938ede69858cd5c872f3c5e97401e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/mpratt/Embera/zipball/840035cbccd938ede69858cd5c872f3c5e97401e",
+                "reference": "840035cbccd938ede69858cd5c872f3c5e97401e",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "php": ">=5.6"
+            },
+            "require-dev": {
+                "phpstan/phpstan": "^1.4",
+                "phpunit/phpunit": "^9.0"
+            },
+            "suggest": {
+                "ext-curl": "Fetch data using curl instead of using file_get_contents"
+            },
+            "time": "2023-11-09T06:38:50+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Embera\\": "src/Embera"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Pratt",
+                    "email": "yo@michael-pratt.com",
+                    "homepage": "http://www.michael-pratt.com",
+                    "role": "Author/Developer"
+                }
+            ],
+            "description": "Oembed consumer library. Converts urls into their html embed code. Supports 150+ sites, such as Youtube, Twitter, vimeo, Instagram etc.",
+            "homepage": "https://github.com/mpratt/Embera",
+            "keywords": [
+                "Auto embed",
+                "Embed Text",
+                "Responsive Embeds",
+                "Url Embed",
+                "embed",
+                "instagram",
+                "oembed",
+                "twitter",
+                "vimeo",
+                "vine",
+                "youtube"
+            ],
+            "support": {
+                "issues": "https://github.com/mpratt/Embera/issues",
+                "source": "https://github.com/mpratt/Embera/tree/2.0.35"
+            },
+            "funding": [
+                {
+                    "url": "https://paypal.me/mtpratt",
+                    "type": "paypal"
+                }
+            ],
+            "install-path": "../mpratt/embera"
+        },
+        {
+            "name": "paragonie/random_compat",
+            "version": "v9.99.100",
+            "version_normalized": "9.99.100.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/paragonie/random_compat.git",
+                "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
+                "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">= 7"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "4.*|5.*",
+                "vimeo/psalm": "^1"
+            },
+            "suggest": {
+                "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
+            },
+            "time": "2020-10-15T08:29:30+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Paragon Initiative Enterprises",
+                    "email": "security@paragonie.com",
+                    "homepage": "https://paragonie.com"
+                }
+            ],
+            "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
+            "keywords": [
+                "csprng",
+                "polyfill",
+                "pseudorandom",
+                "random"
+            ],
+            "support": {
+                "email": "info@paragonie.com",
+                "issues": "https://github.com/paragonie/random_compat/issues",
+                "source": "https://github.com/paragonie/random_compat"
+            },
+            "install-path": "../paragonie/random_compat"
+        },
+        {
+            "name": "paragonie/sodium_compat",
+            "version": "v1.20.0",
+            "version_normalized": "1.20.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/paragonie/sodium_compat.git",
+                "reference": "e592a3e06d1fa0d43988c7c7d9948ca836f644b6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/e592a3e06d1fa0d43988c7c7d9948ca836f644b6",
+                "reference": "e592a3e06d1fa0d43988c7c7d9948ca836f644b6",
+                "shasum": ""
+            },
+            "require": {
+                "paragonie/random_compat": ">=1",
+                "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7|^8"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^3|^4|^5|^6|^7|^8|^9"
+            },
+            "suggest": {
+                "ext-libsodium": "PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.",
+                "ext-sodium": "PHP >= 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security."
+            },
+            "time": "2023-04-30T00:54:53+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "files": [
+                    "autoload.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "ISC"
+            ],
+            "authors": [
+                {
+                    "name": "Paragon Initiative Enterprises",
+                    "email": "security@paragonie.com"
+                },
+                {
+                    "name": "Frank Denis",
+                    "email": "jedisct1@pureftpd.org"
+                }
+            ],
+            "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists",
+            "keywords": [
+                "Authentication",
+                "BLAKE2b",
+                "ChaCha20",
+                "ChaCha20-Poly1305",
+                "Chapoly",
+                "Curve25519",
+                "Ed25519",
+                "EdDSA",
+                "Edwards-curve Digital Signature Algorithm",
+                "Elliptic Curve Diffie-Hellman",
+                "Poly1305",
+                "Pure-PHP cryptography",
+                "RFC 7748",
+                "RFC 8032",
+                "Salpoly",
+                "Salsa20",
+                "X25519",
+                "XChaCha20-Poly1305",
+                "XSalsa20-Poly1305",
+                "Xchacha20",
+                "Xsalsa20",
+                "aead",
+                "cryptography",
+                "ecdh",
+                "elliptic curve",
+                "elliptic curve cryptography",
+                "encryption",
+                "libsodium",
+                "php",
+                "public-key cryptography",
+                "secret-key cryptography",
+                "side-channel resistant"
+            ],
+            "support": {
+                "issues": "https://github.com/paragonie/sodium_compat/issues",
+                "source": "https://github.com/paragonie/sodium_compat/tree/v1.20.0"
+            },
+            "install-path": "../paragonie/sodium_compat"
+        },
+        {
+            "name": "paypal/paypal-checkout-sdk",
+            "version": "1.0.2",
+            "version_normalized": "1.0.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/paypal/Checkout-PHP-SDK.git",
+                "reference": "19992ce7051ff9e47e643f28abb8cc1b3e5f1812"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/paypal/Checkout-PHP-SDK/zipball/19992ce7051ff9e47e643f28abb8cc1b3e5f1812",
+                "reference": "19992ce7051ff9e47e643f28abb8cc1b3e5f1812",
+                "shasum": ""
+            },
+            "require": {
+                "paypal/paypalhttp": "1.0.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^5.7"
+            },
+            "time": "2021-09-21T20:57:38+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Sample\\": "samples/",
+                    "PayPalCheckoutSdk\\": "lib/PayPalCheckoutSdk"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "https://github.com/paypal/Checkout-PHP-SDK/blob/master/LICENSE"
+            ],
+            "authors": [
+                {
+                    "name": "PayPal",
+                    "homepage": "https://github.com/paypal/Checkout-PHP-SDK/contributors"
+                }
+            ],
+            "description": "PayPal's PHP SDK for Checkout REST APIs",
+            "homepage": "http://github.com/paypal/Checkout-PHP-SDK/",
+            "keywords": [
+                "checkout",
+                "orders",
+                "payments",
+                "paypal",
+                "rest",
+                "sdk"
+            ],
+            "support": {
+                "source": "https://github.com/paypal/Checkout-PHP-SDK/tree/1.0.2"
+            },
+            "abandoned": true,
+            "install-path": "../paypal/paypal-checkout-sdk"
+        },
+        {
+            "name": "paypal/paypalhttp",
+            "version": "1.0.1",
+            "version_normalized": "1.0.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/paypal/paypalhttp_php.git",
+                "reference": "7b09c89c80828e842c79230e7f156b61fbb68d25"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/paypal/paypalhttp_php/zipball/7b09c89c80828e842c79230e7f156b61fbb68d25",
+                "reference": "7b09c89c80828e842c79230e7f156b61fbb68d25",
+                "shasum": ""
+            },
+            "require": {
+                "ext-curl": "*"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^5.7",
+                "wiremock-php/wiremock-php": "1.43.2"
+            },
+            "time": "2021-09-14T21:35:26+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "PayPalHttp\\": "lib/PayPalHttp"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PayPal",
+                    "homepage": "https://github.com/paypal/paypalhttp_php/contributors"
+                }
+            ],
+            "support": {
+                "issues": "https://github.com/paypal/paypalhttp_php/issues",
+                "source": "https://github.com/paypal/paypalhttp_php/tree/1.0.1"
+            },
+            "install-path": "../paypal/paypalhttp"
+        },
+        {
+            "name": "phpmailer/phpmailer",
+            "version": "v6.8.1",
+            "version_normalized": "6.8.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/PHPMailer/PHPMailer.git",
+                "reference": "e88da8d679acc3824ff231fdc553565b802ac016"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/e88da8d679acc3824ff231fdc553565b802ac016",
+                "reference": "e88da8d679acc3824ff231fdc553565b802ac016",
+                "shasum": ""
+            },
+            "require": {
+                "ext-ctype": "*",
+                "ext-filter": "*",
+                "ext-hash": "*",
+                "php": ">=5.5.0"
+            },
+            "require-dev": {
+                "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+                "doctrine/annotations": "^1.2.6 || ^1.13.3",
+                "php-parallel-lint/php-console-highlighter": "^1.0.0",
+                "php-parallel-lint/php-parallel-lint": "^1.3.2",
+                "phpcompatibility/php-compatibility": "^9.3.5",
+                "roave/security-advisories": "dev-latest",
+                "squizlabs/php_codesniffer": "^3.7.2",
+                "yoast/phpunit-polyfills": "^1.0.4"
+            },
+            "suggest": {
+                "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
+                "ext-openssl": "Needed for secure SMTP sending and DKIM signing",
+                "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
+                "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
+                "league/oauth2-google": "Needed for Google XOAUTH2 authentication",
+                "psr/log": "For optional PSR-3 debug logging",
+                "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
+                "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
+            },
+            "time": "2023-08-29T08:26:30+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "PHPMailer\\PHPMailer\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-2.1-only"
+            ],
+            "authors": [
+                {
+                    "name": "Marcus Bointon",
+                    "email": "phpmailer@synchromedia.co.uk"
+                },
+                {
+                    "name": "Jim Jagielski",
+                    "email": "jimjag@gmail.com"
+                },
+                {
+                    "name": "Andy Prevost",
+                    "email": "codeworxtech@users.sourceforge.net"
+                },
+                {
+                    "name": "Brent R. Matzelle"
+                }
+            ],
+            "description": "PHPMailer is a full-featured email creation and transfer class for PHP",
+            "support": {
+                "issues": "https://github.com/PHPMailer/PHPMailer/issues",
+                "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.8.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/Synchro",
+                    "type": "github"
+                }
+            ],
+            "install-path": "../phpmailer/phpmailer"
+        },
+        {
+            "name": "psr/http-client",
+            "version": "1.0.3",
+            "version_normalized": "1.0.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-client.git",
+                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+                "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.0 || ^8.0",
+                "psr/http-message": "^1.0 || ^2.0"
+            },
+            "time": "2023-09-23T14:17:50+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Client\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP clients",
+            "homepage": "https://github.com/php-fig/http-client",
+            "keywords": [
+                "http",
+                "http-client",
+                "psr",
+                "psr-18"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-client"
+            },
+            "install-path": "../psr/http-client"
+        },
+        {
+            "name": "psr/http-factory",
+            "version": "1.0.2",
+            "version_normalized": "1.0.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-factory.git",
+                "reference": "e616d01114759c4c489f93b099585439f795fe35"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35",
+                "reference": "e616d01114759c4c489f93b099585439f795fe35",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.0.0",
+                "psr/http-message": "^1.0 || ^2.0"
+            },
+            "time": "2023-04-10T20:10:41+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Message\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interfaces for PSR-7 HTTP message factories",
+            "keywords": [
+                "factory",
+                "http",
+                "message",
+                "psr",
+                "psr-17",
+                "psr-7",
+                "request",
+                "response"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-factory/tree/1.0.2"
+            },
+            "install-path": "../psr/http-factory"
+        },
+        {
+            "name": "psr/http-message",
+            "version": "2.0",
+            "version_normalized": "2.0.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-message.git",
+                "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+                "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2 || ^8.0"
+            },
+            "time": "2023-04-04T09:54:51+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0.x-dev"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Message\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP messages",
+            "homepage": "https://github.com/php-fig/http-message",
+            "keywords": [
+                "http",
+                "http-message",
+                "psr",
+                "psr-7",
+                "request",
+                "response"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-message/tree/2.0"
+            },
+            "install-path": "../psr/http-message"
+        },
+        {
+            "name": "psr/log",
+            "version": "1.1.4",
+            "version_normalized": "1.1.4.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/log.git",
+                "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
+                "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "time": "2021-05-03T11:20:27+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1.x-dev"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Log\\": "Psr/Log/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for logging libraries",
+            "homepage": "https://github.com/php-fig/log",
+            "keywords": [
+                "log",
+                "psr",
+                "psr-3"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/log/tree/1.1.4"
+            },
+            "install-path": "../psr/log"
+        },
+        {
+            "name": "pusher/pusher-php-server",
+            "version": "7.2.3",
+            "version_normalized": "7.2.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/pusher/pusher-http-php.git",
+                "reference": "416e68dd5f640175ad5982131c42a7a666d1d8e9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/416e68dd5f640175ad5982131c42a7a666d1d8e9",
+                "reference": "416e68dd5f640175ad5982131c42a7a666d1d8e9",
+                "shasum": ""
+            },
+            "require": {
+                "ext-curl": "*",
+                "ext-json": "*",
+                "guzzlehttp/guzzle": "^7.2",
+                "paragonie/sodium_compat": "^1.6",
+                "php": "^7.3|^8.0",
+                "psr/log": "^1.0|^2.0|^3.0"
+            },
+            "require-dev": {
+                "overtrue/phplint": "^2.3",
+                "phpunit/phpunit": "^9.3"
+            },
+            "time": "2023-05-17T16:00:06+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "5.0-dev"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Pusher\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "Library for interacting with the Pusher REST API",
+            "keywords": [
+                "events",
+                "messaging",
+                "php-pusher-server",
+                "publish",
+                "push",
+                "pusher",
+                "real time",
+                "real-time",
+                "realtime",
+                "rest",
+                "trigger"
+            ],
+            "support": {
+                "issues": "https://github.com/pusher/pusher-http-php/issues",
+                "source": "https://github.com/pusher/pusher-http-php/tree/7.2.3"
+            },
+            "install-path": "../pusher/pusher-php-server"
+        },
+        {
+            "name": "ralouphie/getallheaders",
+            "version": "3.0.3",
+            "version_normalized": "3.0.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ralouphie/getallheaders.git",
+                "reference": "120b605dfeb996808c31b6477290a714d356e822"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+                "reference": "120b605dfeb996808c31b6477290a714d356e822",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.6"
+            },
+            "require-dev": {
+                "php-coveralls/php-coveralls": "^2.1",
+                "phpunit/phpunit": "^5 || ^6.5"
+            },
+            "time": "2019-03-08T08:55:37+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "files": [
+                    "src/getallheaders.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ralph Khattar",
+                    "email": "ralph.khattar@gmail.com"
+                }
+            ],
+            "description": "A polyfill for getallheaders.",
+            "support": {
+                "issues": "https://github.com/ralouphie/getallheaders/issues",
+                "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+            },
+            "install-path": "../ralouphie/getallheaders"
+        },
+        {
+            "name": "snipe/banbuilder",
+            "version": "dev-master",
+            "version_normalized": "dev-master",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/snipe/banbuilder.git",
+                "reference": "621d6d857400983d0fd6cd291f105a835a317748"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/snipe/banbuilder/zipball/621d6d857400983d0fd6cd291f105a835a317748",
+                "reference": "621d6d857400983d0fd6cd291f105a835a317748",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.6.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^5.7.0"
+            },
+            "time": "2018-08-30T22:45:56+00:00",
+            "default-branch": true,
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Snipe\\BanBuilder\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "AGPL-3.0"
+            ],
+            "authors": [
+                {
+                    "name": "snipe",
+                    "email": "snipe@snipe.net",
+                    "role": "Creator & Developer"
+                }
+            ],
+            "description": "Library to easily handle profanity filtering in PHP projects.",
+            "homepage": "http://banbuilder.com",
+            "keywords": [
+                "filter",
+                "language",
+                "profanity",
+                "strings",
+                "swear"
+            ],
+            "support": {
+                "issues": "https://github.com/snipe/banbuilder/issues",
+                "source": "https://github.com/snipe/banbuilder/tree/2.3.0"
+            },
+            "install-path": "../snipe/banbuilder"
+        },
+        {
+            "name": "spatie/calendar-links",
+            "version": "1.8.0",
+            "version_normalized": "1.8.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/spatie/calendar-links.git",
+                "reference": "654fc0174e6279539b9bf0abdc3339e34c5286d7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/spatie/calendar-links/zipball/654fc0174e6279539b9bf0abdc3339e34c5286d7",
+                "reference": "654fc0174e6279539b9bf0abdc3339e34c5286d7",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.4 || ^8.0"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "^3.0",
+                "phpunit/phpunit": "^9.5",
+                "spatie/phpunit-snapshot-assertions": "^4.0",
+                "vimeo/psalm": "^4.20"
+            },
+            "time": "2022-08-20T12:59:17+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Spatie\\CalendarLinks\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian De Deyne",
+                    "email": "sebastian@spatie.be",
+                    "homepage": "https://spatie.be",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Generate add to calendar links for Google, iCal and other calendar systems",
+            "homepage": "https://github.com/spatie/calendar-links",
+            "keywords": [
+                "calendar-links",
+                "spatie"
+            ],
+            "support": {
+                "issues": "https://github.com/spatie/calendar-links/issues",
+                "source": "https://github.com/spatie/calendar-links/tree/1.8.0"
+            },
+            "funding": [
+                {
+                    "url": "https://spatie.be/open-source/support-us",
+                    "type": "custom"
+                }
+            ],
+            "install-path": "../spatie/calendar-links"
+        },
+        {
+            "name": "stripe/stripe-php",
+            "version": "v7.128.0",
+            "version_normalized": "7.128.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/stripe/stripe-php.git",
+                "reference": "c704949c49b72985c76cc61063aa26fefbd2724e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/stripe/stripe-php/zipball/c704949c49b72985c76cc61063aa26fefbd2724e",
+                "reference": "c704949c49b72985c76cc61063aa26fefbd2724e",
+                "shasum": ""
+            },
+            "require": {
+                "ext-curl": "*",
+                "ext-json": "*",
+                "ext-mbstring": "*",
+                "php": ">=5.6.0"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "3.5.0",
+                "phpstan/phpstan": "^1.2",
+                "phpunit/phpunit": "^5.7 || ^9.0",
+                "squizlabs/php_codesniffer": "^3.3"
+            },
+            "time": "2022-05-05T17:18:02+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Stripe\\": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Stripe and contributors",
+                    "homepage": "https://github.com/stripe/stripe-php/contributors"
+                }
+            ],
+            "description": "Stripe PHP Library",
+            "homepage": "https://stripe.com/",
+            "keywords": [
+                "api",
+                "payment processing",
+                "stripe"
+            ],
+            "support": {
+                "issues": "https://github.com/stripe/stripe-php/issues",
+                "source": "https://github.com/stripe/stripe-php/tree/v7.128.0"
+            },
+            "install-path": "../stripe/stripe-php"
+        },
+        {
+            "name": "symfony/deprecation-contracts",
+            "version": "v2.5.2",
+            "version_normalized": "2.5.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/deprecation-contracts.git",
+                "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
+                "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "time": "2022-01-02T09:53:40+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "2.5-dev"
+                },
+                "thanks": {
+                    "name": "symfony/contracts",
+                    "url": "https://github.com/symfony/contracts"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "files": [
+                    "function.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "A generic function and convention to trigger deprecation notices",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../symfony/deprecation-contracts"
+        },
+        {
+            "name": "tpyo/amazon-s3-php-class",
+            "version": "dev-master",
+            "version_normalized": "dev-master",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/unaio/amazon-s3-php-class.git",
+                "reference": "7c62b0e9b7d956a04e377ca236979a78dbac4e66"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/unaio/amazon-s3-php-class/zipball/7c62b0e9b7d956a04e377ca236979a78dbac4e66",
+                "reference": "7c62b0e9b7d956a04e377ca236979a78dbac4e66",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.2.0"
+            },
+            "time": "2022-06-30T03:20:34+00:00",
+            "default-branch": true,
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "classmap": [
+                    "S3.php"
+                ]
+            },
+            "license": [
+                "BSD-2-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Donovan Schönknecht",
+                    "email": "don@tpyo.net"
+                }
+            ],
+            "description": "A standalone Amazon S3 (REST) client for PHP 5.2.x using CURL that does not require PEAR.",
+            "homepage": "https://github.com/tpyo/amazon-s3-php-class",
+            "support": {
+                "source": "https://github.com/unaio/amazon-s3-php-class/tree/master"
+            },
+            "install-path": "../tpyo/amazon-s3-php-class"
+        },
+        {
+            "name": "twilio/sdk",
+            "version": "5.42.2",
+            "version_normalized": "5.42.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/twilio/twilio-php.git",
+                "reference": "0cfcb871b18a9c427dd9e8f0ed7458d43009b48a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/twilio/twilio-php/zipball/0cfcb871b18a9c427dd9e8f0ed7458d43009b48a",
+                "reference": "0cfcb871b18a9c427dd9e8f0ed7458d43009b48a",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.5.0"
+            },
+            "require-dev": {
+                "apigen/apigen": "^4.1",
+                "guzzlehttp/guzzle": "^6.3",
+                "phpunit/phpunit": ">=4.5"
+            },
+            "suggest": {
+                "guzzlehttp/guzzle": "An HTTP client to execute the API requests"
+            },
+            "time": "2020-02-05T19:55:13+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Twilio\\": "src/Twilio/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Twilio API Team",
+                    "email": "api@twilio.com"
+                }
+            ],
+            "description": "A PHP wrapper for Twilio's API",
+            "homepage": "http://github.com/twilio/twilio-php",
+            "keywords": [
+                "api",
+                "sms",
+                "twilio"
+            ],
+            "support": {
+                "issues": "https://github.com/twilio/twilio-php/issues",
+                "source": "https://github.com/twilio/twilio-php/tree/5.42.2"
+            },
+            "install-path": "../twilio/sdk"
+        },
+        {
+            "name": "unaio/amazon-s3-php-class-hmac-v2",
+            "version": "0.6",
+            "version_normalized": "0.6.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/unaio/amazon-s3-php-class-hmac-v2",
+                "reference": "3687dec0af7be2cb11f21828aa4b12d20c4c5935"
+            },
+            "type": "library",
+            "installation-source": "source",
+            "install-path": "../unaio/amazon-s3-php-class-hmac-v2"
+        },
+        {
+            "name": "wikimedia/less.php",
+            "version": "v4.1.1",
+            "version_normalized": "4.1.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/wikimedia/less.php.git",
+                "reference": "7c814ca156716e7b46ad84ac038bf298ff91cbb4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/wikimedia/less.php/zipball/7c814ca156716e7b46ad84ac038bf298ff91cbb4",
+                "reference": "7c814ca156716e7b46ad84ac038bf298ff91cbb4",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.4.3"
+            },
+            "require-dev": {
+                "mediawiki/mediawiki-codesniffer": "41.0.0",
+                "mediawiki/mediawiki-phan-config": "0.12.1",
+                "mediawiki/minus-x": "1.1.1",
+                "php-parallel-lint/php-console-highlighter": "1.0.0",
+                "php-parallel-lint/php-parallel-lint": "1.3.2",
+                "phpunit/phpunit": "9.5.28"
+            },
+            "time": "2023-08-01T14:54:27+00:00",
+            "bin": [
+                "bin/lessc"
+            ],
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-0": {
+                    "Less": "lib/"
+                },
+                "classmap": [
+                    "lessc.inc.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "Timo Tijhof",
+                    "homepage": "https://timotijhof.net"
+                },
+                {
+                    "name": "Josh Schmidt",
+                    "homepage": "https://github.com/oyejorge"
+                },
+                {
+                    "name": "Matt Agar",
+                    "homepage": "https://github.com/agar"
+                },
+                {
+                    "name": "Martin Jantošovič",
+                    "homepage": "https://github.com/Mordred"
+                }
+            ],
+            "description": "PHP port of the LESS processor",
+            "homepage": "https://gerrit.wikimedia.org/g/mediawiki/libs/less.php",
+            "keywords": [
+                "css",
+                "less",
+                "less.js",
+                "lesscss",
+                "php",
+                "stylesheet"
+            ],
+            "support": {
+                "issues": "https://github.com/wikimedia/less.php/issues",
+                "source": "https://github.com/wikimedia/less.php/tree/v4.1.1"
+            },
+            "install-path": "../wikimedia/less.php"
+        }
+    ],
+    "dev": true,
+    "dev-package-names": []
+}

+ 375 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/composer/installed.php

@@ -0,0 +1,375 @@
+<?php return array(
+    'root' => array(
+        'pretty_version' => 'dev-master',
+        'version' => 'dev-master',
+        'type' => 'library',
+        'install_path' => __DIR__ . '/../../',
+        'aliases' => array(),
+        'reference' => 'd42837e25c7c0a517403209a8a7d1b7f59645787',
+        'name' => 'unaio/una',
+        'dev' => true,
+    ),
+    'versions' => array(
+        'achingbrain/php5-akismet' => array(
+            'pretty_version' => '0.5',
+            'version' => '0.5.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../achingbrain/php5-akismet',
+            'aliases' => array(),
+            'reference' => 'master',
+            'dev_requirement' => false,
+        ),
+        'akeeba/s3' => array(
+            'pretty_version' => 'dev-development',
+            'version' => 'dev-development',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../akeeba/s3',
+            'aliases' => array(
+                0 => '9999999-dev',
+            ),
+            'reference' => '982d78d9f35c15bbfeff42a5f2a6e6ac4fd60f48',
+            'dev_requirement' => false,
+        ),
+        'avto-dev/composer-cleanup-plugin' => array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'type' => 'composer-plugin',
+            'install_path' => __DIR__ . '/../avto-dev/composer-cleanup-plugin',
+            'aliases' => array(
+                0 => '9999999-dev',
+            ),
+            'reference' => '75187ca4854265174021765e3b67cb973dc37723',
+            'dev_requirement' => false,
+        ),
+        'bshaffer/oauth2-server-php' => array(
+            'pretty_version' => 'v1.14.1',
+            'version' => '1.14.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../bshaffer/oauth2-server-php',
+            'aliases' => array(),
+            'reference' => '096db2c86a7d67a2ba45e72be7d208c342694542',
+            'dev_requirement' => false,
+        ),
+        'chargebee/chargebee-php' => array(
+            'pretty_version' => 'v3.23.0',
+            'version' => '3.23.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../chargebee/chargebee-php',
+            'aliases' => array(),
+            'reference' => 'e821b81adf4b6334c0705f1e13471abbe41e94db',
+            'dev_requirement' => false,
+        ),
+        'curl/cacert' => array(
+            'pretty_version' => '0.0.1',
+            'version' => '0.0.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../curl/cacert',
+            'aliases' => array(),
+            'reference' => NULL,
+            'dev_requirement' => false,
+        ),
+        'erusev/parsedown' => array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../erusev/parsedown',
+            'aliases' => array(
+                0 => '9999999-dev',
+            ),
+            'reference' => '77947eda2fdaf06b181c63a7db13e38968306aee',
+            'dev_requirement' => false,
+        ),
+        'ezyang/htmlpurifier' => array(
+            'pretty_version' => 'v4.16.0',
+            'version' => '4.16.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../ezyang/htmlpurifier',
+            'aliases' => array(),
+            'reference' => '523407fb06eb9e5f3d59889b3978d5bfe94299c8',
+            'dev_requirement' => false,
+        ),
+        'ffmpeg/ffmpeg' => array(
+            'pretty_version' => '4.4.0',
+            'version' => '4.4.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../ffmpeg/ffmpeg',
+            'aliases' => array(),
+            'reference' => NULL,
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/guzzle' => array(
+            'pretty_version' => '7.8.0',
+            'version' => '7.8.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/guzzle',
+            'aliases' => array(),
+            'reference' => '1110f66a6530a40fe7aea0378fe608ee2b2248f9',
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/promises' => array(
+            'pretty_version' => '2.0.1',
+            'version' => '2.0.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/promises',
+            'aliases' => array(),
+            'reference' => '111166291a0f8130081195ac4556a5587d7f1b5d',
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/psr7' => array(
+            'pretty_version' => '2.6.1',
+            'version' => '2.6.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/psr7',
+            'aliases' => array(),
+            'reference' => 'be45764272e8873c72dbe3d2edcfdfcc3bc9f727',
+            'dev_requirement' => false,
+        ),
+        'intervention/image' => array(
+            'pretty_version' => '2.7.2',
+            'version' => '2.7.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../intervention/image',
+            'aliases' => array(),
+            'reference' => '04be355f8d6734c826045d02a1079ad658322dad',
+            'dev_requirement' => false,
+        ),
+        'lusitanian/oauth' => array(
+            'pretty_version' => 'v0.8.11',
+            'version' => '0.8.11.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../lusitanian/oauth',
+            'aliases' => array(),
+            'reference' => 'fc11a53db4b66da555a6a11fce294f574a8374f9',
+            'dev_requirement' => false,
+        ),
+        'matthiasmullie/minify' => array(
+            'pretty_version' => '1.3.71',
+            'version' => '1.3.71.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../matthiasmullie/minify',
+            'aliases' => array(),
+            'reference' => 'ae42a47d7fecc1fbb7277b2f2d84c37a33edc3b1',
+            'dev_requirement' => false,
+        ),
+        'matthiasmullie/path-converter' => array(
+            'pretty_version' => '1.1.3',
+            'version' => '1.1.3.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../matthiasmullie/path-converter',
+            'aliases' => array(),
+            'reference' => 'e7d13b2c7e2f2268e1424aaed02085518afa02d9',
+            'dev_requirement' => false,
+        ),
+        'mpratt/embera' => array(
+            'pretty_version' => '2.0.35',
+            'version' => '2.0.35.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../mpratt/embera',
+            'aliases' => array(),
+            'reference' => '840035cbccd938ede69858cd5c872f3c5e97401e',
+            'dev_requirement' => false,
+        ),
+        'paragonie/random_compat' => array(
+            'pretty_version' => 'v9.99.100',
+            'version' => '9.99.100.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../paragonie/random_compat',
+            'aliases' => array(),
+            'reference' => '996434e5492cb4c3edcb9168db6fbb1359ef965a',
+            'dev_requirement' => false,
+        ),
+        'paragonie/sodium_compat' => array(
+            'pretty_version' => 'v1.20.0',
+            'version' => '1.20.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../paragonie/sodium_compat',
+            'aliases' => array(),
+            'reference' => 'e592a3e06d1fa0d43988c7c7d9948ca836f644b6',
+            'dev_requirement' => false,
+        ),
+        'paypal/paypal-checkout-sdk' => array(
+            'pretty_version' => '1.0.2',
+            'version' => '1.0.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../paypal/paypal-checkout-sdk',
+            'aliases' => array(),
+            'reference' => '19992ce7051ff9e47e643f28abb8cc1b3e5f1812',
+            'dev_requirement' => false,
+        ),
+        'paypal/paypalhttp' => array(
+            'pretty_version' => '1.0.1',
+            'version' => '1.0.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../paypal/paypalhttp',
+            'aliases' => array(),
+            'reference' => '7b09c89c80828e842c79230e7f156b61fbb68d25',
+            'dev_requirement' => false,
+        ),
+        'phpmailer/phpmailer' => array(
+            'pretty_version' => 'v6.8.1',
+            'version' => '6.8.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../phpmailer/phpmailer',
+            'aliases' => array(),
+            'reference' => 'e88da8d679acc3824ff231fdc553565b802ac016',
+            'dev_requirement' => false,
+        ),
+        'psr/http-client' => array(
+            'pretty_version' => '1.0.3',
+            'version' => '1.0.3.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-client',
+            'aliases' => array(),
+            'reference' => 'bb5906edc1c324c9a05aa0873d40117941e5fa90',
+            'dev_requirement' => false,
+        ),
+        'psr/http-client-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        'psr/http-factory' => array(
+            'pretty_version' => '1.0.2',
+            'version' => '1.0.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-factory',
+            'aliases' => array(),
+            'reference' => 'e616d01114759c4c489f93b099585439f795fe35',
+            'dev_requirement' => false,
+        ),
+        'psr/http-factory-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        'psr/http-message' => array(
+            'pretty_version' => '2.0',
+            'version' => '2.0.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-message',
+            'aliases' => array(),
+            'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71',
+            'dev_requirement' => false,
+        ),
+        'psr/http-message-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        'psr/log' => array(
+            'pretty_version' => '1.1.4',
+            'version' => '1.1.4.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/log',
+            'aliases' => array(),
+            'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11',
+            'dev_requirement' => false,
+        ),
+        'pusher/pusher-php-server' => array(
+            'pretty_version' => '7.2.3',
+            'version' => '7.2.3.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../pusher/pusher-php-server',
+            'aliases' => array(),
+            'reference' => '416e68dd5f640175ad5982131c42a7a666d1d8e9',
+            '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' => '621d6d857400983d0fd6cd291f105a835a317748',
+            'dev_requirement' => false,
+        ),
+        'spatie/calendar-links' => array(
+            'pretty_version' => '1.8.0',
+            'version' => '1.8.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../spatie/calendar-links',
+            'aliases' => array(),
+            'reference' => '654fc0174e6279539b9bf0abdc3339e34c5286d7',
+            'dev_requirement' => false,
+        ),
+        'stripe/stripe-php' => array(
+            'pretty_version' => 'v7.128.0',
+            'version' => '7.128.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../stripe/stripe-php',
+            'aliases' => array(),
+            'reference' => 'c704949c49b72985c76cc61063aa26fefbd2724e',
+            'dev_requirement' => false,
+        ),
+        'symfony/deprecation-contracts' => array(
+            'pretty_version' => 'v2.5.2',
+            'version' => '2.5.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
+            'aliases' => array(),
+            'reference' => 'e8b495ea28c1d97b5e0c121748d6f9b53d075c66',
+            'dev_requirement' => false,
+        ),
+        'tpyo/amazon-s3-php-class' => array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../tpyo/amazon-s3-php-class',
+            'aliases' => array(
+                0 => '9999999-dev',
+            ),
+            'reference' => '7c62b0e9b7d956a04e377ca236979a78dbac4e66',
+            'dev_requirement' => false,
+        ),
+        'twilio/sdk' => array(
+            'pretty_version' => '5.42.2',
+            'version' => '5.42.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../twilio/sdk',
+            'aliases' => array(),
+            'reference' => '0cfcb871b18a9c427dd9e8f0ed7458d43009b48a',
+            'dev_requirement' => false,
+        ),
+        'unaio/amazon-s3-php-class-hmac-v2' => array(
+            'pretty_version' => '0.6',
+            'version' => '0.6.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../unaio/amazon-s3-php-class-hmac-v2',
+            'aliases' => array(),
+            'reference' => '3687dec0af7be2cb11f21828aa4b12d20c4c5935',
+            'dev_requirement' => false,
+        ),
+        'unaio/una' => array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../../',
+            'aliases' => array(),
+            'reference' => 'd42837e25c7c0a517403209a8a7d1b7f59645787',
+            'dev_requirement' => false,
+        ),
+        'wikimedia/less.php' => array(
+            'pretty_version' => 'v4.1.1',
+            'version' => '4.1.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../wikimedia/less.php',
+            'aliases' => array(),
+            'reference' => '7c814ca156716e7b46ad84ac038bf298ff91cbb4',
+            'dev_requirement' => false,
+        ),
+    ),
+);

+ 221 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Embera.php

@@ -0,0 +1,221 @@
+<?php
+/**
+ * Embera.php
+ *
+ * @package Embera
+ * @author Michael Pratt <yo@michael-pratt.com>
+ * @link   http://www.michael-pratt.com/
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Embera;
+
+use Embera\Http\HttpClient;
+use Embera\Http\OembedClient;
+use Embera\Http\HttpClientInterface;
+use Embera\Html\IgnoreTags;
+use Embera\Html\ResponsiveEmbeds;
+use Embera\ProviderCollection\ProviderCollectionInterface;
+use Embera\ProviderCollection\DefaultProviderCollection;
+
+/**
+ * Embera
+ * The Main Class of this library.
+ */
+class Embera
+{
+    /** @var string Current Library Version */
+    const VERSION = '2.0.35';
+
+    /**
+     * Constants describing how the library is
+     * going to handle fake responses.
+     *
+     * ALLOW_FAKE_RESPONSES is the default value.
+     */
+    const ALLOW_FAKE_RESPONSES = 1;
+    const DISABLE_FAKE_RESPONSES = 2;
+    const ONLY_FAKE_RESPONSES = 3;
+
+    /** @var array Configuration settings */
+    protected $config = [];
+
+    /** @var array Logged errors */
+    protected $errors = [];
+
+    /** @var array Closures to be used on oembed responses */
+    protected $filters = [];
+
+    /** @var ProviderCollectionInterface */
+    protected $providerCollection;
+
+    /** @var HttpClientInterface */
+    protected $httpClient;
+
+    /**
+     * Constructor
+     *
+     * @param array $config
+     * @param ProviderCollectionInterface $collection
+     * @param HttpClientInterface $httpClient
+     * @return void
+     */
+    public function __construct(array $config = [], ProviderCollectionInterface $collection = null, HttpClientInterface $httpClient = null)
+    {
+        $this->config = array_merge([
+            'https_only' => false,
+            'fake_responses' => self::ALLOW_FAKE_RESPONSES,
+            'ignore_tags' => [ 'pre', 'code', 'a', 'img', 'iframe', 'oembed' ],
+            'responsive' => false,
+            'width' => 0,
+            'height' => 0,
+            'maxheight' => 0,
+            'maxwidth' => 0,
+            'referer' => '',
+            'curl_params' => [],
+            'file_get_contents_params' => [],
+        ], $config);
+
+        $this->config['maxwidth'] = max($this->config['width'], $this->config['maxwidth']);
+        $this->config['maxheight'] = max($this->config['height'], $this->config['maxheight']);
+        unset($this->config['height'], $this->config['width']);
+
+        $this->providerCollection = $collection;
+        if (!$collection) {
+            $this->providerCollection = new DefaultProviderCollection($this->config);
+        }
+
+        $this->httpClient = $httpClient;
+        if (!$httpClient) {
+            $this->httpClient = new HttpClient($this->config);
+        }
+
+        // Set the config just in case.
+        $this->providerCollection->setConfig($this->config);
+        $this->httpClient->setConfig($this->config);
+    }
+
+    /**
+     * Embeds known/available services into the given text.
+     *
+     * @param mixed $text
+     * @return string
+     */
+    public function autoEmbed($text)
+    {
+        if (is_string($text)) {
+            $table = [];
+            $providers = $this->getUrlData($text);
+            foreach ($providers as $url => $response) {
+                if (!empty($response['html'])) {
+                    $table[$url] = $response['html'];
+                }
+            }
+
+            if (!empty($table)) {
+
+                if (!empty($this->config['ignore_tags']) && strpos($text, '>') !== false) {
+                    $ignoreTags = new IgnoreTags($this->config['ignore_tags']);
+                    return $ignoreTags->replace($text, $table);
+                }
+
+                return strtr($text, $table);
+            }
+
+        }  else {
+            $this->errors[] = 'For auto-embedding purposes, the input must be a string';
+        }
+
+        return $text;
+    }
+
+    /**
+     * Returns the oembed response from the given data.
+     *
+     * @param array|string $urls An array with urls or a string with urls.
+     * @return array
+     */
+    public function getUrlData($urls)
+    {
+        $return = [];
+        foreach ($this->providerCollection->findProviders($urls) as $url => $provider) {
+            try {
+
+                $oembedClient = new OembedClient($this->config, $this->httpClient);
+                $response = $oembedClient->getResponseFrom($provider);
+
+                if ($this->config['responsive'] && !$provider->hasResponsiveSupport()) {
+                    $responsive = new ResponsiveEmbeds();
+                    $response = $responsive->transform($response);
+                }
+
+                $return[$url] = $this->applyFilters($response);
+
+            } catch (\Exception $e) {
+                $this->errors[] = $e->getMessage();
+            }
+        }
+
+        return array_filter($return);
+    }
+
+    /**
+     * Adds a filter to the oembed response
+     *
+     * @param callable $closure
+     * @return void
+     */
+    public function addFilter(callable $closure)
+    {
+        $this->filters[] = $closure;
+    }
+
+    /**
+     * Applies registered filters/closures
+     * to the oembed response.
+     *
+     * @param array $response
+     * @return array
+     */
+    protected function applyFilters(array $response)
+    {
+        foreach ($this->filters as $closure) {
+            $response = $closure($response);
+        }
+
+        return $response;
+    }
+
+    /**
+     * Gets the last error found
+     *
+     * @return string
+     */
+    public function getLastError()
+    {
+        return end($this->errors);
+    }
+
+    /**
+     * Returns an array with all the errors
+     *
+     * @return array
+     */
+    public function getErrors()
+    {
+        return $this->errors;
+    }
+
+    /**
+     * Checks if there were errors
+     *
+     * @return bool
+     */
+    public function hasErrors()
+    {
+        return (!empty($this->errors));
+    }
+
+}

+ 56 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Assemblrworld.php

@@ -0,0 +1,56 @@
+<?php
+/**
+ * Assemblrworld.php
+ *
+ * @package Embera
+ * @author Michael Pratt <yo@michael-pratt.com>
+ * @link   http://www.michael-pratt.com/
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Embera\Provider;
+
+use Embera\Url;
+
+/**
+ * Assemblrworld Provider
+ *
+ * @link https://assemblrworld
+ */
+class Assemblrworld extends ProviderAdapter implements ProviderInterface
+{
+    /** inline {@inheritdoc} */
+    protected $endpoint = 'https://studio.assemblrworld.com/api/oembed?format=json';
+
+    /** inline {@inheritdoc} */
+    protected static $hosts = [
+        '*.assemblrworld.com',
+        '*.assemblr.world',
+    ];
+
+    /** inline {@inheritdoc} */
+    protected $allowedParams = [ 'maxwidth', 'maxheight' ];
+
+    /** inline {@inheritdoc} */
+    protected $httpsSupport = true;
+
+    /** inline {@inheritdoc} */
+    protected $responsiveSupport = false;
+
+    /** inline {@inheritdoc} */
+    public function validateUrl(Url $url)
+    {
+        return (bool) (preg_match('~\.(com|world)/([^/]+)~i', (string) $url));
+    }
+
+    /** inline {@inheritdoc} */
+    public function normalizeUrl(Url $url)
+    {
+        $url->convertToHttps();
+        $url->removeLastSlash();
+
+        return $url;
+    }
+}

+ 56 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Behance.php

@@ -0,0 +1,56 @@
+<?php
+/**
+ * Behance.php
+ *
+ * @package Embera
+ * @author Michael Pratt <yo@michael-pratt.com>
+ * @link   http://www.michael-pratt.com/
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Embera\Provider;
+
+use Embera\Url;
+
+/**
+ * Behance Provider
+ *
+ * @link https://behance.net
+ */
+class Behance extends ProviderAdapter implements ProviderInterface
+{
+    /** inline {@inheritdoc} */
+    protected $endpoint = 'https://www.behance.net/services/oembed?format=json';
+
+    /** inline {@inheritdoc} */
+    protected static $hosts = [
+        'behance.net'
+    ];
+
+    /** inline {@inheritdoc} */
+    protected $allowedParams = [ 'maxwidth', 'maxheight' ];
+
+    /** inline {@inheritdoc} */
+    protected $httpsSupport = true;
+
+    /** inline {@inheritdoc} */
+    protected $responsiveSupport = false;
+
+    /** inline {@inheritdoc} */
+    public function validateUrl(Url $url)
+    {
+        return (bool) (preg_match('~behance\.net/([^/]+)/([^/]+)/([^/]+)~i', (string) $url));
+    }
+
+    /** inline {@inheritdoc} */
+    public function normalizeUrl(Url $url)
+    {
+        $url->convertToHttps();
+        $url->removeQueryString();
+        $url->removeLastSlash();
+
+        return $url;
+    }
+}

+ 57 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Docswell.php

@@ -0,0 +1,57 @@
+<?php
+/**
+ * Docswell.php
+ *
+ * @package Embera
+ * @author Michael Pratt <yo@michael-pratt.com>
+ * @link   http://www.michael-pratt.com/
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Embera\Provider;
+
+use Embera\Url;
+
+/**
+ * Docswell Provider
+ *
+ * @link https://docswell.com
+ */
+class Docswell extends ProviderAdapter implements ProviderInterface
+{
+    /** inline {@inheritdoc} */
+    protected $endpoint = 'https://www.docswell.com/service/oembed?format=json';
+
+    /** inline {@inheritdoc} */
+    protected static $hosts = [
+        'docswell.com'
+    ];
+
+    /** inline {@inheritdoc} */
+    protected $allowedParams = [ 'maxwidth', 'maxheight' ];
+
+    /** inline {@inheritdoc} */
+    protected $httpsSupport = true;
+
+    /** inline {@inheritdoc} */
+    protected $responsiveSupport = false;
+
+    /** inline {@inheritdoc} */
+    public function validateUrl(Url $url)
+    {
+        return (bool) (preg_match('~docswell\.com/s/([^/]+)/([^/]+)~i', (string) $url));
+    }
+
+    /** inline {@inheritdoc} */
+    public function normalizeUrl(Url $url)
+    {
+        $url->convertToHttps();
+        $url->removeQueryString();
+        $url->removeLastSlash();
+
+        return $url;
+    }
+
+}

+ 54 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/EchoesHQ.php

@@ -0,0 +1,54 @@
+<?php
+/**
+ * EchoesHQ.php
+ *
+ * @package Embera
+ * @author Michael Pratt <yo@michael-pratt.com>
+ * @link   http://www.michael-pratt.com/
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Embera\Provider;
+
+use Embera\Url;
+
+/**
+ * EchoesHQ Provider
+ *
+ * @link https://echoeshq.com
+ */
+class EchoesHQ extends ProviderAdapter implements ProviderInterface
+{
+    /** inline {@inheritdoc} */
+    protected $endpoint = 'https://api.echoeshq.com/oembed?format=json';
+
+    /** inline {@inheritdoc} */
+    protected static $hosts = [
+        '*.echoeshq.com'
+    ];
+
+    /** inline {@inheritdoc} */
+    protected $allowedParams = [ 'maxwidth', 'maxheight' ];
+
+    /** inline {@inheritdoc} */
+    protected $httpsSupport = true;
+
+    /** inline {@inheritdoc} */
+    protected $responsiveSupport = true;
+
+    /** inline {@inheritdoc} */
+    public function validateUrl(Url $url)
+    {
+        return (bool) (preg_match('~echoeshq\.com/embed/([^/]+)~i', (string) $url));
+    }
+
+    /** inline {@inheritdoc} */
+    public function normalizeUrl(Url $url)
+    {
+        $url->convertToHttps();
+        $url->removeLastSlash();
+        return $url;
+    }
+}

+ 74 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Fader.php

@@ -0,0 +1,74 @@
+<?php
+/**
+ * Fader.php
+ *
+ * @package Embera
+ * @author Michael Pratt <yo@michael-pratt.com>
+ * @link   http://www.michael-pratt.com/
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Embera\Provider;
+
+use Embera\Url;
+
+/**
+ * Fader Provider
+ * No Description
+ *
+ * @link https://app.getfader.com
+ */
+class Fader extends ProviderAdapter implements ProviderInterface
+{
+    /** inline {@inheritdoc} */
+    protected $endpoint = 'https://app-api.getfader.com/api/oembed?format=json';
+
+    /** inline {@inheritdoc} */
+    protected static $hosts = [
+        'app.getfader.com'
+    ];
+
+    /** inline {@inheritdoc} */
+    protected $httpsSupport = true;
+
+    /** inline {@inheritdoc} */
+    public function validateUrl(Url $url)
+    {
+        return (bool) (preg_match('~app\.getfader\.com/projects/([^/]+)/publish/?$~i', (string) $url));
+    }
+
+    /** inline {@inheritdoc} */
+    public function normalizeUrl(Url $url)
+    {
+        $url->convertToHttps();
+        $url->removeQueryString();
+        $url->removeLastSlash();
+
+        return $url;
+    }
+
+    /** inline {@inheritdoc} */
+    public function getFakeResponse()
+    {
+        $embedUrl = $this->url;
+        // <iframe allowfullscreen="allowfullscreen" allowvr="allowvr" width=1200 height=600 src="https://app.getfader.com/projects/89a73dd0-4926-4f05-a9a8-6896a0a07a71/publish"></iframe>
+
+        $attr = [];
+        $attr[] = 'allowfullscreen="allowfullscreen"';
+        $attr[] = 'allowvr="allowvr"';
+        $attr[] = 'width="{width}"';
+        $attr[] = 'height="{height}"';
+        $attr[] = 'src="' . $embedUrl . '"';
+
+        return [
+            'type' => 'rich',
+            'provider_name' => 'Fader',
+            'provider_url' => 'https://app.getfader.com',
+            'title' => 'Unknown title',
+            'html' => '<iframe ' . implode(' ', $attr). '></iframe>',
+        ];
+    }
+
+}

+ 56 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Fooday.php

@@ -0,0 +1,56 @@
+<?php
+/**
+ * fooday.php
+ *
+ * @package Embera
+ * @author Michael Pratt <yo@michael-pratt.com>
+ * @link   http://www.michael-pratt.com/
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Embera\Provider;
+
+use Embera\Url;
+
+/**
+ * fooday Provider
+ *
+ * @link https://fooday.app
+ */
+class Fooday extends ProviderAdapter implements ProviderInterface
+{
+    /** inline {@inheritdoc} */
+    protected $endpoint = 'https://fooday.app/oembed?format=json';
+
+    /** inline {@inheritdoc} */
+    protected static $hosts = [
+        'fooday.app'
+    ];
+
+    /** inline {@inheritdoc} */
+    protected $allowedParams = [ 'maxwidth', 'maxheight' ];
+
+    /** inline {@inheritdoc} */
+    protected $httpsSupport = true;
+
+    /** inline {@inheritdoc} */
+    protected $responsiveSupport = false;
+
+    /** inline {@inheritdoc} */
+    public function validateUrl(Url $url)
+    {
+        return (bool) (preg_match('~fooday\.app/([^/]+)/(reviews|spots)/([^/]+)~i', (string) $url));
+    }
+
+    /** inline {@inheritdoc} */
+    public function normalizeUrl(Url $url)
+    {
+        $url->convertToHttps();
+        $url->removeQueryString();
+        $url->removeLastSlash();
+
+        return $url;
+    }
+}

+ 58 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Spotify.php

@@ -0,0 +1,58 @@
+<?php
+/**
+ * Spotify.php
+ *
+ * @package Embera
+ * @author Michael Pratt <yo@michael-pratt.com>
+ * @link   http://www.michael-pratt.com/
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Embera\Provider;
+
+use Embera\Url;
+
+/**
+ * Spotify Provider
+ * En Spotify, puedes encontrar toda la música que necesitas.
+ *
+ * @link https://spotify.com
+ * @see  https://developer.spotify.com/documentation/embeds/reference/oembed
+ *
+ */
+class Spotify extends ProviderAdapter implements ProviderInterface
+{
+    /** inline {@inheritdoc} */
+    protected $endpoint = 'https://open.spotify.com/oembed?format=json';
+
+    /** inline {@inheritdoc} */
+    protected static $hosts = [
+        'open.spotify.com'
+    ];
+
+    /** inline {@inheritdoc} */
+    protected $httpsSupport = true;
+
+    /** inline {@inheritdoc} */
+    public function validateUrl(Url $url)
+    {
+        return (bool) (
+            preg_match('~spotify\.com/(?:track|album|playlist|show|episode)/(?:[^/]+)(?:/[^/]*)?$~i', (string) $url) ||
+            preg_match('~spotify\.com/user/(?:[^/]+)/playlist/(?:[^/]+)/?$~i', (string) $url)
+            // preg_match('~spoti\.fi/(?:[^/]+)$~i', (string) $url)
+        );
+    }
+
+    /** inline {@inheritdoc} */
+    public function normalizeUrl(Url $url)
+    {
+        $url->convertToHttps();
+        $url->removeQueryString();
+        $url->removeLastSlash();
+
+        return $url;
+    }
+
+}

+ 56 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Spyke.php

@@ -0,0 +1,56 @@
+<?php
+/**
+ * Spyke.php
+ *
+ * @package Embera
+ * @author Michael Pratt <yo@michael-pratt.com>
+ * @link   http://www.michael-pratt.com/
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Embera\Provider;
+
+use Embera\Url;
+
+/**
+ * Spyke Provider
+ *
+ * @link https://spyke.social
+ */
+class Spyke extends ProviderAdapter implements ProviderInterface
+{
+    /** inline {@inheritdoc} */
+    protected $endpoint = 'https://api.spyke.social/embed/oembed?format=json';
+
+    /** inline {@inheritdoc} */
+    protected static $hosts = [
+        '*.spyke.social'
+    ];
+
+    /** inline {@inheritdoc} */
+    protected $allowedParams = [ 'maxwidth', 'maxheight' ];
+
+    /** inline {@inheritdoc} */
+    protected $httpsSupport = true;
+
+    /** inline {@inheritdoc} */
+    protected $responsiveSupport = false;
+
+    /** inline {@inheritdoc} */
+    public function validateUrl(Url $url)
+    {
+        return (bool) (preg_match('~spyke\.social/([^/]+)/([^/]+)~i', (string) $url));
+    }
+
+    /** inline {@inheritdoc} */
+    public function normalizeUrl(Url $url)
+    {
+        $url->convertToHttps();
+        $url->removeQueryString();
+        $url->removeLastSlash();
+
+        return $url;
+    }
+}

+ 83 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Tickcounter.php

@@ -0,0 +1,83 @@
+<?php
+/**
+ * Tickcounter.php
+ *
+ * @package Embera
+ * @author Michael Pratt <yo@michael-pratt.com>
+ * @link   http://www.michael-pratt.com/
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Embera\Provider;
+
+use Embera\Url;
+
+/**
+ * Tickcounter Provider
+ * No description.
+ *
+ * @link https://tickcounter.com
+ *
+ */
+class Tickcounter extends ProviderAdapter implements ProviderInterface
+{
+    /** inline {@inheritdoc} */
+    protected $endpoint = 'https://www.tickcounter.com/oembed?format=json';
+
+    /** inline {@inheritdoc} */
+    protected static $hosts = [
+        'tickcounter.com'
+    ];
+
+    /** inline {@inheritdoc} */
+    protected $httpsSupport = true;
+
+    /** inline {@inheritdoc} */
+    public function validateUrl(Url $url)
+    {
+        return (bool) (preg_match('~tickcounter\.com/(countdown|countup|ticker|worldclock|widget|clock)/([^/]+)~i', (string) $url));
+    }
+
+    /** inline {@inheritdoc} */
+    public function normalizeUrl(Url $url)
+    {
+        $url->convertToHttps();
+        $url->removeQueryString();
+        $url->removeLastSlash();
+
+        return $url;
+    }
+
+    /** inline {@inheritdoc} */
+    public function modifyResponse(array $response = [])
+    {
+        if (!empty($response['html'])) {
+            $response['html'] = preg_replace('~title="(.+)"~i', 'title=""', $response['html']);
+        }
+
+        return $response;
+    }
+
+    /** inline {@inheritdoc} */
+    public function getFakeResponse()
+    {
+        preg_match('~(countdown|countup|ticker|worldclock)/([^/]+)~', (string) $this->url, $m);
+        $embedUrl = 'https://www.tickcounter.com/widget/' . $m['1'] . '/' . $m['2'];
+
+        $attr = [];
+        $attr[] = 'src="' . $embedUrl . '"';
+        $attr[] = 'style="top:0; left:0; width:100%; height:100%; position:absolute; border:0; overflow:hidden';
+        $attr[] = 'title=""';
+
+        return [
+            'type' => 'rich',
+            'provider_name' => 'Tickcounter',
+            'provider_url' => 'https://tickcounter.com',
+            'title' => 'Unknown title',
+            'html' => '<div style="left:0; width:500px; height:125px; position:relative; padding-bottom:0; margin:0 auto"><iframe ' . implode(' ', $attr). '></iframe></div>',
+        ];
+    }
+
+}

+ 57 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/TonicAudio.php

@@ -0,0 +1,57 @@
+<?php
+/**
+ * TonicAudio.php
+ *
+ * @package Embera
+ * @author Michael Pratt <yo@michael-pratt.com>
+ * @link   http://www.michael-pratt.com/
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Embera\Provider;
+
+use Embera\Url;
+
+/**
+ * TonicAudio Provider
+ *
+ * @link https://tonicaudio.com
+ */
+class TonicAudio extends ProviderAdapter implements ProviderInterface
+{
+    /** inline {@inheritdoc} */
+    protected $endpoint = 'https://tonicaudio.com/oembed?format=json';
+
+    /** inline {@inheritdoc} */
+    protected static $hosts = [
+        'tonicaudio.com',
+        'tnic.io',
+    ];
+
+    /** inline {@inheritdoc} */
+    protected $allowedParams = [ 'maxwidth', 'maxheight' ];
+
+    /** inline {@inheritdoc} */
+    protected $httpsSupport = true;
+
+    /** inline {@inheritdoc} */
+    protected $responsiveSupport = false;
+
+    /** inline {@inheritdoc} */
+    public function validateUrl(Url $url)
+    {
+        return (bool) (preg_match('~\.(com|io)/(song|take)/([^/]+)~i', (string) $url));
+    }
+
+    /** inline {@inheritdoc} */
+    public function normalizeUrl(Url $url)
+    {
+        $url->convertToHttps();
+        $url->removeQueryString();
+        $url->removeLastSlash();
+
+        return $url;
+    }
+}

+ 64 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Twitter.php

@@ -0,0 +1,64 @@
+<?php
+/**
+ * Twitter.php
+ *
+ * @package Embera
+ * @author Michael Pratt <yo@michael-pratt.com>
+ * @link   http://www.michael-pratt.com/
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Embera\Provider;
+
+use Embera\Url;
+
+/**
+ * Twitter Provider
+ * No description.
+ *
+ * @link https://twitter.com
+ * @see https://developer.twitter.com/en/docs/twitter-for-websites/oembed-api
+ */
+class Twitter extends ProviderAdapter implements ProviderInterface
+{
+    /** inline {@inheritdoc} */
+    protected $endpoint = 'https://publish.twitter.com/oembed?format=json';
+
+    /** inline {@inheritdoc} */
+    protected static $hosts = [
+        'twitter.com',
+        'x.com'
+    ];
+
+    /** inline {@inheritdoc} */
+    protected $allowedParams = [
+        'maxwidth', 'maxheight', 'hide_media', 'hide_thread', 'omit_script', 'align',
+        'related', 'lang', 'theme', 'link_color', 'widget_type', 'dnt'
+    ];
+
+    /** inline {@inheritdoc} */
+    protected $httpsSupport = true;
+
+    /** inline {@inheritdoc} */
+    protected $responsiveSupport = true;
+
+    /** inline {@inheritdoc} */
+    public function validateUrl(Url $url)
+    {
+        return (bool) (preg_match('~twitter\.com/(?:[^/]+)/(?:status|moments)/(?:[0-9]+)~i', (string) $url));
+    }
+
+    /** inline {@inheritdoc} */
+    public function normalizeUrl(Url $url)
+    {
+        $url->convertToHttps();
+        $url->removeQueryString();
+        $url->removeLastSlash();
+
+        $url->setHost('twitter.com');
+        return $url;
+    }
+
+}

+ 59 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/Provider/Wistia.php

@@ -0,0 +1,59 @@
+<?php
+/**
+ * Wistia.php
+ *
+ * @package Embera
+ * @author Michael Pratt <yo@michael-pratt.com>
+ * @link   http://www.michael-pratt.com/
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Embera\Provider;
+
+use Embera\Url;
+
+/**
+ * Wistia Provider
+ * See how our video and podcast hosting, marketing tools, player customizations, and detailed ana...
+ *
+ * @link https://wistia.com
+ *
+ */
+class Wistia extends ProviderAdapter implements ProviderInterface
+{
+    /** inline {@inheritdoc} */
+    protected $endpoint = 'https://fast.wistia.com/oembed.json';
+
+    /** inline {@inheritdoc} */
+    protected static $hosts = [
+        '*.wistia.com'
+    ];
+
+    /** inline {@inheritdoc} */
+    protected $httpsSupport = true;
+
+    /** inline {@inheritdoc} */
+    protected $responsiveSupport = true;
+
+    /** inline {@inheritdoc} */
+    public function validateUrl(Url $url)
+    {
+        return (bool) (
+            preg_match('~wistia\.com/embed/(iframe|playlists)/([^/]+)~i', (string) $url) ||
+            preg_match('~wistia\.com/medias/([^/]+)~i', (string) $url)
+        );
+    }
+
+    /** inline {@inheritdoc} */
+    public function normalizeUrl(Url $url)
+    {
+        $url->convertToHttps();
+        $url->removeQueryString();
+        $url->removeLastSlash();
+
+        return $url;
+    }
+
+}

+ 256 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/ProviderCollection/DefaultProviderCollection.php

@@ -0,0 +1,256 @@
+<?php
+/**
+ * DefaultProviderCollection.php
+ *
+ * @package Embera
+ * @author Michael Pratt <yo@michael-pratt.com>
+ * @link   http://www.michael-pratt.com/
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Embera\ProviderCollection;
+
+/**
+ * Basically its a wrapper for the ProvidercollectionAdapter but defines the
+ * default providers supported by the library
+ */
+class DefaultProviderCollection extends ProviderCollectionAdapter
+{
+    /** inline {@inheritdoc} */
+    public function __construct(array $config = [])
+    {
+        parent::__construct($config);
+        $this->registerProvider([
+            'TwentyThreeHq',
+            'Acast',
+            'ActBlue',
+            'Adilo',
+            'Adways',
+            'Afreecatv',
+            'Altru',
+            'Altium',
+            'Animoto',
+            'AnnieMusic',
+            'Apester',
+            'Archivos',
+            'Assemblrworld',
+            'Audioboom',
+            'Audiocom',
+            'AudioClip',
+            'Audiomack',
+            'Audiomeans',
+            'BeautifulAI',
+            'Behance',
+            'BeamsFm',
+            'BlackfireIO',
+            'Blogcast',
+            'Bookingmood',
+            'Buttondown',
+            'Bumper',
+            'Canva',
+            'Ceros',
+            'Chainflix',
+            'ChartBlocks',
+            'Chroco',
+            'CircuitLab',
+            'Clyp','CodeHS',
+            'Codepoints',
+            'CodeSandbox',
+            'Commaful',
+            'Coub',
+            'Crumbs',
+            'Cueup',
+            'Curated',
+            'Dadan',
+            'Datawrapper',
+            'DailyMotion',
+            'Dalexni',
+            'Deseretnews',
+            'Deviantart',
+            'Didacte',
+            'Digiteka',
+            'DocDroid',
+            'Docswell',
+            'DreamBroker',
+            'EchoesHQ',
+            'Embedery',
+            'Ethfiddle',
+            'EventLive',
+            'Everviz',
+            'Eyrie',
+            'Facebook',
+            'Fader',
+            'FaithLifeTV',
+            'Fitapp',
+            'FITE',
+            'Flickr',
+            'Flourish',
+            'Framer',
+            'Fooday',
+            'GeographUk',
+            'GeographCI',
+            'GeographDE',
+            'GetShow',
+            'GettyImages',
+            'Giphy',
+            'GloriaTV',
+            'Gong',
+            'Gmetri',
+            'Grain',
+            'Gumlet',
+            'Gyazo',
+            'Hash',
+            'Hearthis',
+            'Heyzine',
+            'HiHaHo',
+            'HippoVideo',
+            'Huffduffer',
+            'iFixit',
+            'IHeartRadio',
+            'Imgur',
+            'Infogram',
+            'Infoveave',
+            'Injurymap',
+            'Instagram',
+            'InsticatorContentEngagementUnit',
+            'Issuu',
+            'ItemisCreate',
+            'Jovian',
+            'KakaoTV',
+            'Kickstarter',
+            'KirimEmail',
+            'Kit',
+            'Kurozora',
+            'Kooapp',
+            'Knacki',
+            'LearningApps',
+            'Libsyn',
+            'Lineplace',
+            'Livestream',
+            'LocalVoicesNetwork',
+            'Loom',
+            'LottieFiles',
+            'Ludus',
+            'Lumiere',
+            'Matterport',
+            'MediaLab',
+            'Mediastream',
+            'MedienArchivKuenste',
+            'MermaidInk',
+            'MicrosoftStream',
+            'Miro',
+            'MixCloud',
+            'Mixpanel',
+            'MusicboxManiacs',
+            'Namchey',
+            'Nanoo',
+            'NaturalAtlas',
+            'NDLA',
+            'Nfb',
+            'NFTndx',
+            'Odysee',
+            'OmnyStudio',
+            'OraTV',
+            'Orbitvu',
+            'Origits',
+            'Outplayed',
+            'OverflowIO',
+            'Pandavideo',
+            'Pastery',
+            'Piggy',
+            'Pikasso',
+            'Pinpoll',
+            'Pinterest',
+            'Pitchhub',
+            'Playbuzz',
+            'Plusdocs',
+            'Podbean',
+            'Polldaddy',
+            'Portfolium',
+            'Prezi',
+            'QTpi',
+            'RadioPublic',
+            'Rcvis',
+            'Reddit',
+            'ReleaseWire',
+            'Replit',
+            'ReverbNation',
+            'RoosterTeeth',
+            'Rumble',
+            'RunKit',
+            'SapoVideos',
+            'ScreenNine',
+            'Screencast',
+            'ScribbleMaps',
+            'Scribd',
+            'SendToNews',
+            'Shoudio',
+            'ShowTheWay',
+            'Sketchfab',
+            'SlateApp',
+            'Slideshare',
+            'SmashNotes',
+            'Smeme',
+            'Smugmug',
+            'SocialExplorer',
+            'SongLink',
+            'SoundCloud',
+            'SpeakerDeck',
+            'Spotify',
+            'Spotlightr',
+            'Spreaker',
+            'SproutVideo',
+            'Spyke',
+            'StandfordDigitalRepository',
+            'Streamable',
+            'Streamio',
+            'Sudomemo',
+            'Sutori',
+            'Sway',
+            'Synthesia',
+            'Ted',
+            'TheNewYorkTimes',
+            'Tickcounter',
+            'TikTok',
+            'Toornament',
+            'TonicAudio',
+            'TrinityAudio',
+            'Tumblr',
+            'Tuxx',
+            'Tvcf',
+            'Twinmotion',
+            'Twitter',
+            'UAPod',
+            'UniversitePantheonSorbonne',
+            'UniversityCambridgeMap',
+            'UpecPod',
+            'UstreamTV',
+            'Ustudio',
+            'Verse',
+            'VidMount',
+            'Videfit',
+            'Vidyard',
+            'Vimeo',
+            'Viously',
+            'Vlipsy',
+            'VouchFor',
+            'WaveVideo',
+            'Wecandeo',
+            'WolframCloud',
+            'VoxSnap',
+            'Whimsical',
+            'Wistia',
+            'Wizer',
+            'Wokwi',
+            'Wordpress',
+            'Wordwall',
+            'Youtube',
+            'Yumpu',
+            'Zeplin',
+            'Zingsoft',
+            'Zoomable',
+        ]);
+    }
+}

+ 52 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/plugins/mpratt/embera/src/Embera/ProviderCollection/SlimProviderCollection.php

@@ -0,0 +1,52 @@
+<?php
+/**
+ * SlimProviderCollection.php
+ *
+ * @package Embera
+ * @author Michael Pratt <yo@michael-pratt.com>
+ * @link   http://www.michael-pratt.com/
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Embera\ProviderCollection;
+
+/**
+ * A slim provider collection with popular oembed providers/sites.
+ * There is no criteria for choosing this providers other than my perception of
+ * popularity. if you want me to add a "popular" site to this collection
+ * please let me know.
+ */
+class SlimProviderCollection extends ProviderCollectionAdapter
+{
+    /** inline {@inheritdoc} */
+    public function __construct(array $config = [])
+    {
+        parent::__construct($config);
+        $this->registerProvider([
+            'DailyMotion',
+            'Deviantart',
+            'Facebook',
+            'Flickr',
+            'GettyImages',
+            'Giphy',
+            'Instagram',
+            'Imgur',
+            'Kickstarter',
+            'Livestream','Reddit',
+            'Scribd',
+            'Sketchfab',
+            'Slideshare',
+            'SoundCloud',
+            'SpeakerDeck',
+            'Spotify',
+            'Ted',
+            'Tumblr',
+            'Twitter',
+            'Vimeo',
+            'Wordpress',
+            'Youtube',
+        ]);
+    }
+}

+ 615 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/studio/classes/BxDolStudioBuilderPageQuery.php

@@ -0,0 +1,615 @@
+<?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 BxDolStudioBuilderPageQuery extends BxDolStudioPageQuery
+{
+    function __construct()
+    {
+        parent::__construct();
+    }
+
+    public function insertPage ($sObj, $sModule, $sUri, $sUrl, $sTitleLangKey, $iType = 1, $iLayoutId = 5, $iVisibleForLevels = 2147483647, $sClass = '', $sClassFile = '')
+    {
+        $b = $this->query('INSERT INTO `sys_objects_page` SET
+            `author` = :author,
+            `added` = :added,
+            `object` = :obj,
+            `uri` = :uri,
+            `title` = :title,
+            `module` = :module,
+            `cover` = :cover,
+            `type_id` = :type,
+            `layout_id` = :layout,
+            `visible_for_levels` = :levels, 
+            `visible_for_levels_editable` = 1,
+            `url` = :url,
+            `cache_lifetime` = 0,
+            `cache_editable` = 1,
+            `deletable` = 1,
+            `override_class_name` = :class,
+            `override_class_file` = :file
+        ', array(
+            'author' => bx_get_logged_profile_id(),
+            'added' => time(),
+            'obj' => $sObj,
+            'uri' => $sUri,
+            'title' => $sTitleLangKey,
+            'module' => $sModule,
+            'cover' => 0,
+            'type' => $iType,
+            'layout' => $iLayoutId,
+            'levels' => $iVisibleForLevels,
+            'url' => $sUrl,
+            'class' => $sClass,
+            'file' => $sClassFile,
+        ));
+        if (!$b)
+            return false;
+
+        return $this->lastId();
+    }
+
+    function getPages($aParams)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sSelectClause = "
+            `tp`.`id` AS `id`,
+            `tp`.`object` AS `object`,
+            `tp`.`uri` AS `uri`,
+            `tp`.`title_system` AS `title_system`,
+            `tp`.`title` AS `title`,
+            `tp`.`module` AS `module`,
+            `tp`.`cover` AS `cover`,
+            `tp`.`cover_image` AS `cover_image`,
+            `tp`.`cover_title` AS `cover_title`,
+            `tp`.`type_id` AS `type_id`,
+            `tp`.`layout_id` AS `layout_id`,
+            `tp`.`submenu` AS `submenu`,
+            `tp`.`visible_for_levels` AS `visible_for_levels`,
+            `tp`.`visible_for_levels_editable` AS `visible_for_levels_editable`,
+            `tp`.`url` AS `url`,
+            `tp`.`meta_title` AS `meta_title`,
+            `tp`.`meta_description` AS `meta_description`,
+            `tp`.`meta_keywords` AS `meta_keywords`,
+            `tp`.`meta_robots` AS `meta_robots`,
+            `tp`.`cache_lifetime` AS `cache_lifetime`,
+            `tp`.`cache_editable` AS `cache_editable`,
+            `tp`.`inj_head` AS `inj_head`,
+            `tp`.`inj_footer` AS `inj_footer`,
+            `tp`.`sticky_columns` AS `sticky_columns`,
+            `tp`.`deletable` AS `deletable`,
+            `tp`.`override_class_name` AS `override_class_name`,
+            `tp`.`override_class_file` AS `override_class_file`";
+        $sJoinClause = $sWhereClause = $sGroupClause = $sOrderClause = $sLimitClause = "";
+
+        if(!isset($aParams['order']) || empty($aParams['order']))
+           $sOrderClause = "ORDER BY `tp`.`object` ASC";
+
+        switch($aParams['type']) {
+            case 'by_id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'id' => $aParams['value']
+                );
+
+                $sWhereClause = " AND `tp`.`id`=:id ";
+                break;
+            case 'by_object':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'object' => $aParams['value']
+                );
+
+                $sWhereClause = " AND `tp`.`object`=:object ";
+                break;
+
+            case 'by_uri':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'uri' => $aParams['value']
+                );
+
+                $sWhereClause = " AND `tp`.`uri`=:uri ";
+                break;
+
+            case 'by_object_full':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'object' => $aParams['value']
+                );
+
+                $sSelectClause .= ", `tpl`.`name` AS `layout_name`, `tpl`.`icon` AS `layout_icon`, `tpl`.`title` AS `layout_title`, `tpl`.`template` AS `layout_template`, `tpl`.`cells_number` AS `layout_cells_number`";
+                $sJoinClause = "LEFT JOIN `sys_pages_layouts` AS `tpl` ON `tp`.`layout_id`=`tpl`.`id`";
+                $sWhereClause = " AND `tp`.`object`=:object ";
+                break;
+
+            case 'by_module':
+            	$aMethod['params'][1] = array(
+                    'module' => $aParams['value']
+                );
+
+                $sWhereClause = " AND `tp`.`module`=:module ";
+                break;
+
+            case 'export':
+                $sSelectClause = "`tp`.*";
+                break;
+
+            case 'all':
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT 
+                " . $sSelectClause . "
+            FROM `sys_objects_page` AS `tp` " . $sJoinClause . "
+            WHERE 1 " . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause . " " . $sLimitClause;
+
+        return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+
+    function isPageExists($sObject)
+    {
+        $aPage = $this->getPages(['type' => 'by_object', 'value' => $sObject]);
+        return !empty($aPage) && is_array($aPage);
+    }
+
+    function addPage($aFields)
+    {
+        $sSql = "INSERT INTO `sys_objects_page` SET `" . implode("`=?, `", array_keys($aFields)) . "`=?";
+        $sSql = call_user_func_array(array($this, 'prepare'), array_merge(array($sSql), array_values($aFields)));
+        return (int)$this->query($sSql) > 0 ? $this->lastId() : 0;
+    }
+
+    function updatePage($iId, $aFields)
+    {
+        $sSql = "UPDATE `sys_objects_page` SET `" . implode("`=?, `", array_keys($aFields)) . "`=?  WHERE `id`=?";
+        $sSql = call_user_func_array(array($this, 'prepare'), array_merge(array($sSql), array_values($aFields), array($iId)));
+        return $this->query($sSql);
+    }
+    
+    function updatePageByObject($sObject, $aFields)
+    {
+        return $this->query("UPDATE `sys_objects_page` SET " . $this->arrayToSQL($aFields) . " WHERE `object`=:object", [
+            'object' => $sObject
+        ]);
+    }
+
+    function deletePages($aParams)
+    {
+    	$aBindings = array();
+        $sWhereClause = "";
+
+        switch($aParams['type']) {
+            case 'by_id':
+            	$aBindings = array(
+                	'id' => $aParams['value']
+                );
+
+                $sWhereClause = "AND `tp`.`id`=:id ";
+                break;
+
+            case 'by_object':
+            	$aBindings = array(
+                	'object' => $aParams['value']
+                );
+
+                $sWhereClause = "AND `tp`.`object`=:object ";
+                break;
+
+            case 'all':
+                break;
+        }
+
+        $sSql = "DELETE FROM `tp` USING `sys_objects_page` AS `tp` WHERE 1 " . $sWhereClause;
+        return (int)$this->query($sSql, $aBindings) > 0;
+    }
+
+    function isUniqUri($sUri)
+    {
+        $sSql = $this->prepare("SELECT `id` FROM `sys_objects_page` WHERE `uri`=? LIMIT 1", $sUri);
+        return (int)$this->getOne($sSql) <= 0;
+    }
+
+	function getTypes($aParams, &$aItems, $bReturnCount = true)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sSelectClause = $sJoinClause = $sWhereClause = $sGroupClause = $sOrderClause = $sLimitClause = "";
+
+        if(!isset($aParams['order']) || empty($aParams['order']))
+           $sOrderClause = "ORDER BY `tpt`.`id` ASC";
+
+        switch($aParams['type']) {
+            case 'by_id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                	'id' => $aParams['value']
+                );
+
+                $sWhereClause = "AND `tpt`.`id`=:id ";
+                break;
+
+            case 'all':
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT " . ($bReturnCount ? "SQL_CALC_FOUND_ROWS" : "") . "
+                `tpt`.`id` AS `id`,
+                `tpt`.`title` AS `title`,
+                `tpt`.`template` AS `template`,
+                `tpt`.`order` AS `order`" . $sSelectClause . "
+            FROM `sys_pages_types` AS `tpt` " . $sJoinClause . "
+            WHERE 1 " . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause . " " . $sLimitClause;
+        $aItems = call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+
+        if(!$bReturnCount)
+            return !empty($aItems);
+
+        return (int)$this->getOne("SELECT FOUND_ROWS()");
+    }
+
+    function getContentPlaceholders()
+    {
+        return $this->getPairs('SELECT `id`, `title` FROM `sys_pages_content_placeholders` ORDER BY `order`', 'id', 'title');
+    }
+
+    function getSubmenus($mixedTemplateIds = 8)
+    {
+        if(!is_array($mixedTemplateIds))
+            $mixedTemplateIds = array($mixedTemplateIds);
+
+        return BxDolDb::getInstance()->getPairs('SELECT `object`, `title` FROM `sys_objects_menu` WHERE `template_id` IN (' . $this->implode_escape($mixedTemplateIds) . ')', 'object', 'title');
+    }
+
+    function getBlockSubmenus($mixedTemplateIds = array(25, 26))
+    {
+        if(!is_array($mixedTemplateIds))
+            $mixedTemplateIds = array($mixedTemplateIds);
+        
+        return BxDolDb::getInstance()->getPairs('SELECT `object`, `title` FROM `sys_objects_menu` WHERE `template_id` IN (' . $this->implode_escape($mixedTemplateIds) . ')', 'object', 'title');
+    }
+
+    function getLayouts($aParams, &$aItems, $bReturnCount = true)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sSelectClause = $sJoinClause = $sWhereClause = $sGroupClause = $sOrderClause = $sLimitClause = "";
+
+        if(!isset($aParams['order']) || empty($aParams['order']))
+           $sOrderClause = "ORDER BY `tpl`.`id` ASC";
+
+        switch($aParams['type']) {
+            case 'by_id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                	'id' => $aParams['value']
+                );
+
+                $sWhereClause = "AND `tpl`.`id`=:id ";
+                break;
+
+            case 'all':
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT " . ($bReturnCount ? "SQL_CALC_FOUND_ROWS" : "") . "
+                `tpl`.`id` AS `id`,
+                `tpl`.`name` AS `name`,
+                `tpl`.`icon` AS `icon`,
+                `tpl`.`title` AS `title`,
+                `tpl`.`template` AS `template`,
+                `tpl`.`cells_number` AS `cells_number`" . $sSelectClause . "
+            FROM `sys_pages_layouts` AS `tpl` " . $sJoinClause . "
+            WHERE 1 " . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause . " " . $sLimitClause;
+        $aItems = call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+
+        if(!$bReturnCount)
+            return !empty($aItems);
+
+        return (int)$this->getOne("SELECT FOUND_ROWS()");
+    }
+
+    function getDesignBoxes($aParams, &$aItems, $bReturnCount = true)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sSelectClause = $sJoinClause = $sWhereClause = $sGroupClause = $sOrderClause = $sLimitClause = "";
+
+        if(!isset($aParams['order']) || empty($aParams['order']))
+           $sOrderClause = "ORDER BY `tpd`.`order` ASC";
+
+        switch($aParams['type']) {
+            case 'by_id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                	'id' => $aParams['value']
+                );
+
+                $sWhereClause = "AND `tpd`.`id`=:id ";
+                break;
+
+            case 'ordered':
+                $sWhereClause = "AND `tpd`.`order`<>0 ";
+                break;
+
+            case 'all':
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT " . ($bReturnCount ? "SQL_CALC_FOUND_ROWS" : "") . "
+                `tpd`.`id` AS `id`,
+                `tpd`.`title` AS `title`,
+                `tpd`.`template` AS `template`" . $sSelectClause . "
+            FROM `sys_pages_design_boxes` AS `tpd` " . $sJoinClause . "
+            WHERE 1 " . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause . " " . $sLimitClause;
+        $aItems = call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+
+        if(!$bReturnCount)
+            return !empty($aItems);
+
+        return (int)$this->getOne("SELECT FOUND_ROWS()");
+    }
+
+    function getModulesWithCopyableBlocks()
+    {
+        $aTypes = [BX_DOL_MODULE_TYPE_MODULE, BX_DOL_MODULE_TYPE_TEMPLATE];
+
+    	$sSql = $this->prepare("SELECT
+                `tm`.`name` AS `module`
+            FROM `sys_modules` AS `tm`
+            LEFT JOIN `sys_pages_blocks` AS `tpb` ON `tm`.`name`=`tpb`.`module`
+            WHERE `tm`.`type` IN (" . $this->implode_escape($aTypes) . ") AND `tpb`.`copyable`=?
+            GROUP BY `tm`.`name`", 1);
+    	return $this->getColumn($sSql);
+    }
+
+    function getBlocks($aParams, &$aItems, $bReturnCount = true)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sSelectClause = "
+            `tpb`.`id` AS `id`,
+            `tpb`.`object` AS `object`,
+            `tpb`.`cell_id` AS `cell_id`,
+            `tpb`.`module` AS `module`,
+            `tpb`.`title_system` AS `title_system`,
+            `tpb`.`title` AS `title`,
+            `tpb`.`designbox_id` AS `designbox_id`,
+            `tpb`.`class` AS `class`,
+            `tpb`.`async` AS `async`,
+            `tpb`.`cache_lifetime` AS `cache_lifetime`,
+            `tpb`.`submenu` AS `submenu`,
+            `tpb`.`tabs` AS `tabs`,
+            `tpb`.`hidden_on` AS `hidden_on`,
+            `tpb`.`visible_for_levels` AS `visible_for_levels`,
+            `tpb`.`type` AS `type`,
+            `tpb`.`content` AS `content`,
+            `tpb`.`content_empty` AS `content_empty`,
+            `tpb`.`help` AS `help`,
+            `tpb`.`deletable` AS `deletable`,
+            `tpb`.`copyable` AS `copyable`,
+            `tpb`.`active` AS `active`,
+            `tpb`.`active_api` AS `active_api`,
+            `tpb`.`order` AS `order`
+        ";
+        $sJoinClause = $sWhereClause = $sGroupClause = $sOrderClause = $sLimitClause = "";
+
+        if(!isset($aParams['order']) || empty($aParams['order']))
+           $sOrderClause = "ORDER BY `tpb`.`order` ASC";
+
+        switch($aParams['type']) {
+            case 'by_id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'id' => $aParams['value']
+                );
+
+                $sWhereClause = "AND `tpb`.`id`=:id ";
+                break;
+            
+            case 'by_fields':
+                $aMethod['name'] = 'getRow';
+
+                $sWhereClause = "AND " . $this->arrayToSQL($aParams['fields'], ' AND ');
+                break;
+
+            case 'skeleton_by_type':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                    'module' => BX_DOL_STUDIO_BP_SKELETONS,
+                    'type' => $aParams['value']
+                );
+
+                $sWhereClause = "AND `tpb`.`object`='' AND `tpb`.`module`=:module AND `tpb`.`type`=:type ";
+                break;
+
+            case 'by_ids':
+                $sWhereClause = "AND `tpb`.`id` IN (" . $this->implode_escape($aParams['value']) . ")";
+                break;
+
+            case 'by_object':
+            	$aMethod['params'][1] = array(
+                    'object' => $aParams['value']
+                );
+
+                $sWhereClause = "AND `tpb`.`object`=:object";
+                break;
+            
+            case 'export_by_object':
+                $aMethod['params'][1] = [
+                    'object' => $aParams['value']
+                ];
+
+                $sSelectClause = "`tpb`.*";
+                $sWhereClause = "AND `tpb`.`object`=:object";
+                break;
+
+            case 'by_object_cell':
+            	$aMethod['params'][1] = array(
+                    'object' => $aParams['object'],
+                    'cell_id' => $aParams['cell']
+                );
+
+                $sWhereClause = "AND `tpb`.`object`=:object AND `tpb`.`cell_id`=:cell_id";
+                break;
+
+            case 'by_module_to_copy':
+            	$aMethod['params'][1] = array(
+                    'module' => $aParams['value']
+                );
+
+                $sWhereClause = "AND `tpb`.`module`=:module AND `tpb`.`copyable`=1";
+
+                if($aParams['value'] == BX_DOL_STUDIO_BP_SKELETONS)
+                    $sWhereClause .= " AND `tpb`.`object`=''";
+                break;
+
+            case 'counter_by_pages':
+                $aMethod['name'] = 'getPairs';
+                $aMethod['params'][1] = 'object';
+                $aMethod['params'][2] = 'counter';
+                $sSelectClause .= ", COUNT(*) AS `counter`";
+                $sGroupClause = "GROUP BY `tpb`.`object`";
+                break;
+
+            case 'all':
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT " . ($bReturnCount ? "SQL_CALC_FOUND_ROWS" : "") . $sSelectClause . "
+            FROM `sys_pages_blocks` AS `tpb` " . $sJoinClause . "
+            WHERE 1 " . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause . " " . $sLimitClause;
+        $aItems = call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+
+        if(!$bReturnCount)
+            return !empty($aItems);
+
+        return (int)$this->getOne("SELECT FOUND_ROWS()");
+    }
+
+    function isBlockExists($aFields)
+    {
+        $aBlock = [];
+        $this->getBlocks(['type' => 'by_fields', 'fields' => $aFields], $aBlock, false);
+
+        return !empty($aBlock) && is_array($aBlock);
+    }
+            
+    function insertBlock($aData)
+    {
+        $aData['order'] = $this->getBlockOrderMax($aData['object']) + 1;
+
+        return $this->query("INSERT INTO `sys_pages_blocks` SET " . $this->arrayToSQL($aData)) ? $this->lastId() : false;
+    }
+
+    function updateBlock($iId, $aData)
+    {
+        return $this->query("UPDATE `sys_pages_blocks` SET " . $this->arrayToSQL($aData) . " WHERE `id`=:id", [
+            'id' => $iId
+        ]) ? true : false;
+    }
+    
+    function updateBlockByFields($aFields, $aData)
+    {
+        return $this->query("UPDATE `sys_pages_blocks` SET " . $this->arrayToSQL($aData) . " WHERE " . $this->arrayToSQL($aFields, ' AND ')) ? true : false;
+    }
+    
+    function updateBlocks($aParamsSet, $aParamsWhere = [])
+    {
+        if(empty($aParamsSet))
+            return false;
+
+        $sWhereClause = "1";
+        if(!empty($aParamsWhere))
+            $sWhereClause = $this->arrayToSQL($aParamsWhere, " AND ");
+
+        return $this->query("UPDATE `sys_pages_blocks` SET " . $this->arrayToSQL($aParamsSet) . " WHERE " . $sWhereClause);
+    }
+
+    function deleteBlocks($aParams)
+    {
+    	$aBindings = array();
+        $sWhereClause = "";
+        $aBlockIds = array();
+        switch($aParams['type']) {
+            case 'by_id':
+            	$aBindings = array(
+                	'id' => $aParams['value']
+                );
+
+                $sWhereClause = "AND `tpb`.`id`=:id ";
+                $aBlockIds[] = $aParams['value'];
+                break;
+
+            case 'by_object':
+            	$aBindings = array(
+                	'object' => $aParams['value']
+                );
+
+                $sWhereClause = "AND `tpb`.`object`=:object ";
+                $aBlockIds = $this->getColumn("SELECT `id` FROM `sys_pages_blocks` AS `tpb` WHERE 1 " . $sWhereClause, $aBindings);
+                break;
+
+            default:
+                return false;
+        }
+
+        $sSql = "DELETE FROM `tpb` USING `sys_pages_blocks` AS `tpb` WHERE 1 " . $sWhereClause;
+        $b = ((int)$this->query($sSql, $aBindings) > 0);
+        if ($b && $aBlockIds)
+            BxDolWiki::onBlockDelete($aBlockIds);
+        return $b;
+    }
+
+    function resetBlocksByPage($sObject, $iCellId, $bDeactivate = false)
+    {
+        $aSetClause = array('cell_id' => 0);
+        if($bDeactivate)
+            $aSetClause['active'] = 0;
+
+        return $this->query("UPDATE `sys_pages_blocks` SET " . $this->arrayToSQL($aSetClause) . " WHERE `object`=:object AND `cell_id`>:cell_id", array(
+            'object' => $sObject,
+            'cell_id' => $iCellId
+        ));
+    }
+
+    function getBlockOrderMax($sObject, $iCellId = 1)
+    {
+        $sSql = $this->prepare("SELECT MAX(`order`) FROM `sys_pages_blocks` WHERE `object`=? AND `cell_id`=? LIMIT 1", $sObject, $iCellId);
+        return (int)$this->getOne($sSql);
+    }
+
+    function deleteImage($aParams)
+    {
+        $sWhereClause = "";
+
+        switch($aParams['type']) {
+            case 'by_id':
+            	$aMethod['params'][1] = array(
+                	'id' => $aParams['value']
+                );
+
+                $sWhereClause = "AND `tpb`.`id`=:id ";
+                break;
+
+            case 'all':
+                break;
+        }
+
+        $sSql = "DELETE FROM `tpb` USING `sys_pages_blocks` AS `tpb` WHERE 1 " . $sWhereClause;
+        return (int)$this->query($sSql) > 0;
+    }
+
+    function getMenus($bFull = false)
+    {
+        $sSql = "SELECT * FROM `sys_objects_menu` WHERE 1 ORDER BY `module`, `object`";
+        if($bFull)
+            return $this->getAllWithKey($sSql, 'object');
+        else
+            return $this->getPairs($sSql, 'object', 'title');
+    }
+}
+
+/** @} */

+ 2008 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/studio/template/scripts/BxBaseStudioBuilderPage.php

@@ -0,0 +1,2008 @@
+<?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',
+        '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',
+            'page_layouts.css', 
+            'builder_page.css'
+        ));
+    }
+
+    function getPageJs()
+    {
+        return array_merge(parent::getPageJs(), array(
+            'codemirror/codemirror.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 = array(), $aMarkers = array())
+    {
+        $aMenuItems = array(
+            BX_DOL_STUDIO_MODULE_SYSTEM => array(
+                'name' => BX_DOL_STUDIO_MODULE_SYSTEM,
+                'icon' => 'cog',
+            ),
+            BX_DOL_STUDIO_MODULE_CUSTOM => array(
+                'name' => BX_DOL_STUDIO_MODULE_CUSTOM,
+                'icon' => 'wrench',
+                'title' => '_adm_bp_cpt_type_' . BX_DOL_STUDIO_MODULE_CUSTOM,
+            )
+        );
+
+        $aModulesDb = BxDolModuleQuery::getInstance()->getModulesBy(array('type' => 'type', 'value' => array(BX_DOL_MODULE_TYPE_MODULE, BX_DOL_MODULE_TYPE_TEMPLATE)));
+        foreach($aModulesDb as $aModuleDb) {
+            $sName = $aModuleDb['name'];
+
+            if(!empty($aMenuItems[$sName]))
+                $aMenuItems[$sName] = array_merge($aMenuItems[$sName], $aModuleDb);
+            else
+                $aMenuItems[$sName] = $aModuleDb;
+
+            $aMenuItems[$sName]['title'] = BxDolStudioUtils::getModuleTitle($sName);
+
+            if(empty($aMenuItems[$sName]['icon']))
+                $aMenuItems[$sName]['icon'] = BxDolStudioUtils::getModuleIcon($aModuleDb, 'menu', false); 
+        }
+
+        $aMenu = array();
+        foreach($aMenuItems as $aMenuItem)
+            $aMenu[] = array(
+                'name' => $aMenuItem['name'],
+                'icon' => $aMenuItem['icon'],
+                'link' =>  sprintf($this->sTypeUrl, $aMenuItem['name']),
+                'title' => $aMenuItem['title'],
+                '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 = !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(array('type' => 'by_uri', 'value' => $sUri));
+            if(!empty($aPage) && is_array($aPage)) 
+            	return array('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']);
+
+            $iId = (int)$oForm->insert(array('author' => bx_get_logged_profile_id(), 'added' => time(), 'object' => $sObject, 'url' => $this->sPageBaseUrl . $sUri));
+            if($iId != 0)
+                return array('eval' => $sJsObject . '.onCreatePage(\'' . $sModule . '\', \'' . $sObject . '\')');
+            else
+                return array('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']);
+            }
+
+            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 = array(
+            BX_DOL_STUDIO_BP_SKELETONS => array(
+                'name' => BX_DOL_STUDIO_BP_SKELETONS,
+                'icon' => 'qrcode',
+                'title' => '_sys_block_types_skeletons',
+                'selected' => $sSelected == BX_DOL_STUDIO_BP_SKELETONS,
+            ),
+            BX_DOL_STUDIO_MODULE_SYSTEM => array(
+                'name' => BX_DOL_STUDIO_MODULE_SYSTEM,
+                'icon' => 'cog',
+                'title' => '_sys_block_types_system',
+                'selected' => $sSelected == BX_DOL_STUDIO_MODULE_SYSTEM,
+            ),
+            BX_DOL_STUDIO_MODULE_CUSTOM => array(
+                'name' => BX_DOL_STUDIO_MODULE_CUSTOM,
+                'icon' => 'wrench',
+                '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(array('template' => 'menu_side.html', 'menu_items' => $aMenu));
+        $oMenu->setInlineIcons(false);
+
+        $aTmplParams = array(
+            'menu' => $oMenu->getCode(),
+            'html_block_lists_id' => $this->aHtmlIds['block_lists_id'],
+            'blocks' => $this->getBlockList($sSelected)
+        );
+
+        $aForm['inputs']['blocks']['content'] = $oTemplate->parseHtmlByName('bp_add_block_form.html', $aTmplParams);
+
+        $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'])), $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_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:
+                $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_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 = array(
+            'type' => 'checkbox',
+            'name' => 'blocks[]',
+            'attrs' => array(
+                'onChange' => 'javascript:' . $sJsObject . '.onSelectBlock(this);'
+            ),
+            'value' => ''
+        );
+
+        $aTmplParams = array(
+            'html_block_list_id' => $this->aHtmlIds['block_list_id'] . $sModule,
+            'bx_repeat:blocks' => array()
+        );
+
+        $aBlocks = $this->getBlocks($sModule);
+        foreach($aBlocks as $aBlock) {
+        	list($sIcon, $sIconUrl) = $this->getBlockIcon($aBlock);
+
+            $aInputCheckbox['value'] = $aBlock['id'];
+
+            $aTmplParams['bx_repeat:blocks'][] = array(
+                'js_object' => $sJsObject,
+                'html_id' => $this->aHtmlIds['block_id'] . $aBlock['id'],
+                'bx_if:is_inactive' => array(
+                    'condition' => false,
+                    'content' => array()
+                ),
+                'bx_if:show_link' => array(
+                    'condition' => false,
+                    'content' => array()
+                ),
+                'bx_if:show_text' => array(
+                    'condition' => true,
+                    'content' => array(
+                        'title' => _t(!empty($aBlock['title_system']) ? $aBlock['title_system'] : $aBlock['title']),
+                    )
+                ),
+                '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' => true,
+                    'content' => array(
+                        'content' => $oForm->genRow($aInputCheckbox)
+                    )
+                ),
+                'bx_if:show_drag_handle' => array(
+                    'condition' => false,
+                    'content' => array()
+                )
+            );
+        }
+
+        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;
+    }
+}
+
+/** @} */

+ 51 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/css/editor.quill.css

@@ -0,0 +1,51 @@
+.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);
+}

+ 305 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseAccountForms.php

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

+ 96 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseCaptchaHCaptcha.php

@@ -0,0 +1,96 @@
+<?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
+ * @{
+ */
+
+/**
+ * reCAPTCHA representation.
+ * @see BxDolCaptcha
+ */
+class BxBaseCaptchaHCaptcha extends BxDolCaptcha
+{
+    protected $_sSkin;
+    protected $sApiUrl;
+    protected $sVerifyUrl;
+
+    public function __construct ($aObject, $oTemplate)
+    {
+        parent::__construct ($aObject, $oTemplate);
+		
+        $this->_sSkin = 'light';
+        $this->sApiUrl = 'https://js.hcaptcha.com/1/api.js';
+        $this->sVerifyUrl = 'https://hcaptcha.com/siteverify';
+    }
+
+    /**
+     * Display captcha.
+     */
+    public function display ($bDynamicMode = false)
+    {
+       $sCode = '';
+        $aApiParams = array();
+        if($bDynamicMode)  {
+        	$sPostfix = $this->_sObject;
+        	$sId = 'sys-captcha-' . $sPostfix;
+
+        	$sOnLoadFunction = 'onLoadCallback' . $sPostfix;
+        	$sOnLoadCode = "
+	        	var " . $sOnLoadFunction . " = function() {
+					hcaptcha.render('" . $sId . "', {
+						'sitekey': '" . $this->_sKeyPublic . "',
+						'theme': '" . $this->_sSkin . "'
+					});
+				};
+		        ";
+
+        	$aApiParams = array(
+        		'onload' => $sOnLoadFunction,
+        		'render' => 'explicit'
+        	);
+
+        	$sCode .= $this->_oTemplate->_wrapInTagJsCode($sOnLoadCode);
+        	$sCode .= '<div id="' . $sId . '"></div>';
+        }
+        else {
+        	$aApiParams = array(
+        		'render' => 'onload'
+        	);
+
+        	$sCode .= '<div class="h-captcha" data-sitekey="' . $this->_sKeyPublic . '" data-theme="' . $this->_sSkin . '"></div>';
+        }
+
+        $aApiParams['hl'] = BxDolLanguages::getInstance()->getCurrentLanguage();
+        $sCodeJs = $this->_oTemplate->addJs(bx_append_url_params($this->sApiUrl, $aApiParams), true);
+
+        return $sCodeJs . $sCode;
+    }
+
+    /**
+     * Check captcha.
+     */
+    public function check ()
+    {
+    	$mixedResponce = bx_file_get_contents($this->sVerifyUrl, array(
+    		'secret' => $this->_sKeyPrivate, 
+    		'response' => bx_process_input(bx_get('h-captcha-response'))
+    	), 'post');
+	
+    	if($mixedResponce === false)
+    		return false;
+
+    	$aResponce = json_decode($mixedResponce, true); 	
+    	if(isset($aResponce['success']) && $aResponce['success'] === true)
+    		return true;
+
+		if(!empty($aResponce['error-codes']))
+			$this->_error = $aResponce['error-codes'];
+
+		return false;
+    }
+}
+
+/** @} */

+ 96 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseCaptchaReCAPTCHANew.php

@@ -0,0 +1,96 @@
+<?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
+ * @{
+ */
+
+/**
+ * reCAPTCHA representation.
+ * @see BxDolCaptcha
+ */
+class BxBaseCaptchaReCAPTCHANew extends BxDolCaptcha
+{
+    protected $_sSkin;
+    protected $sApiUrl;
+    protected $sVerifyUrl;
+
+    public function __construct ($aObject, $oTemplate)
+    {
+        parent::__construct ($aObject, $oTemplate);
+		
+        $this->_sSkin = 'light';
+        $this->sApiUrl = 'https://www.google.com/recaptcha/api.js';
+        $this->sVerifyUrl = 'https://www.google.com/recaptcha/api/siteverify';
+    }
+
+    /**
+     * Display captcha.
+     */
+    public function display ($bDynamicMode = false)
+    {
+        $sCode = '';
+        $aApiParams = array();
+        if($bDynamicMode)  {
+        	$sPostfix = $this->_sObject;
+        	$sId = 'sys-captcha-' . $sPostfix;
+
+        	$sOnLoadFunction = 'onLoadCallback' . $sPostfix;
+        	$sOnLoadCode = "
+	        	var " . $sOnLoadFunction . " = function() {
+					grecaptcha.render('" . $sId . "', {
+						'sitekey': '" . $this->_sKeyPublic . "',
+						'theme': '" . $this->_sSkin . "'
+					});
+				};
+		        ";
+
+        	$aApiParams = array(
+        		'onload' => $sOnLoadFunction,
+        		'render' => 'explicit'
+        	);
+
+        	$sCode .= $this->_oTemplate->_wrapInTagJsCode($sOnLoadCode);
+        	$sCode .= '<div id="' . $sId . '"></div>';
+        }
+        else {
+        	$aApiParams = array(
+        		'render' => 'onload'
+        	);
+
+        	$sCode .= '<div class="g-recaptcha" data-sitekey="' . $this->_sKeyPublic . '" data-theme="' . $this->_sSkin . '"></div>';
+        }
+
+        $aApiParams['hl'] = BxDolLanguages::getInstance()->getCurrentLanguage();
+        $sCodeJs = $this->_oTemplate->addJs(bx_append_url_params($this->sApiUrl, $aApiParams), true);
+
+        return $sCodeJs . $sCode;
+    }
+
+    /**
+     * Check captcha.
+     */
+    public function check ()
+    {
+    	$mixedResponce = bx_file_get_contents($this->sVerifyUrl, array(
+    		'secret' => $this->_sKeyPrivate, 
+    		'response' => bx_process_input(bx_get('g-recaptcha-response')),
+    		'remoteip' => getVisitorIP()
+    	));
+    	if($mixedResponce === false)
+    		return false;
+
+    	$aResponce = json_decode($mixedResponce, true); 	
+    	if(isset($aResponce['success']) && $aResponce['success'] === true)
+    		return true;
+
+		if(!empty($aResponce['error-codes']))
+			$this->_error = $aResponce['error-codes'];
+
+		return false;
+    }
+}
+
+/** @} */

+ 168 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseCategory.php

@@ -0,0 +1,168 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * Category objects representation.
+ * @see BxDolCategory
+ */
+class BxBaseCategory extends BxDolCategory
+{
+    protected $_oTemplate;
+
+    protected $_oModule;
+    protected $_bModule;
+
+    protected $_sBrowseAllUrl;
+    protected $_sBrowseAllTitle;
+    protected $_sBrowseUrl;
+
+    public function __construct ($aObject, $oTemplate = null)
+    {
+        parent::__construct ($aObject);
+
+        if ($oTemplate)
+            $this->_oTemplate = $oTemplate;
+        else
+            $this->_oTemplate = BxDolTemplate::getInstance();
+
+        $this->_oModule = null;
+        if(!empty($this->_aObject['module']))
+            $this->_oModule = BxDolModule::getInstance($this->_aObject['module']);
+        $this->_bModule = $this->_oModule !== null;
+
+        $this->_sBrowseAllUrl = '';
+        $this->_sBrowseAllTitle = '_all';
+
+        $this->_sBrowseUrl = bx_append_url_params('searchKeyword.php', [
+            'cat' => '{category}',
+            'keyword' => '{keyword}'
+        ], true, ['{category}', '{keyword}']) . '{sections}' . '{context}';
+
+        if($this->_bModule) {
+            $CNF = &$this->_oModule->_oConfig->CNF;
+
+            if(!empty($CNF['URL_HOME']))
+                $this->_sBrowseAllUrl = BxDolPermalinks::getInstance()->permalink($CNF['URL_HOME']);
+
+            if(!isset($CNF['T']['txt_all_categories'])) {
+                $sBrowseAllTitleKey = '_' . $this->_oModule->getName() . '_txt_all_categories';
+                if(strcmp($sBrowseAllTitleKey, _t($sBrowseAllTitleKey)) != 0)
+                    $this->_sBrowseAllTitle = $sBrowseAllTitleKey;
+            }
+            else
+                $this->_sBrowseAllTitle = $CNF['T']['txt_all_categories'];
+
+            if(!empty($CNF['URL_CATEGORY']))
+                $this->_sBrowseUrl = bx_append_url_params(BxDolPermalinks::getInstance()->permalink($CNF['URL_CATEGORY']), [
+                    'category' => '{keyword}'
+                ], true, ['{keyword}']);
+        }
+    }
+
+    public function getCategoryIcon($sValue)
+    {
+        return 'star';
+    }
+            
+    public function getCategoryTitle($sValue)
+    {
+    	$a = BxDolForm::getDataItems($this->_aObject['list_name']);
+        if (!$a || !isset($a[$sValue]))
+            return '';
+
+        return $a[$sValue];
+    }
+
+    public function getCategoryUrl($sValue, $aParams = [])
+    {
+        $s = BX_DOL_URL_ROOT . bx_replace_markers($this->_sBrowseUrl, [
+            'category' => rawurlencode($this->getObjectName()),
+            'keyword' => rawurlencode($sValue),
+            'sections' => $this->_aObject['search_object'] ? '&section[]=' . rawurlencode($this->_aObject['search_object']) : '',
+            'context' => isset($aParams['context_id']) ? '&context_id=' . $aParams['context_id'] : ''
+        ]);
+
+        if (bx_is_api())
+            return bx_api_get_relative_url($s);
+
+        return $s;
+    }
+
+    /**
+     * Get link to list all items with the same category
+     * @param $sName category title
+     * @param $sValue category value
+     * @return category name wrapped with A tag
+     */
+    public function getCategoryLink($sName, $sValue)
+    {
+        $sUrl = $this->getCategoryUrl($sValue);
+        return '<a class="bx-category-link" href="' . $sUrl . '">' . $sName . '</a>';
+    }
+
+    /**
+     * Get all categories list
+     * @param $bDisplayEmptyCats display categories with no items, true by default
+     * @return categories list html
+     */
+    public function getCategoriesList($bDisplayEmptyCats = true, $bAsArray = false)
+    {
+        $aContextInfo = bx_get_page_info();
+
+        $mProfileContextId = false;
+        if ($aContextInfo !== false)
+            $mProfileContextId = $aContextInfo['context_profile_id'];
+
+        $a = BxDolForm::getDataItems($this->_aObject['list_name']);
+        if (!$a)
+            return $bAsArray ? [] : '';
+
+        $aVars = [
+            'bx_repeat:cats' => [],
+            'bx_if:show_all' => [
+                'condition' => $this->_sBrowseAllUrl != '',
+                'content' => [
+                    'url' => $this->_sBrowseAllUrl,
+                    'name' => _t($this->_sBrowseAllTitle)
+                ]
+            ]
+        ];
+
+        foreach ($a as $sValue => $sName) {
+            if (!is_numeric($sValue) && !$sValue)
+                continue;
+            
+            $iNum = $this->getItemsNum($sValue, ['context_id' => $mProfileContextId]);
+            if (!$bDisplayEmptyCats && !$iNum)
+                continue;
+            
+            $aVars['bx_repeat:cats'][] = [
+                'url' => $this->getCategoryUrl($sValue, ($mProfileContextId ? ['context_id' => $mProfileContextId] : [])),
+                'name' => $sName,
+                'value' => $sValue,
+                'num' => $iNum,
+                'icon' => $this->getCategoryIcon($sValue),
+                'selected_class' => $sValue == bx_get('category') ? 'bx-category-list-item-selected' : '',
+            ];
+        }
+
+        if(bx_is_api())
+            return [bx_api_get_block('categories_list',  $aVars['bx_repeat:cats'])];
+
+        if ($bAsArray)
+            return $aVars;
+
+        if (!$aVars['bx_repeat:cats'])
+            return '';
+
+        return $this->_oTemplate->parseHtmlByName('category_list.html', $aVars);
+    }
+}
+
+/** @} */

+ 678 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseCmtsServices.php

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

+ 412 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseMenu.php

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

+ 40 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseMenuSubmenuWithAddons.php

@@ -0,0 +1,40 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+
+/**
+ * Menu representation.
+ * @see BxDolMenu
+ */
+class BxBaseMenuSubmenuWithAddons extends BxTemplMenu
+{
+    public function __construct ($aObject, $oTemplate)
+    {
+       
+        parent::__construct ($aObject, $oTemplate);
+        $this->setDisplayAddons(true);
+    }
+    
+    protected function _getTmplVarsAddon($mixedAddon, $aMenuItem)
+    {
+        $sAddonF = '';
+        if(!empty($mixedAddon))
+            $sAddonF = $this->_oTemplate->parseHtmlByTemplateName('menu_item_addon', array(
+                'content' => $mixedAddon
+            ));
+
+        return array(
+            'addon' => $mixedAddon,
+            'addonf' => $sAddonF
+        );
+    }
+}
+
+/** @} */

+ 1171 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBasePage.php

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

+ 112 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBasePageHome.php

@@ -0,0 +1,112 @@
+<?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
+ * @{
+ */
+
+/**
+ * Homepage.
+ */
+class BxBasePageHome extends BxTemplPage
+{
+    public function __construct($aObject, $oTemplate)
+    {
+        parent::__construct($aObject, $oTemplate);
+      
+
+        $aCover = $this->getPageCoverImage();
+
+        $bTmplVarsCover = !empty($aCover['id']);
+        $aTmplVarsCover = $bTmplVarsCover ? array('image_url' => BxDolTranscoder::getObjectInstance($aCover['transcoder'])->getFileUrlById($aCover['id'])) : array();
+
+        BxDolCover::getInstance()->set(array(
+            'class' => 'bx-cover-homepage',
+            'title' => _t('_sys_txt_homepage_cover', bx_absolute_url(BxDolPermalinks::getInstance()->permalink('page.php?i=create-account'))),
+            'bx_if:empty_cover_class' => array (
+                'condition' => !$bTmplVarsCover,
+                'content' => array(),
+            ),
+            'bx_if:bg' => array (
+                'condition' => $bTmplVarsCover,
+                'content' => $aTmplVarsCover,
+            ),
+        ), 'cover_home.html');
+        
+        $sSelName = 'home';
+        if(bx_get('i') !== false)
+            $sSelName = bx_process_input(bx_get('i'));
+
+        BxDolMenu::setSelectedGlobal('system', $sSelName);
+    }
+
+    public function getCode ()
+    {
+        $s = parent::getCode ();
+        if (isAdmin() && getParam('site_tour_home') == 'on')
+            $s .= $this->_oTemplate->parseHtmlByName('homepage_tour.html', array());
+
+        return $s;
+    }
+
+    protected function _getBlockRaw($aBlock)
+    {
+        if(strpos($aBlock['title'], 'splash') !== false) {
+            $oPermalink = BxDolPermalinks::getInstance();
+
+            $sJoinForm = $sLoginForm = '';
+            if(!isLogged()) {
+                $sJoinForm = BxDolService::call('system', 'create_account_form', array(), 'TemplServiceAccount');
+                $sLoginForm = BxDolService::call('system', 'login_form', array(), 'TemplServiceLogin');
+            }
+
+            $oTemplate = BxDolTemplate::getInstance();
+            $oTemplate->addJs(array('lottie.min.js'));
+            $aBlock['content'] = $oTemplate->parseHtmlByContent($aBlock['content'], array(
+                'join_link' => bx_absolute_url($oPermalink->permalink('page.php?i=create-account')),
+                'join_form' => $sJoinForm,
+                'join_form_in_box' => !empty($sJoinForm) ? DesignBoxContent(_t('_sys_txt_splash_join'), $sJoinForm, BX_DB_PADDING_DEF) : '',
+                'login_link' => bx_absolute_url($oPermalink->permalink('page.php?i=login')),
+                'login_form' => $sLoginForm,
+                'login_form_in_box' => !empty($sLoginForm) ? DesignBoxContent(_t('_sys_txt_splash_login'), $sLoginForm, BX_DB_PADDING_DEF) : '',
+            ));
+        }
+
+        return parent::_getBlockRaw($aBlock);
+    }
+
+    protected function _addJsCss()
+    {
+        parent::_addJsCss();
+        if (isAdmin()) {
+            $this->_oTemplate->addJs(array(
+                'shepherd/js/shepherd.min.js',
+            ));
+            $this->_oTemplate->addCss(array(
+                'homepage.css',
+                BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'shepherd/css/|shepherd.css'
+            ));
+        }
+    }
+
+    protected function _getPageMetaImage()
+    {
+        $iImage = 0;
+        foreach(['icon_apple', 'icon_android', 'icon_android_splash'] as $sIcon)
+            if(($iImage = (int)getParam('sys_site_' . $sIcon)) != 0)
+                break;
+
+        if(empty($iImage))
+            return '';
+
+        $oStorage = BxDolStorage::getObjectInstance(BX_DOL_STORAGE_OBJ_IMAGES);
+        if(!$oStorage)
+            return '';
+
+        return $oStorage->getFileUrlById($iImage);
+    }
+}
+
+/** @} */

+ 596 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseReport.php

@@ -0,0 +1,596 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * @see BxDolReport
+ */
+class BxBaseReport extends BxDolReport
+{
+    protected $_bCssJsAdded;
+
+    protected $_sJsObjClass;
+    protected $_sJsObjName;
+    protected $_sStylePrefix;
+
+    protected $_aHtmlIds;
+
+    protected $_aElementDefaults;
+    protected $_aElementDefaultsApi;
+    protected $_aElementParamsApi; //--- Params from DefaultsApi array to be passed to Api
+
+    public function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
+    {
+        parent::__construct($sSystem, $iId, $iInit, $oTemplate);
+
+        $this->_bCssJsAdded = false;
+
+        $this->_sJsObjClass = 'BxDolReport';
+        $this->_sJsObjName = 'oReport' . bx_gen_method_name($sSystem, array('_' , '-')) . $iId;
+        $this->_sStylePrefix = 'bx-report';
+
+        $sHtmlId = str_replace(array('_' , ' '), array('-', '-'), $sSystem) . '-' . $iId;
+        $this->_aHtmlIds = array(
+            'main' => 'bx-report-' . $sHtmlId,
+            'counter' => 'bx-report-counter-' . $sHtmlId,
+            'do_link' => 'bx-report-do-link-' . $sHtmlId,
+            'do_popup' => 'bx-report-do-popup-' . $sHtmlId,
+            'do_form' => 'bx-report-do-form-' . $sHtmlId,
+            'by_popup' => 'bx-report-by-popup-' . $sHtmlId
+        );
+
+        $this->_aElementDefaults = array(
+            'show_do_report_as_button' => false,
+            'show_do_report_as_button_small' => false,
+            'show_do_report_icon' => true,
+            'show_do_report_label' => false,
+            'show_counter' => true,
+            'show_counter_only' => true
+        );
+        if($this->_bApi) {
+            $aTypes = BxDolFormQuery::getDataItems($this->_sTypesPreList);
+
+            $this->_aElementDefaultsApi = array_merge($this->_aElementDefaults, [
+                'show_counter' => true,
+                'types' => array_map(function($sType) use ($aTypes) {
+                    return ['name' => $sType, 'title' => $aTypes[$sType]];
+                }, $this->_aTypes)
+            ]);
+            $this->_aElementParamsApi = ['is_reported', 'types'];
+        }
+
+        $this->_sTmplContentElementBlock = $this->_oTemplate->getHtml('report_element_block.html');
+        $this->_sTmplContentElementInline = $this->_oTemplate->getHtml('report_element_inline.html');
+        $this->_sTmplContentDoAction = $this->_oTemplate->getHtml('report_do_report.html');
+        $this->_sTmplContentDoActionLabel = $this->_oTemplate->getHtml('report_do_report_label.html');
+    }
+
+    public function report($aParams = [])
+    {
+        if (!$this->isEnabled())
+            return ['code' => 1, 'message' => _t('_report_err_not_enabled')];
+
+        if(!$this->isAllowedReport())
+            return ['code' => 2, 'message' => $this->msgErrAllowedReport()];
+
+        $iAuthorId = $this->_getAuthorId();
+        $iAuthorNip = bx_get_ip_hash($this->_getAuthorIp());
+
+        $iObjectId = $this->_iId;
+        $iObjectAuthorId = $this->_oQuery->getObjectAuthorId($iObjectId);
+
+        $bPerformed = $this->isPerformed($iObjectId, $iAuthorId);
+        if($bPerformed) {
+            if(!$this->isUndo())
+                return ['code' => 4, 'message' => _t('_report_err_duplicate_report')];
+
+            if(($iId = $this->_oQuery->putReport($iObjectId, $iAuthorId, true)) !== false) {
+                $this->_trigger();
+
+                bx_alert($this->_sSystem, 'undoReport', $iObjectId, $iAuthorId, array('report_id' => $iId, 'report_author_id' => $iAuthorId, 'object_author_id' => $iObjectAuthorId));
+                bx_alert('report', 'undo', $iId, $iAuthorId, array('object_system' => $this->_sSystem, 'object_id' => $iObjectId, 'object_author_id' => $iObjectAuthorId));
+
+                $aReport = $this->_getReport($iObjectId, true);
+                $aResult = $this->_returnReportData($iObjectId, $iAuthorId, $iId, $aReport, !$bPerformed);
+
+                if(($oSockets = BxDolSockets::getInstance()) && $oSockets->isEnabled())
+                    $oSockets->sendEvent($this->getSocketName(), $iObjectId, 'reported', json_encode($this->_returnReportDataForSocket($aResult)));
+
+                return $aResult;
+            }
+        }
+
+        $oForm = $this->_getFormObject();
+        $oForm->setId($this->_aHtmlIds['do_form']);
+        $oForm->setName($this->_aHtmlIds['do_form']);
+        $oForm->aParams['db']['table'] = $this->_aSystem['table_track'];
+        $oForm->aInputs['sys']['value'] = $this->_sSystem;
+        $oForm->aInputs['object_id']['value'] = $this->_iId;
+        $oForm->aInputs['action']['value'] = 'Report';
+
+        $oForm->initChecker();
+        if($oForm->isSubmittedAndValid() || ($this->_bApi && !empty($aParams) && is_array($aParams))) {
+            $iObjectId = $this->_bApi ? $this->_iId : $oForm->getCleanValue('object_id');
+            $iObjectAuthorId = $this->_oQuery->getObjectAuthorId($iObjectId);
+
+            if(!$this->isAllowedReport(true))
+                return ['code' => 2, 'message' => $this->msgErrAllowedReport()];
+
+            $sType = $this->_bApi ? $aParams['type'] : $oForm->getCleanValue('type');
+            if(!in_array($sType, $this->_aTypes)) 
+                return ['code' => 5, 'message' => _t('_report_err_wrong_type')];
+
+            $sText = $this->_bApi ? $aParams['text'] : $oForm->getCleanValue('text');
+            $sText = bx_process_input($sText, BX_DATA_TEXT_MULTILINE);
+
+            $iId = (int)$oForm->insert(['object_id' => $iObjectId, 'author_id' => $iAuthorId, 'author_nip' => $iAuthorNip, 'type' => $sType, 'text' => $sText,  'date' => time()]);
+            if($iId != 0 && $this->_oQuery->putReport($iObjectId, $iAuthorId)) {
+                if(!empty($this->_sObjectCmts) && ($oCmts = BxDolCmts::getObjectInstance($this->_sObjectCmts, $this->getId()))) {
+                    $sCmtText = '_report_comment';
+                    if(!empty($sText))
+                        $sCmtText = '_report_comment_with_note';
+
+                    $aTypes = BxDolForm::getDataItems('sys_report_types');
+
+                    $oCmts->add(array(
+                        'cmt_author_id' => $iAuthorId,
+                        'cmt_parent_id' => 0,
+                        'cmt_text' => _t($sCmtText, $aTypes[$sType], $sText)
+                    ));
+                }
+
+                $aTemplate = BxDolEmailTemplates::getInstance()->parseTemplate('t_Reported', array(
+                    'report_type' => $sType,
+                    'report_text' => $sText,
+                    'report_url' => $this->getBaseUrl(),
+                ));
+                if($aTemplate)
+                    sendMail(getParam('site_email'), $aTemplate['Subject'], $aTemplate['Body']);
+
+                $mixedParams = $this->_prepareAuditParams($iObjectId, array('type' => $sType, 'text' => $sText));
+                if($mixedParams) {
+                    $sActionName = $mixedParams['action_name'];
+                    $iAuditObjectId = $mixedParams['object_id'];
+                    unset($mixedParams['action_name']);
+                    unset($mixedParams['object_id']);
+                    bx_audit($iAuditObjectId, $this->_sSystem, $sActionName, $mixedParams);
+                }
+
+                $aReport = $this->_getReport($iObjectId, true);
+                
+                $iBlockContentAfter = (int)getParam('sys_security_block_content_after_n_reports');
+                if ($iBlockContentAfter > 0 && $aReport['count'] >= $iBlockContentAfter){
+                    $oModule = BxDolModule::getInstance($this->_aSystem['module_name']);
+                    if($oModule)
+                        $oModule->_oDb->updateStatusAdmin($iObjectId, false);
+                }
+
+                $this->_trigger();
+
+                bx_alert($this->_sSystem, 'doReport', $iObjectId, $iAuthorId, array('report_id' => $iId, 'report_author_id' => $iAuthorId, 'object_author_id' => $iObjectAuthorId, 'type' => $sType, 'text' => $sText));
+                bx_alert('report', 'do', $iId, $iAuthorId, array('object_system' => $this->_sSystem, 'object_id' => $iObjectId, 'object_author_id' => $iObjectAuthorId, 'type' => $sType, 'text' => $sText));
+
+                $aResult = $this->_returnReportData($iObjectId, $iAuthorId, $iId, $aReport, !$bPerformed);
+
+                if(($oSockets = BxDolSockets::getInstance()) && $oSockets->isEnabled())
+                    $oSockets->sendEvent($this->getSocketName(), $iObjectId, 'reported', json_encode($this->_returnReportDataForSocket($aResult)));
+
+                return $aResult;
+            }
+
+            return ['code' => 3, 'message' => _t('_report_err_cannot_perform_action')];
+        }
+
+        $sPopupId = $this->_aHtmlIds['do_popup'];
+        $sPopupContent = BxTemplFunctions::getInstance()->transBox($sPopupId, $this->_oTemplate->parseHtmlByName('report_do_report_form.html', array(
+            'style_prefix' => $this->_sStylePrefix,
+            'js_object' => $this->getJsObjectName(),
+            'form' => $oForm->getCode(),
+            'form_id' => $oForm->id,
+        )));
+
+        return ['popup' => $sPopupContent, 'popup_id' => $sPopupId];
+    }
+
+    public function getJsObjectName()
+    {
+        return $this->_sJsObjName;
+    }
+
+    public function getJsScript($bDynamicMode = false)
+    {
+        $aParams = array(
+            'sObjName' => $this->_sJsObjName,
+            'sSystem' => $this->getSystemName(),
+            'iAuthorId' => $this->_getAuthorId(),
+            'iObjId' => $this->getId(),
+            'sRootUrl' => BX_DOL_URL_ROOT,
+            'sSocket' => $this->getSocketName(),
+            'sStylePrefix' => $this->_sStylePrefix,
+            'aHtmlIds' => $this->_aHtmlIds,
+            'sUnreportConfirm' => bx_js_string(_t('_report_do_unreport_confirm'))
+        );
+        $sCode = "var " . $this->_sJsObjName . " = new " . $this->_sJsObjClass . "(" . json_encode($aParams) . ");";
+
+        return $this->_oTemplate->_wrapInTagJsCode($sCode);
+    }
+
+    public function getJsClick()
+    {
+        return $this->getJsObjectName() . '.report(this)';
+    }
+
+    public function getCounter($aParams = [])
+    {
+        $aParams = array_merge($this->_aElementDefaults, $aParams);
+
+        $bShowDoReportAsButtonSmall = isset($aParams['show_do_report_as_button_small']) && $aParams['show_do_report_as_button_small'] == true;
+        $bShowDoReportAsButton = !$bShowDoReportAsButtonSmall && isset($aParams['show_do_report_as_button']) && $aParams['show_do_report_as_button'] == true;
+
+        $aReport = $this->_getReport($this->getId());
+
+        $sClass = 'sys-action-counter';
+        if(isset($aParams['show_counter_only']) && (bool)$aParams['show_counter_only'] === true)
+            $sClass .= ' sys-ac-only';
+
+        $sClass .= ' ' . $this->_sStylePrefix . '-counter';
+        if($bShowDoReportAsButtonSmall)
+            $sClass .= ' bx-btn-small-height';
+        if($bShowDoReportAsButton)
+            $sClass .= ' bx-btn-height';
+
+        return $this->_oTemplate->parseLink('javascript:void(0)',  (int)$aReport['count'] > 0 ? $this->_getCounterLabel($aReport['count']) : '', array(
+            'id' => $this->_aHtmlIds['counter'],
+            'class' => $sClass, 
+            'title' => _t('_report_do_report_by'),
+            'onclick' => 'javascript:' . $this->getJsObjectName() . '.toggleByPopup(this)' 
+        ));
+    }
+
+    public function getCounterAPI($aParams = [])
+    {
+        $aParams = array_merge($this->_aElementDefaultsApi, $aParams);
+
+        return $this->_getReport();
+    }
+
+    public function getElementBlock($aParams = array())
+    {
+        $aParams['usage'] = BX_DOL_REPORT_USAGE_BLOCK;
+
+        return $this->getElement($aParams);
+    }
+
+    public function getElementInline($aParams = array())
+    {
+        $aParams['usage'] = BX_DOL_REPORT_USAGE_INLINE;
+
+        return $this->getElement($aParams);
+    }
+
+    public function getElement($aParams = array())
+    {
+    	$aParams = array_merge($this->_aElementDefaults, $aParams);
+    	$bDynamicMode = isset($aParams['dynamic_mode']) && $aParams['dynamic_mode'] === true;
+
+        $bShowDoReportAsButtonSmall = isset($aParams['show_do_report_as_button_small']) && $aParams['show_do_report_as_button_small'] == true;
+        $bShowDoReportAsButton = !$bShowDoReportAsButtonSmall && isset($aParams['show_do_report_as_button']) && $aParams['show_do_report_as_button'] == true;
+        $bShowCounter = isset($aParams['show_counter']) && $aParams['show_counter'] === true && $this->isAllowedReportView();
+
+        $iObjectId = $this->getId();
+        $iAuthorId = $this->_getAuthorId();
+        $aReport = $this->_getReport($iObjectId);
+
+        if(!$this->isAllowedReport() && (!$this->isAllowedReportView() || (int)$aReport['count'] == 0))
+            return '';
+
+        $aParams['is_reported'] = $this->isPerformed($iObjectId, $iAuthorId) ? true : false;
+
+        $sTmplName = $this->{'_getTmplContentElement' . bx_gen_method_name(!empty($aParams['usage']) ? $aParams['usage'] : BX_DOL_REPORT_USAGE_DEFAULT)}();
+        return $this->_oTemplate->parseHtmlByContent($sTmplName, array(
+            'style_prefix' => $this->_sStylePrefix,
+            'html_id' => $this->_aHtmlIds['main'],
+            'class' => $this->_sStylePrefix . ($bShowDoReportAsButton ? '-button' : '') . ($bShowDoReportAsButtonSmall ? '-button-small' : ''),
+            'count' => $aReport['count'],
+            'do_report' => $this->_getDoReport($aParams),
+            'bx_if:show_counter' => array(
+                'condition' => $bShowCounter,
+                'content' => array(
+                    'style_prefix' => $this->_sStylePrefix,
+                    'bx_if:show_hidden' => array(
+                        'condition' => (int)$aReport['count'] == 0,
+                        'content' => array()
+                    ),
+                    'counter' => $this->getCounter(array_merge($aParams, [
+                        'show_counter_only' => false
+                    ]))
+                )
+            ),
+            'script' => $this->getJsScript($bDynamicMode)
+        ));
+    }
+
+    public function getElementAPI($aParams = [])
+    {
+        if(!$this->_bApi)
+            return;
+        
+        if(!$this->isEnabled())
+            return bx_api_get_msg('_report_err_not_enabled');
+
+        $aParams = array_merge($this->_aElementDefaultsApi, $aParams);
+
+        $iObjectId = $this->getId();
+        $iAuthorId = $this->_getAuthorId();
+
+        $bCount = $this->_isCount();
+        $isAllowedReport = $this->isAllowedReport();
+        $isAllowedReportView = $this->isAllowedReportView();
+        $aParams['is_reported'] = $this->isPerformed($iObjectId, $iAuthorId);
+        $aParams['track'] = $aParams['is_reported'] ? $this->_getTrack($iObjectId, $iAuthorId) : [];
+
+        //--- Do Report
+        $bDoReport = $this->_isShowDoReport($aParams, $isAllowedReport, $bCount);
+        $aDoReport = $bDoReport ? $this->_getDoReport($aParams, $isAllowedReport) : [];
+
+        //--- Counter
+        $bCounter = $this->_isShowCounter($aParams, $isAllowedReport, $isAllowedReportView, $bCount);
+        $aCounter = $bCounter ? $this->getCounterAPI(array_merge($aParams, [
+            'show_counter_only' => false, 
+            'show_script' => false
+        ])) : [];
+
+        if(!$bDoReport && !$bCounter)
+            return bx_api_get_msg('');
+
+        return [
+            'type' => 'reports',
+            'system' => $this->_sSystem,
+            'object_id' => $this->_iId,
+            'params' => array_intersect_key($aParams, array_flip($this->_aElementParamsApi)),
+            'action' => $aDoReport,
+            'counter' => $aCounter
+        ];
+    }
+
+    public function getReportedByWithComments($sObjectNotes)
+    {
+        $aTmplReports = array();
+        $aTypes = BxDolForm::getDataItems('sys_report_types');
+
+        $aReports = $this->_oQuery->getPerformedBy($this->getId());
+        foreach($aReports as $aReport) {
+            list($sUserName, $sUserUrl, $sUserIcon, $sUserUnit) = $this->_getAuthorInfo($aReport['author_id']);
+
+            $sText = bx_process_output($aReport['text'], BX_DATA_TEXT_MULTILINE);
+            
+            $oCmtsNotes = BxDolCmts::getObjectInstance($sObjectNotes, -$aReport['id'], true, $this->_oTemplate);
+            $aCmtsNotes = $oCmtsNotes->getCommentsBlock(array(), array('in_designbox' => false));
+            $iCmtsNotesCount = $oCmtsNotes->getCommentsCount();
+           
+            $aTmplReports[] = array(
+                'style_prefix' => $this->_sStylePrefix,
+                'user_name' => $sUserName,
+                'id' => $aReport['id'],
+                'user_url' => $sUserUrl,
+            	'type' => $aTypes[$aReport['type']],
+                'comments_count' => _t('_report_comments_count', $iCmtsNotesCount),
+                'comments' => $aCmtsNotes['content'],
+                'date' => bx_time_js($aReport['date']),
+            	'bx_if:show_text' => array(
+                    'condition' => strlen($sText) > 0,
+                    'content' => array(
+                        'text' => $sText
+                    )
+            	)
+            );
+        }
+
+        if(empty($aTmplReports))
+            return '';
+
+        return $this->_oTemplate->parseHtmlByName('report_by_list_with_comments.html', array(
+            'style_prefix' => $this->_sStylePrefix,
+            'bx_repeat:list' => $aTmplReports
+        ));
+    }
+
+    protected function _getDoReport($aParams = array())
+    {
+    	$bReported = isset($aParams['is_reported']) && $aParams['is_reported'] === true;
+        $bShowDoReportAsButtonSmall = isset($aParams['show_do_report_as_button_small']) && $aParams['show_do_report_as_button_small'] == true;
+        $bShowDoReportAsButton = !$bShowDoReportAsButtonSmall && isset($aParams['show_do_report_as_button']) && $aParams['show_do_report_as_button'] == true;
+        $bDisabled = !$this->isAllowedReport() || ($bReported && !$this->isUndo());
+
+        $sTitle = call_user_func_array('_t', $this->_getTitleDoReport($bReported));
+
+        if($this->_bApi)
+            return [
+                'is_undo' => $this->isUndo(),
+                'is_reported' => $bReported,
+                'is_disabled' => $bDisabled,
+                'title' => $sTitle,
+            ];
+
+        $sClass = '';
+        if($bShowDoReportAsButton)
+            $sClass = 'bx-btn';
+        else if ($bShowDoReportAsButtonSmall)
+            $sClass = 'bx-btn bx-btn-small';
+
+        if($bDisabled)
+            $sClass .= $bShowDoReportAsButton || $bShowDoReportAsButtonSmall ? ' bx-btn-disabled' : 'bx-report-disabled';
+
+        if($bReported)
+            $sClass .= ' bx-report-reported';
+
+        return $this->_oTemplate->parseHtmlByContent($this->_getTmplContentDoAction(), array(
+            'style_prefix' => $this->_sStylePrefix,
+            'html_id' => $this->_aHtmlIds['do_link'],
+            'class' => $sClass,
+            'title' => bx_html_attribute($sTitle),
+        	'bx_if:show_onclick' => array(
+                    'condition' => !$bDisabled,
+                    'content' => array(
+                        'js_object' => $this->getJsObjectName()
+                    )
+        	),
+            'do_report' => $this->_getLabelDoReport($aParams),
+        ));
+    }
+
+    protected function _getCounterLabel($iCount)
+    {
+        return _t('_report_counter', $iCount);
+    }
+
+    protected function _getLabelDoReport($aParams = array())
+    {
+    	$bReported = isset($aParams['is_reported']) && $aParams['is_reported'] === true;
+        return $this->_oTemplate->parseHtmlByContent($this->_getTmplContentDoActionLabel(), array(
+            'style_prefix' => $this->_sStylePrefix,
+            'bx_if:show_icon' => array(
+                'condition' => isset($aParams['show_do_report_icon']) && $aParams['show_do_report_icon'] == true,
+                'content' => array(
+                    'style_prefix' => $this->_sStylePrefix,
+                    'name' => $this->_getIconDoReport($bReported)
+                )
+            ),
+            'bx_if:show_text' => array(
+                'condition' => isset($aParams['show_do_report_label']) && $aParams['show_do_report_label'] == true,
+                'content' => array(
+                    'style_prefix' => $this->_sStylePrefix,
+                    'text' => call_user_func_array('_t', $this->_getTitleDoReport($bReported))
+                )
+            )
+        ));
+    }
+
+    protected function _getReportedBy()
+    {
+        $aTmplReports = array();
+        $aTypes = BxDolForm::getDataItems('sys_report_types');
+
+        $aReports = $this->_oQuery->getPerformedBy($this->getId());
+        foreach($aReports as $aReport) {
+            list($sUserName, $sUserUrl, $sUserIcon, $sUserUnit) = $this->_getAuthorInfo($aReport['author_id']);
+
+            $sText = bx_process_output($aReport['text'], BX_DATA_TEXT_MULTILINE);
+
+            $aTmplReports[] = array(
+                'style_prefix' => $this->_sStylePrefix,
+                'user_unit' => $sUserUnit,
+            	'type' => $aTypes[$aReport['type']],
+            	'bx_if:show_text' => array(
+                    'condition' => strlen($sText) > 0,
+                    'content' => array(
+                        'text' => $sText
+                    )
+            	)
+            );
+        }
+
+        if(empty($aTmplReports))
+            $aTmplReports = MsgBox(_t('_Empty'));
+
+        return $this->_oTemplate->parseHtmlByName('report_by_list.html', array(
+            'style_prefix' => $this->_sStylePrefix,
+            'bx_repeat:list' => $aTmplReports
+        ));
+    }
+
+    protected function _returnReportData($iObjectId, $iAuthorId, $iReportId, $aData, $bPerformed)
+    {
+        $bUndo = $this->isUndo();
+        if(empty($aData) || !is_array($aData))
+            $aData = $this->_getReport($iObjectId);
+
+        $aResult = array(
+            'eval' => $this->getJsObjectName() . '.onReport(oData, oElement)',
+            'code' => 0,
+            'id' => $iReportId, 
+            'count' => $aData['count'], 
+            'countf' => (int)$aData['count'] > 0 ? $this->_getCounterLabel($aData['count']) : '',
+            'label_icon' => $this->_getIconDoReport($bPerformed),
+            'label_title' => call_user_func_array('_t', $this->_getTitleDoReport($bPerformed)),
+            'reported' => $bPerformed,
+            'disabled' => $bPerformed && !$bUndo,
+        );
+
+        $aResult['api'] = [
+            'performer_id' => $iAuthorId,
+            'is_reported' => $aResult['reported'],
+            'is_disabled' => $aResult['disabled'],
+            'icon' => $aResult['label_icon'],
+            'title' => $aResult['label_title'],
+            'counter' => $this->_getReport()
+        ];
+
+        return $aResult;
+    }
+
+    protected function _returnReportDataForSocket($aData, $aMask = [])
+    {
+        if(empty($aMask) || !is_array($aMask))
+            $aMask = ['code', 'count', 'countf', 'api'];
+
+        return array_intersect_key($aData, array_flip($aMask));
+    }
+
+    private function _prepareAuditParams($iObjectId, $aData)
+    {
+        $sModule = $this->_sSystem;
+        $sKeyObjectContentInfo = 'OBJECT_CONTENT_INFO';
+        $sActionName = '_sys_audit_action_report';
+        
+        if ($this->_sSystem == 'sys_cmts'){
+            $aCommentData = BxDolCmtsQuery::getInfoByUniqId($iObjectId);
+            $oComment = BxDolCmts::getObjectInstance($aCommentData['system_name'], $aCommentData['cmt_object_id']);
+            $aComment = $oComment->getCommentSimple($aCommentData['cmt_id']);
+            $aSystem = $oComment->getSystemInfo();
+            
+            $sModule = $aSystem['module'];
+            $aData['comment_id'] = $aCommentData['cmt_id'];
+            $aData['comment_author_id'] = $aComment['cmt_author_id'];
+            $aData['comment_text'] = $aComment['cmt_text'];
+            $iObjectId = $aCommentData['cmt_object_id'];
+            
+            $sKeyObjectContentInfo = 'OBJECT_CMTS_CONTENT_INFO';
+            $sActionName = '_sys_audit_action_report_comment';
+        }
+
+        $oModule = BxDolModule::getInstance($sModule);
+        if ($oModule) {
+            $CNF = $oModule->_oConfig->CNF;
+
+            $aContentInfo = BxDolRequest::serviceExists($sModule, 'get_all') ? BxDolService::call($sModule, 'get_all', array(array('type' => 'id', 'id' => $iObjectId))) : array();
+                    
+            return [
+                'content_title' => !empty($CNF['FIELD_TITLE'])  ? $aContentInfo[$CNF['FIELD_TITLE']] : '',
+                'content_info_object' => !empty($CNF[$sKeyObjectContentInfo]) ? $CNF[$sKeyObjectContentInfo] : '',
+                'data' => $aData,
+                'action_name' => $sActionName, 
+                'object_id' => $iObjectId 
+            ];
+        }
+        
+        return false;
+    }
+
+    protected function _isShowDoReport($aParams, $isAllowedVote, $bCount)
+    {
+        return !isset($aParams['show_do_report']) || (bool)$aParams['show_do_report'] === true;
+    }
+
+    protected function _isShowCounter($aParams, $isAllowedVote, $isAllowedVoteView, $bCount)
+    {
+        return isset($aParams['show_counter']) && (bool)$aParams['show_counter'] === true && $isAllowedVoteView && ($isAllowedVote || $bCount);
+    }
+}
+
+/** @} */

+ 99 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseReportServices.php

@@ -0,0 +1,99 @@
+<?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 Reports.
+ */
+class BxBaseReportServices extends BxDol
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-report Report
+     * @subsubsection bx_system_general-do do
+     * 
+     * @code bx_srv('system', 'do', [[...]], 'TemplReportServices'); @endcode
+     * @code {{~system:do:TemplReportServices[[...]]~}} @endcode
+     * 
+     * Performs Do (Report) action
+     * @param $aParams an array with necessary parameters 
+     * 
+     * @see BxBaseReportServices::serviceDo
+     */
+    /** 
+     * @ref bx_system_general-do "do"
+     * @api @ref bx_system_general-do "do"
+     */
+    public function serviceDo($aParams)
+    {
+        if(is_string($aParams))
+            $aParams = json_decode($aParams, true);
+
+        if(!$aParams['s'] || !$aParams['o'])
+            return ['code' => BX_DOL_OBJECT_ERR_NOT_AVAILABLE];
+
+        $oReport = BxDolReport::getObjectInstance($aParams['s'], $aParams['o']);
+        if(!$oReport || !$oReport->isEnabled())
+            return ['code' => BX_DOL_OBJECT_ERR_NOT_AVAILABLE];
+
+        $aResult = $oReport->report($aParams);
+        return (int)$aResult['code'] != 0 ? $aResult : $aResult['api'];
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-report Report
+     * @subsubsection bx_system_general-get_performed_by get_performed_by
+     * 
+     * @code bx_srv('system', 'get_performed_by', [[...]], 'TemplReportServices'); @endcode
+     * @code {{~system:get_performed_by:TemplReportServices[[...]]~}} @endcode
+     * 
+     * Gets a list of PerformedBy users.
+     * @param $aParams an array with necessary parameters 
+     * 
+     * @see BxBaseReportServices::serviceGetPerformedBy
+     */
+    /** 
+     * @ref bx_system_general-get_performed_by "get_performed_by"
+     * @api @ref bx_system_general-get_performed_by "get_performed_by"
+     */
+    public function serviceGetPerformedBy($aParams)
+    {
+        if(is_string($aParams))
+            $aParams = json_decode($aParams, true);
+
+        if(!$aParams['s'] || !$aParams['o'])
+            return ['code' => BX_DOL_OBJECT_ERR_NOT_AVAILABLE];
+
+        $oReport = BxDolReport::getObjectInstance($aParams['s'], $aParams['o']);
+        if(!$oReport || !$oReport->isEnabled())
+            return ['code' => BX_DOL_OBJECT_ERR_NOT_AVAILABLE];
+
+        $aValues = $oReport->getQueryObject()->getPerformedBy($oReport->getId());
+
+        $aTmplUsers = [];
+        foreach($aValues as $mValue) {
+            $mValue = is_array($mValue) ? $mValue : ['author_id' => (int)$mValue, 'reaction' => ''];
+
+            $aTmplUsers[] = BxDolProfile::getData($mValue['author_id']);
+        }
+
+        return [
+            'performed_by' => $aTmplUsers
+        ];
+    }
+}
+
+/** @} */

+ 443 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseSearchResult.php

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

+ 714 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseServiceAccount.php

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

+ 1146 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseServiceProfiles.php

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

+ 154 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseServiceRecommendations.php

@@ -0,0 +1,154 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * System services for recommendations.
+ */
+class BxBaseServiceRecommendations extends BxDol
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    public function serviceUpdateData($iProfileId, $bVerbose = false)
+    {
+        $aResults = [];
+
+        $aObjects = BxDolRecommendationQuery::getObjects();
+        foreach($aObjects as $aObject)
+            if(($oRecommendation = BxDolRecommendation::getObjectInstance($aObject['name'])) !== false)
+                $aResults[] = [
+                    'object' => $aObject['name'],
+                    'items' => $oRecommendation->processCriteria($iProfileId)
+                ];
+
+        if($bVerbose)
+            return $aResults;
+    }
+
+    /**
+     * @page service Service Calls
+     * @section bx_system_general System Services 
+     * @subsection bx_system_general-recommendations Recommendations
+     * @subsubsection bx_system_general-perform perform
+     * 
+     * @code bx_srv('system', 'perform', [[...]], 'TemplServiceRecommendations'); @endcode
+     * @code {{~system:perform:TemplServiceRecommendations[[...]]~}} @endcode
+     * 
+     * Performs an action (add, remove, etc) with recommendations object.
+     * @param $aParams an array with necessary parameters 
+     * 
+     * @see BxBaseServiceRecommendations::servicePerform
+     */
+    /** 
+     * @ref bx_system_general-perform "perform"
+     * @api @ref bx_system_general-perform "perform"
+     */
+    public function servicePerform($aParams)
+    {
+        if(is_string($aParams))
+            $aParams = json_decode($aParams, true);
+
+        if(!$aParams['o'] || !$aParams['a'] || !$aParams['iid'] || !$aParams['cid'])
+            return ['code' => 1];
+
+        $oRecommendation = BxDolRecommendation::getObjectInstance($aParams['o']);
+        if(!$oRecommendation)
+            return ['code' => 2];
+
+        $sMethod = 'action' . bx_gen_method_name($aParams['a']);
+        if(!method_exists($oRecommendation, $sMethod))
+            return ['code' => 2];
+
+        $aResult = $oRecommendation->$sMethod($aParams['iid'], $aParams['cid']);
+        if($aResult['code'] != 0)
+            return ['code' => 3, 'message' => $aResult['msg']];
+
+        $aFlip = [
+            'add' => '',
+            'ignore' => ''
+        ];
+
+        $sFlipped = $aFlip[$aParams['a']];
+        return [
+            'a' => $sFlipped,
+            'title' => !empty($sFlipped) ? _t('_sys_menu_item_title_sm_' . $sFlipped) : '',
+        ];
+    }
+
+    public function serviceGetFriendRecommendationsBySharedContext($iProfileId, $sConnection, $iPoints)
+    {
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if(!$oProfile)
+            return [];
+        
+        $oConnection = BxDolConnection::getObjectInstance($sConnection);
+        if(!$oConnection)
+            return [];
+
+        $sQuery = "SELECT `tm`.`initiator`AS `id`, SUM({points}) AS `value` FROM `{connection}` AS `tg` INNER JOIN `{connection}` AS `tm` ON `tg`.`content`=`tm`.`content` AND `tm`.`initiator`<>{profile_id} AND `tm`.`initiator` NOT IN (SELECT `content` FROM `sys_profiles_conn_friends` WHERE `initiator`={profile_id} AND `mutual`='1') AND `tm`.`mutual`='1' WHERE `tg`.`initiator`={profile_id} AND `tg`.`mutual`='1' GROUP BY `id`";
+        $sQuery = bx_replace_markers($sQuery, [
+            'profile_id' => $iProfileId,
+            'connection' => $oConnection->getTable(),
+            'points' => $iPoints
+        ]);
+
+        return BxDolDb::getInstance()->getPairs($sQuery, 'id', 'value');
+    }
+
+    public function serviceGetFriendRecommendationsBySharedLocation($iProfileId, $iRadius, $iPoints)
+    {
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if(!$oProfile)
+            return [];
+
+        $sProfileModule = $oProfile->getModule();
+        $iProfileContentId = $oProfile->getContentId();
+
+        $aLocation = bx_srv($sProfileModule, 'get_location', [$iProfileContentId]);
+        if(empty($aLocation) || !is_array($aLocation))
+            return [];
+
+        $aIds = bx_srv('system', 'profiles_search_by_location', [$aLocation, $iRadius], 'TemplServiceProfiles');
+        if(empty($aIds) || !is_array($aIds))
+            return [];
+
+        /**
+         * Exclude friends and oneself
+         */
+        $aIdsExclude = BxDolConnection::getObjectInstance('sys_profiles_friends')->getConnectedContent($iProfileId);
+        $aIdsExclude[] = $iProfileId;
+
+        $aIds = array_diff($aIds, $aIdsExclude);
+        return array_combine($aIds, array_fill(0, count($aIds), $iPoints));
+    }
+
+    public function serviceGetSubscriptionRecommendationsBySharedContext($iProfileId, $sConnection, $iPoints)
+    {
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if(!$oProfile)
+            return [];
+
+        $oConnection = BxDolConnection::getObjectInstance($sConnection);
+        if(!$oConnection)
+            return [];
+
+        $sQuery = "SELECT `tm`.`initiator`AS `id`, SUM({points}) AS `value` FROM `{connection}` AS `tg` INNER JOIN `{connection}` AS `tm` ON `tg`.`content`=`tm`.`content` AND `tm`.`initiator`<>{profile_id} AND `tm`.`initiator` NOT IN (SELECT `content` FROM `sys_profiles_conn_subscriptions` WHERE `initiator`={profile_id}) AND `tm`.`mutual`='1' WHERE `tg`.`initiator`={profile_id} AND `tg`.`mutual`='1' GROUP BY `id`";
+        $sQuery = bx_replace_markers($sQuery, [
+            'profile_id' => $iProfileId,
+            'connection' => $oConnection->getTable(),
+            'points' => $iPoints
+        ]);
+
+        return BxDolDb::getInstance()->getPairs($sQuery, 'id', 'value');
+    }
+}
+
+/** @} */

+ 1368 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts/BxBaseServices.php

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

+ 21 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts_templ/BxTemplMenuSubmenuWithAddons.php

@@ -0,0 +1,21 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaTemplate UNA Template Classes
+ * @{
+ */
+
+/**
+ * @see BxDolMenu
+ */
+class BxTemplMenuSubmenuWithAddons extends BxBaseMenuSubmenuWithAddons
+{
+    public function __construct ($aObject, $oTemplate = false)
+    {
+        parent::__construct ($aObject, $oTemplate);
+    }
+}
+
+/** @} */

+ 16 - 0
upgrade/files/13.1.0.RC2-13.1.0.RC3/files/template/scripts_templ/BxTemplReportServices.php

@@ -0,0 +1,16 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaTemplate UNA Template Classes
+ * @{
+ */
+
+/**
+ * @see BxBaseReportServices
+ */
+class BxTemplReportServices extends BxBaseReportServices 
+{}
+
+/** @} */

+ 20 - 1
upgrade/files/13.1.0.RC2-13.1.0.RC3/files_delete.php

@@ -1,7 +1,26 @@
 <?php
 
 $aFilesDelete = array(
-
+    'plugins/mpratt/embera/src/Embera/Provider/AmCharts.php',
+    'plugins/mpratt/embera/src/Embera/Provider/Avocode.php',
+    'plugins/mpratt/embera/src/Embera/Provider/Backtracks.php',
+    'plugins/mpratt/embera/src/Embera/Provider/Byzart.php',
+    'plugins/mpratt/embera/src/Embera/Provider/Chirbit.php',
+    'plugins/mpratt/embera/src/Embera/Provider/CodePen.php',
+    'plugins/mpratt/embera/src/Embera/Provider/EduMedia.php',
+    'plugins/mpratt/embera/src/Embera/Provider/EnystreMusic.php',
+    'plugins/mpratt/embera/src/Embera/Provider/Gfycat.php',
+    'plugins/mpratt/embera/src/Embera/Provider/IMenuPro.php',
+    'plugins/mpratt/embera/src/Embera/Provider/Minesweeper.php',
+    'plugins/mpratt/embera/src/Embera/Provider/NoPaste.php',
+    'plugins/mpratt/embera/src/Embera/Provider/OnSizzle.php',
+    'plugins/mpratt/embera/src/Embera/Provider/Padlet.php',
+    'plugins/mpratt/embera/src/Embera/Provider/Saooti.php',
+    'plugins/mpratt/embera/src/Embera/Provider/Smrthi.php',
+    'plugins/mpratt/embera/src/Embera/Provider/Subscribi.php',
+    'plugins/mpratt/embera/src/Embera/Provider/Veer.php',
+    'plugins/mpratt/embera/src/Embera/Provider/Vlive.php',
+    'plugins/mpratt/embera/src/Embera/Provider/Waltrack.php',
 
 //	'plugins/phpmailer/Exception.php',
 //	'plugins/phpmailer/LICENSE',