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

Upgrade script from 13.0.0-RC1 to 13.0.0-RC2

Alex Trofimov 3 лет назад
Родитель
Сommit
50c55cb85a
100 измененных файлов с 23744 добавлено и 0 удалено
  1. 2202 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/classes/BxDolCmts.php
  2. 406 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/classes/BxDolConnectionQuery.php
  3. 1979 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/classes/BxDolForm.php
  4. 325 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/classes/BxDolLiveUpdates.php
  5. 235 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/classes/BxDolModuleQuery.php
  6. 839 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/classes/BxDolPage.php
  7. 1545 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/classes/BxDolStorage.php
  8. 3546 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/classes/BxDolTemplate.php
  9. 545 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/classes/BxDolUploader.php
  10. 1043 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/js/classes/BxDolCmts.js
  11. 89 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/js/classes/BxDolPage.js
  12. 1104 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/js/classes/BxDolUploader.js
  13. 444 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/js/editor.quill.js
  14. 1465 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/js/functions.js
  15. 2327 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/utils.inc.php
  16. 12 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/version.inc.php
  17. 279 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/general/template/css/main.css
  18. 1943 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/groups/classes/BxBaseModGroupsModule.php
  19. 43 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/groups/template/cover_block.html
  20. 225 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/payment/classes/BxBaseModPaymentGridOrders.php
  21. 759 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/profile/classes/BxBaseModProfileTemplate.php
  22. 38 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/profile/template/cover.html
  23. 50 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/profile/template/cover_block.html
  24. 130 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/template/js/sidebar.js
  25. 1064 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/text/classes/BxBaseModTextTemplate.php
  26. 25 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/text/template/unit_gallery.html
  27. 7 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins/autoload.php
  28. 15 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins/composer/autoload_files.php
  29. 75 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins/composer/autoload_real.php
  30. 224 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins/composer/autoload_static.php
  31. 348 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins/composer/installed.php
  32. 5 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/index.global.min.js
  33. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/af.global.min.js
  34. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ar-dz.global.min.js
  35. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ar-kw.global.min.js
  36. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ar-ly.global.min.js
  37. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ar-ma.global.min.js
  38. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ar-sa.global.min.js
  39. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ar-tn.global.min.js
  40. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ar.global.min.js
  41. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/az.global.min.js
  42. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/bg.global.min.js
  43. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/bn.global.min.js
  44. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/bs.global.min.js
  45. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ca.global.min.js
  46. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/cs.global.min.js
  47. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/cy.global.min.js
  48. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/da.global.min.js
  49. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/de-at.global.min.js
  50. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/de.global.min.js
  51. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/el.global.min.js
  52. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/en-au.global.min.js
  53. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/en-gb.global.min.js
  54. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/en-nz.global.min.js
  55. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/eo.global.min.js
  56. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/es-us.global.min.js
  57. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/es.global.min.js
  58. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/et.global.min.js
  59. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/eu.global.min.js
  60. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/fa.global.min.js
  61. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/fi.global.min.js
  62. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/fr-ca.global.min.js
  63. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/fr-ch.global.min.js
  64. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/fr.global.min.js
  65. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/gl.global.min.js
  66. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/he.global.min.js
  67. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/hi.global.min.js
  68. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/hr.global.min.js
  69. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/hu.global.min.js
  70. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/hy-am.global.min.js
  71. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/id.global.min.js
  72. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/is.global.min.js
  73. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/it.global.min.js
  74. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ja.global.min.js
  75. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ka.global.min.js
  76. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/kk.global.min.js
  77. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/km.global.min.js
  78. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ko.global.min.js
  79. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ku.global.min.js
  80. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/lb.global.min.js
  81. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/lt.global.min.js
  82. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/lv.global.min.js
  83. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/mk.global.min.js
  84. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ms.global.min.js
  85. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/nb.global.min.js
  86. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ne.global.min.js
  87. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/nl.global.min.js
  88. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/nn.global.min.js
  89. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/pl.global.min.js
  90. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/pt-br.global.min.js
  91. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/pt.global.min.js
  92. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ro.global.min.js
  93. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ru.global.min.js
  94. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/si-lk.global.min.js
  95. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/sk.global.min.js
  96. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/sl.global.min.js
  97. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/sm.global.min.js
  98. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/sq.global.min.js
  99. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/sr-cyrl.global.min.js
  100. 6 0
      upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/sr.global.min.js

+ 2202 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/classes/BxDolCmts.php

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

+ 406 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/classes/BxDolConnectionQuery.php

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

+ 1979 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/classes/BxDolForm.php

@@ -0,0 +1,1979 @@
+<?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_FORM_METHOD_GET', 'get');
+define('BX_DOL_FORM_METHOD_POST', 'post');
+define('BX_DOL_FORM_METHOD_SPECIFIC', 'specific');
+define('BX_DOL_FORM_METHOD_DEFAULT', BX_DOL_FORM_METHOD_POST);
+
+define('BX_DATA_LISTS_KEY_PREFIX', '#!');
+define('BX_DATA_VALUES_DEFAULT', 'LKey'); ///< Use default values for data items, @see BxDolForm::getDataItems
+define('BX_DATA_VALUES_ADDITIONAL', 'LKey2'); ///< Use additional values for data items, @see BxDolForm::getDataItems
+define('BX_DATA_VALUES_ALL', 'all'); ///< Return all available info for data items, @see BxDolForm::getDataItems
+
+bx_import('BxDolPrivacy');
+
+/**
+ * Forms allows to display forms from data stored in DB tables, before it was possible to display forms from PHP arrays only.
+ *
+ *
+ * The Form Objects have the following main advantages:
+ * - Minimal coding is needed to create different forms
+ * - Easy forms alterations
+ * - Multiple representations of the same form
+ * - Automated data inserting into database
+ * - Automated data updating
+ * - Automated data checking
+ * - Automatic security checking
+ * - Automatic spam filter
+ *
+ *
+ * Forms API uses several definitions:
+ * - Form or Form Object - record in sys_objects_form table, or instance of Form class.
+ * - Form Display - set of some form inputs in particular order, displayed for some purpose; one form can have several displays, for example add and edit displays.
+ * - Form Input or Form Field - form input field, like textarea, checkbox or set of radio buttons.
+ *
+ *
+ * @section form_creating_object Creating the Form Object
+ *
+ * 1. Create Form Object, add record to sys_objects_form table:
+ * - object - name of the Form Object, in the format: vendor prefix, underscore, module prefix, underscore, internal identifier or nothing; for example: bx_group - for group data processing, like group adding or editing.
+ * - title - Form Object title to display in studio forms builder.
+ * - action - url to submit form to, if url is not full and not empty, then site url is added automatically.
+ * - form_attrs - serialized array of additional form attributes.
+ * - submit_name - name of form field with submit button, it is used to determine if form is submitted.
+ * - table - DB table name (for automatic saving/updating).
+ * - key - DB table field with unique ID (for automatic updating).
+ * - uri - DB table field with URI (for automatic URI generation, aling with uri_title).
+ * - uri_title - DB table field with data title (for automatic URI generation, aling with uri).
+ * - params - serialized array of additional form parameters:
+ * - checker_helper - name of custom BxDolFormCheckerHelper class.
+ * - csrf - array of Cross-site request forgery attack prevention parameters, for now only one boolean parameter is supported - disabled, so it can be disabled for particular form.
+ * - active - 1 or 0, if form is inactive then it can not be used anywhere.
+ * - override_class_name - user defined class name which is derived from BxTemplFormView.
+ * - override_class_file - the location of the user defined class, leave it empty if class is located in system folders.
+ *
+ *
+ * 2. Create Form Displays, add records to sys_form_displays table:
+ * - display_name - name of the Form Display, in the format:
+ *                  form object name, underscore, internal identifier or nothing;
+ *                  for example: bx_group_add - for displaying group adding form, bx_group_edit - for displaying group editing form
+ * - module - module name this display belongs to, it must be associated with name field in sys_modules table.
+ * - object - form object name from sys_objects_form table this Form Display belongs to.
+ * - title - Form Display title to display in studio forms builder.
+ * - view_mode - display form as read-only.
+ *
+ *
+ * 3. Create Form Fields, add records to sys_form_inputs table:
+ * - object - form object name from sys_objects_form table this Form Field belongs to.
+ * - module - module name this field belongs to.
+ * - name - unique Form Field name in particular From Object.
+ * - value - default value, or empty if there is no default value.
+ * - values - possible values, for certain form field types.
+ * - checked - 0 or 1, it determines if form field is checked, for certain form field types.
+ * - type - form field type, for now the following types are supported:
+ *      - text - text input field.
+ *      - password - password input field.
+ *      - textarea - multiline input field.
+ *      - number - number input field.
+ *      - select - select one from all available values.
+ *      - select_multiple - select one, multiple or all items from all available values.
+ *      - switcher - on/off switcher.
+ *      - checkbox - one checkbox.
+ *      - checkbox_set - set of checkboxes.
+ *      - radio_set - set of radio buttons.
+ *      - slider - select some numeric value within the range using slider control.
+ *      - doublerange - select range values within the range using slider control.
+ *      - datepicker - date selection control.
+ *      - datetime - date/time selection control.
+ *      - dateselect - date selection control (alternative).
+ *      - captcha - image captcha.
+ *      - hidden - hidden input field.
+ *      - file - file upload input.
+ *      - button - button control.
+ *      - image - form image button.
+ *      - reset - form reset button.
+ *      - submit - form submit button.
+ *      - value - just displaying value without any crontol.
+ *      - block_header - start group of field.
+ *      - custom - custom control.
+ *      - input_set - set of other form controls.
+ *      detailed description of every type will be described below.
+ * - caption - input title.
+ * - info - some info to help user to input data into the field, it's better to specify format and limits here.
+ * - required - indicate that the input is required by displaying asterisk near the field,
+ *              please note that this field don't perform any checking automatically,
+ *              since you mark field as required you need to specify checked function which will check entered value.
+ * - collapsed - display section as collapsed by default, for block_header field type only.
+ * - html - display visual editor of certain type, for textarea field type only.
+ *      - 0 - no visual editor, leave textarea field as it is.
+ *      - 1 - standard(default) visual editor, @see BxDolEditor.
+ *      - 2 - full visual editor, @see BxDolEditor.
+ *      - 3 - mini visual editor, @see BxDolEditor.
+ * - attrs - serialized array of additional input attributes.
+ * - attrs_tr - serialized array of additional attributes for the whole input row.
+ * - attrs_wrapper - serialized array of additional attributes for input wrapper.
+ * - checker_func - checked function, if you marked field as required in textarea field you need to point one of the following checked functions:
+ *      - Length - check value length, additional params must contain min and/or max values for checking.
+ *      - Date - check if date is entered correctly.
+ *      - DateTime - check if datetime is entered correctly.
+ *      - Preg - check value with provided regular expression in checker_params field.
+ *      - Avail - just check if value isn't 0 or empty string, additional function parameters are not used.
+ *      - Email - check if value is written in valid email format.
+ *      - Captcha - check if captcha is entered correctly, for captcha field type only.
+ *      You can inherit BxDolFormCheckerHelper class and add own checker functions, you will need to point your custom class in Form Object params array.
+ * - checker_params - serialized array of checker_func parameters.
+ * - checker_error - error message to show in case of checking function returns false.
+ * - db_pass - function to pass value through before saving to database and after restoring from database (for example when date need to be converted from/to timestamp value),
+ *              available values are the following:
+ *      - Int - convert value to integer.
+ *      - Float - convert value to floating point number.
+ *      - Date,DateUtc - convert value to timestamp value before saving to database, and convert from timespamp value after restoring from database.
+ *      - DateTime,DateTimeUtc - convert value to timestamp value before saving to database, and convert from timespamp value after restoring from database.
+ *      - Xss - it warns you that this text can contain XSS vulnerabilities and you need to be extra careful with this, and always use Forms engine to output string to the browser or use bx_process_output if going to output text manually.
+ *      - XssHtml - this text cam have HTML tags, so perform XSS vulnerabilies cleaning before saving to database.
+ *      - All - do not perform any conversion and pass text as it is, be careful with this, use it only when no other function can be used, and make all necessary security checking by yourself.
+ *      - Preg - perform regular expression on the text before saving data to database, regular expression can be provided in db_params field.
+ *      - Boolean - this is used for checkboxes with 'on' value which need to be converted into boolean value.
+ *      - Set - convert set of values into bit integer before saving to database, and restore bit integer into array of values upon restoration from database, it can be used for select_multiple and checkbox_set field types.
+ *      Please note that values for this field must be 1,2,4,8,... (values of power of 2); the max number of values are 31 for 32bit hardware and 63 for 64bit hardware.
+ *      You can inherit BxDolFormCheckerHelper class and add own pass functions, you will need to point your custom class in Form Object params array.
+ * - db_params - serialized array of db_pass parameters.
+ * - editable - allow to edit this field from admin forms builder.
+ * - deletable - allow to delete this field from admin forms builder.
+ *
+ *
+ * 4. Add Form Fields and Form Displays associations, add records to sys_form_display_inputs table:
+ * - display_name - name of the Form Display from sys_form_displays table.
+ * - input_name - name of the Form Field from sys_form_inputs table.
+ * - visible_for_levels - bit field with set of member level ids. To use member level id in bit field the level id minus 1 is used as power of 2, for example:
+ *      - user level id = 1 -> 2^(1-1) = 1
+ *      - user level id = 2 -> 2^(2-1) = 2
+ *      - user level id = 3 -> 2^(3-1) = 4
+ *      - user level id = 4 -> 2^(4-1) = 8
+ * - active - 1 - form field displayed on form, or 0 - isn't displayed.
+ * - order - fields are displayed in this order.
+ *
+ *
+ * @section form_field_types Form Field Types
+ *
+ * Detailed description of Form Field Types.
+ *
+ * Almost all fields have the following common parameters:
+ * - object
+ * - name
+ * - type
+ * - caption
+ * - info
+ * - required
+ * - attrs
+ * - attrs_tr
+ * - attrs_wrapper
+ *
+ * We will not describe above list of parameters in every type, since they work the same way for all types.
+ *
+ * The list below are field types with their unique parameters, which are designed especially for this field, or some parameters which work differently for the specified field type.
+ *
+ * text - text input field. It is displayed as regular single line text input.
+ *      Parameters:
+ *      - value - default value, or empty if there is no default value.
+ *      - values - not applicable here.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - checker_func
+ *          Can be used here: Length, Preg, Avail, Email
+ *          Make no sense to use it here: Date, DateTime, Captcha
+ *      - db_pass
+ *          Can be used here: Int, Float, Xss, All, Preg
+ *          Make no sense to use it here: Date, DateTime, XssHtml, Boolean, Set
+ *
+ * password - password input field. It is displayed as HTML input element with invisible input.
+ *      Parameters:
+ *      - value - default value, or empty if there is no default value.
+ *      - values - not applicable here.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - checker_func
+ *          Can be used here: Length, Preg, Avail.
+ *          Make no sense to use it here: Date, DateTime, Captcha, Email.
+ *      - db_pass
+ *          Can be used here: Xss, All.
+ *          Make no sense to use it here: Int, Float, Date, DateTime, XssHtml, Boolean, Set, Preg.
+ *
+ * textarea - multiline input field. It can be displayed as regular textarea field or as visual HTML editor.
+ *      Parameters:
+ *      - value - default value, or empty if there is no default value.
+ *      - values - not applicable here.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - use visual editor or not.
+ *          - 0 - no visual editor, leave textarea field as it is.
+ *          - 1 - standard(default) visual editor, @see BxDolEditor.
+ *          - 2 - full visual editor, @see BxDolEditor.
+ *          - 3 - mini visual editor, @see BxDolEditor.
+ *      - checker_func
+ *          Can be used here: Length, Preg, Avail
+ *          Make no sense to use it here: Email, Date, DateTime, Captcha
+ *      - db_pass
+ *          Can be used here: Int, Float, Xss, XssHtml, All, Preg
+ *          Make no sense to use it here: Date, DateTime, Boolean, Set
+ *
+ * number - number input field. It is displayed as HTL text input, but with limited width. Also some browsers can add additional controls to this field.
+ *      Parameters:
+ *      - value - default value, or empty if there is no default value.
+ *      - values - not applicable here.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - checker_func
+ *          Can be used here: Length, Preg, Avail
+ *          Make no sense to use it here: Email, Date, DateTime, Captcha
+ *      - db_pass
+ *          Can be used here: Int, Float
+ *          Make no sense to use it here: Xss, XssHtml, All, Preg, Date, DateTime, Boolean, Set
+ *
+ * select - select one from all available values. It is displayed as HTML combo-box.
+ *      Parameters:
+ *      - value - default value (array index of selected item from values array), or empty - if there is no default value.
+ *      - values - serialized array of available values, or reference to predefined set of values.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - checker_func
+ *          Can be used here: Length, Preg, Avail
+ *          Make no sense to use it here: Email, Date, DateTime, Captcha
+ *      - db_pass
+ *          Can be used here: Int, Float, Xss, All, Preg
+ *          Make no sense to use it here: Date, DateTime, XssHtml, Boolean, Set
+ *
+ * select_multiple - select one, multiple or all items from all available values. It is displayed as HTML multiple selection input.
+ *      Parameters:
+ *      - value - default value (bit integer of array indexes of selected items from values array), or empty - if there is no default value.
+ *      - values - serialized array of available values, or reference to predefined set of values. Array index must be power of 2. Max number of values is 31 on 32bit hardware or 63 on 64bit hardware.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - checker_func
+ *          Can be used here: Length, Preg, Avail
+ *          Make no sense to use it here: Email, Date, DateTime, Captcha
+ *      - db_pass
+ *          Can be used here: Int
+ *          Make no sense to use it here: Float, Xss, All, Preg, Date, DateTime, XssHtml, Boolean, Set
+ *
+ * switcher - on/off switcher. It is displayed as custom HTML element with own styles, but on background it works as regular HTML checkbox element.
+ *      Parameters:
+ *      - value - the value which will be submitted if switcher is on, if switcher is off - nothing is submitted.
+ *      - values - not applicable here.
+ *      - checked - if set to 1 then switcher is on by default, 0 - it is off by default.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - checker_func
+ *          Can be used here: Length, Preg, Avail
+ *          Make no sense to use it here: Email, Date, DateTime, Captcha
+ *      - db_pass
+ *          Can be used here: Int, Float, Xss, All, Preg, Boolean
+ *          Make no sense to use it here: Date, DateTime, XssHtml, Set
+ *
+ * checkbox - one checkbox. Displayed as HTML checkbox input element.
+ *      Parameters:
+ *      - value - the value which will be submitted if checkbox is checked, if checkbox isn't checked - nothing is submitted.
+ *      - values - not applicable here.
+ *      - checked - if set to 1 then checkbox is checked by default, 0 - it is unchecked by default.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - checker_func
+ *          Can be used here: Length, Preg, Avail
+ *          Make no sense to use it here: Email, Date, DateTime, Captcha
+ *      - db_pass
+ *          Can be used here: Int, Float, Xss, All, Preg, Boolean
+ *          Make no sense to use it here: Date, DateTime, XssHtml, Set
+ *
+ * checkbox_set - set of checkboxes. It is displayed as set of checkboxes. It is displayed in one row if number of items is equal or less than 3 or every item is displayed on new line if there is more than 3 items in the set.
+ *      Parameters:
+ *      - value - default value (bit integer of array indexes of selected items from values array), or empty - if there is no default value.
+ *      - values - serialized array of available values, or reference to predefined set of values. Array index must be power of 2. Max number of values is 31 on 32bit hardware or 63 on 64bit hardware.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - checker_func
+ *          Can be used here: Length, Preg, Avail
+ *          Make no sense to use it here: Email, Date, DateTime, Captcha
+ *      - db_pass
+ *          Can be used here: Int
+ *          Make no sense to use it here: Float, Xss, All, Preg, Date, DateTime, XssHtml, Boolean, Set
+ *
+ * radio_set - set of radio buttons. It is displayed as set of radio buttons. It is displayed in one row if number of items is equal or less than 3 or every item is displayed on new line if there is more than 3 items in the set.
+ *      Parameters:
+ *      - value - default value (array index of selected radio button from values array), or empty - if there is no default value.
+ *      - values - serialized array of available values, or reference to predefined set of values.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - checker_func
+ *          Can be used here: Length, Preg, Avail
+ *          Make no sense to use it here: Email, Date, DateTime, Captcha
+ *      - db_pass
+ *          Can be used here: Int, Float, Xss, All, Preg
+ *          Make no sense to use it here: Date, DateTime, XssHtml, Boolean, Set
+ *
+ * slider - select some numeric value within the range using slider control. It is displayed as jQuery UI HTML control, but on background it works as regular HTML text input element.
+ *      Parameters:
+ *      - value - default value in the format, or empty if there is no default value.
+ *      - values - not applicable here.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - attrs - the following additional attributes can be used here:
+ *      - min - minimal value, default is 0.
+ *      - max - maximal value, default is 100.
+ *      - step - value can be changed by this step only, default is 1.
+ *      - checker_func
+ *          Can be used here: Length, Avail
+ *          Make no sense to use it here: Preg, Email, Date, DateTime, Captcha
+ *      - db_pass
+ *          Can be used here: Int, Float
+ *          Make no sense to use it here: Xss, XssHtml, All, Preg, Date, DateTime, Boolean, Set
+ *
+ * doublerange - select range values within the range using slider control.
+ *      Parameters:
+ *      - value - default value in the format [min value]-[max value], for example 16-99, or empty if there is no default value.
+ *      - values - not applicable here.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - attrs - the following additional attributes can be used here:
+ *          - min - minimal value, default is 0.
+ *          - max - maximal value, default is 100.
+ *          - step - value can be changed by this step only, default is 1.
+ *      - checker_func
+ *          Can be used here: Length, Avail
+ *          Make no sense to use it here: Preg, Email, Date, DateTime, Captcha
+ *      - db_pass
+ *          Can be used here: Xss, All, Preg
+ *          Make no sense to use it here: Int, Float, XssHtml, Date, DateTime, Boolean, Set
+ *
+ * datepicker - date selection control. It is displayed as HTML text input control, when clicking on this input then popup with date selector control is appeared.
+ *      Parameters:
+ *      - value - default value, in the format YYYY-MMM-DD, or empty if there is no default value.
+ *      - values - not applicable here.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - checker_func
+ *          Can be used here: Date
+ *          Make no sense to use it here: Length, Preg, Avail, Email, DateTime, Captcha
+ *      - db_pass
+ *          Can be used here: Date, DateUtc
+ *          Make no sense to use it here: Int, Float, Xss, All, Preg, DateTime, XssHtml, Boolean, Set
+ *
+ * datetime - date/time selection control.
+ *      Parameters:
+ *      - value - default value, in the format YYYY-MMM-DD HH:MM:SS, or empty if there is no default value.
+ *      - values - not applicable here.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - checker_func
+ *          Can be used here: DateTime
+ *          Make no sense to use it here: Length, Preg, Avail, Email, Date, Captcha
+ *      - db_pass
+ *          Can be used here: DateTime, DateTimeUtc
+ *          Make no sense to use it here: Int, Float, Xss, All, Preg, Date, XssHtml, Boolean, Set
+ *
+ * captcha - image captcha. Displayed as image with some text along with HTML text input for entering displayed on the image text.
+ *      Parameters:
+ *      - value - not applicable here.
+ *      - values - not applicable here.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - checker_func
+ *          Can be used here: Captcha
+ *          Make no sense to use it here: Length, Preg, Avail, Email, Date, DateTime
+ *      - db_pass
+ *          Can be used here: Xss, All, Preg
+ *          Make no sense to use it here: Int, Float, Date, DateTime, XssHtml, Boolean, Set
+ *
+ * hidden - hidden input field. Displayed as hidden HTML input.
+ *      Parameters:
+ *      - value - hidden input value.
+ *      - values - not applicable here.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - checker_func
+ *          Can be used here: Length, Preg, Avail, Email, Date, DateTime
+ *          Make no sense to use it here: Captcha
+ *      - db_pass
+ *          Can be used here: Int, Float, Xss, All, Preg, Date, DateUtc, DateTime, DateTimeUtc, XssHtml, Boolean
+ *          Make no sense to use it here: Set
+ *
+ * file - file upload input. Displayed as file upload HTML input.
+ *      Parameters:
+ *      - value - not applicable here.
+ *      - values - not applicable here.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - checker_func
+ *          File name is passed for checking.
+ *          Can be used here: Avail, Length, Preg
+ *          Make no sense to use it here: Email, Date, DateTime, Captcha
+ *      - db_pass
+ *          File can't be stored in the database, so this field isn't applicable here.
+ *
+ * files - files upload input. Displayed as complex uploading HTML control.
+ *          This control is too complex to describe it using default set of database fields, you need to use custom class to display this control.
+ *
+ * button - button control. Displayes as HTML button element.
+ *      Parameters:
+ *      - value - translatable button caption.
+ *      - values - not applicable here.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - checker_func - not applicable here.
+ *      - db_pass - not applicable here.
+ *
+ * image - form image button. It is displayed as HTML form image input element.
+ *      Parameters:
+ *      - value - not applicable here.
+ *      - values - not applicable here.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - attrs - the following mandatory attribute must be specified here:
+ *          - src - image URL.
+ *      - checker_func - not applicable here.
+ *      - db_pass - not applicable here.
+ *
+ * reset - form reset button. Displayed as HTML form reset input button. By clicking on this button the form is reset to its default state.
+ *      Parameters:
+ *      - value - translatable button caption.
+ *      - values - not applicable here.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - checker_func - not applicable here.
+ *      - db_pass - not applicable here.
+ *
+ * submit - form submit button. Displayed as HTML form submit input button. This button have the primary button style to distinguish it from other buttons. By clicking on this button the form is submitted.
+ *      Parameters:
+ *      - value - translatable button caption.
+ *      - values - not applicable here.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - checker_func - not applicable here.
+ *      - db_pass - not applicable here.
+ *
+ * value - just displaying value without any control.
+ *      Parameters:
+ *      - value - the value to display.
+ *      - values - not applicable here.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - checker_func - not applicable here.
+ *      - db_pass - not applicable here.
+ *
+ * block_header - start group of fields. Displayed as form fields divider with caption - then it can be collapsible or without caption - then it is just divider without any functionality.
+ *      Parameters:
+ *      - value - not applicable here.
+ *      - values - not applicable here.
+ *      - checked - not applicable here.
+ *      - collapsed - display group of field collapsed by default, 1 - the group is collapsed, 0 - expanded (default value).
+ *      - html - not applicable here.
+ *      - checker_func - not applicable here.
+ *      - db_pass - not applicable here.
+ *
+ * custom - custom control. You need custom class to display this control, so the exact used values are determined by particular realisation.
+ *
+ * input_set - set of other form controls.
+ *      Parameters:
+ *      - value - not applicable here.
+ *      - values - comma separated list of field names (by name field) of fields to display here.
+ *      - checked - not applicable here.
+ *      - collapsed - not applicable here.
+ *      - html - not applicable here.
+ *      - checker_func - not applicable here.
+ *      - db_pass - not applicable here.
+ *
+ *
+ * @section form_using_own_class Using own class for custom behavior
+ *
+ * It is possible to provide own class for displaying and processing the form.
+ * To do this you need to point it in override_class_name and override_class_file fields in sys_objects_form table.
+ * Your custom class must be inherited from BxTemplFormView class or its descendants.
+ *
+ *
+ * @section form_display_custom_control Displaying custom field control
+ *
+ * It is possible to leave form field with default caption and override only the form field control.
+ * To override some field you need to define the following function:
+ * @code
+ *      protected function genCustomInput[field name] ($aInput).
+ * @endcode
+ * Where [field name] is form field name.
+ * For example:
+ *
+ * @code
+ *     protected function genCustomInputCustom ($aInput) {
+ *         return
+ *         'r: <input type="text" size="2" value="'.(isset($aInput['value'][0]) ? $aInput['value'][0] : '').'" name="'.$aInput['name'].'[]" />' .
+ *         'g: <input type="text" size="2" value="'.(isset($aInput['value'][1]) ? $aInput['value'][1] : '').'" name="'.$aInput['name'].'[]" />' .
+ *         'b: <input type="text" size="2" value="'.(isset($aInput['value'][2]) ? $aInput['value'][2] : '').'" name="'.$aInput['name'].'[]" />';
+ *     }
+ * @endcode
+ *
+ *
+ * @section form_display_custom_row Displaying custom field row
+ *
+ * Form row consists of caption and control, by default it is displayed with default design and functionality.
+ * If you need to display some field with custom header and control you need to declare the following function:
+ * @code
+ *      protected function genCustomRow[field name] ($aInput).
+ * @endcode
+ * Where [field name] is form field name.
+ *
+ *
+ * @section example Example of usage
+ *
+ * Printing the form for adding new record to the database:
+ *
+ * @code
+ *      $oForm = BxDolForm::getObjectInstance('sample_form_objects', 'sample_form_objects_add'); // get form instance for specified form object and display
+ *      if (!$oForm)
+ *          die('"sample_form_objects_add" form object or "sample_form_objects_add" display is not defined');
+ *      $oForm->initChecker(); // init form checker witout any data - adding new record
+ *      if ($oForm->isSubmittedAndValid())
+ *          echo 'inserted id: ' . $oForm->insert (); // add new record to the database
+ *      echo $oForm->getCode(); // display form
+ * @endcode
+ *
+ * Printing the form for editing existing record in the database:
+ *
+ * @code
+ *      // $iEditId - ID of edited row, for example from _GET parameter
+ *      $oDb = BxDolDb::getInstance();
+ *      $sQuery = $oDb->prepare("SELECT * FROM `sample_input_types` WHERE id = ?", $iEditId);
+ *      $aRecord = $oDb->getRow();
+ *      if (!$aRecord)
+ *          die("$iEditId record wasn't found.");
+ *
+ *      $oForm = BxDolForm::getObjectInstance('sample_form_objects', 'sample_form_objects_edit'); // get form instance for specified form object and display
+ *      if (!$oForm)
+ *          die('"sample_form_objects_edit" form object or "sample_form_objects_edit" display is not defined');
+ *      $oForm->initChecker($aRecord); // init form checker with edited data
+ *      if ($oForm->isSubmittedAndValid())
+ *          echo 'updated: ' . $oForm->update ($iEditId); // update database
+ *      echo $oForm->getCode(); // display form
+ * @endcode
+ *
+ * Example of custom form class and custom checking helper class:
+ *
+ * @code
+ *      class BxSampleForm extends BxTemplFormView {
+ *
+ *          public function __construct ($aInfo, $oTemplate = false) {
+ *              parent::__construct ($aInfo, $oTemplate);
+ *          }
+ *
+ *
+ *          // display input with 'custom' name
+ *          protected function genCustomInputCustom ($aInput) {
+ *              return
+ *              'r: <input type="text" size="2" value="'.(isset($aInput['value'][0]) ? $aInput['value'][0] : '').'" name="'.$aInput['name'].'[]" />' .
+ *              'g: <input type="text" size="2" value="'.(isset($aInput['value'][1]) ? $aInput['value'][1] : '').'" name="'.$aInput['name'].'[]" />' .
+ *              'b: <input type="text" size="2" value="'.(isset($aInput['value'][2]) ? $aInput['value'][2] : '').'" name="'.$aInput['name'].'[]" />';
+ *          }
+ *
+ *      }
+ *
+ *      class BxSampleFormCheckerHelper extends BxDolFormCheckerHelper {
+ *
+ *          protected $_sDiv = ',';
+ *
+ *          // prepare RBG values to save to the DB
+ *          function passRgb ($s) {
+ *              if (!is_array($s))
+ *                  return false;
+ *
+ *              $sRet = '';
+ *              foreach ($s as $k => $v)
+ *                  $sRet .= (int)trim($v) . $this->_sDiv;
+ *
+ *              return trim($sRet, $this->_sDiv);
+ *          }
+ *
+ *          // prepare RGB values to output to the screen
+ *          function displayRgb ($s) {
+ *              return explode($this->_sDiv, $s);
+ *          }
+ *      }
+ * @endcode
+ *
+ * The recommended way is to define forms in database, if it is impossible for some reasons you can init form object from array, there is an example:
+ *
+ * @code
+ *      $aForm = array(
+ *            'form_attrs' => array(
+ *                'name'     => 'form_my',
+ *                'method'   => 'post',
+ *            ),
+ *
+ *            'params' => array (
+ *                'db' => array(
+ *                    'table' => 'table_name', // table name
+ *                    'key' => 'ID', // key field name
+ *                    'uri' => 'EntryUri', // uri field name
+ *                    'uri_title' => 'Title', // title field to generate uri from
+ *                    'submit_name' => 'submit_form', // some filed name with non empty value to determine if the for was submitted,
+ *                                                       in most cases it is submit button name
+ *                ),
+ *                'csrf' => array(
+ *                      'disable' => true, //if it wasn't set or has some other value then CSRF checking is enabled for current form, take a look at sys_security_form_token_enable to disable CSRF checking completely.
+ *                )
+ *              ),
+ *
+ *            'inputs' => array(
+ *
+ *                'Title' => array(
+ *                    'type' => 'text',
+ *                    'name' => 'Title', // the same as key and database field name
+ *                    'caption' => 'Some caption',
+ *                    'required' => true,
+ *
+ *                    // checker params
+ *                    'checker' => array (
+ *                        'func' => 'length', // see BxDolFormCheckerHelper class for all check* functions
+ *                        'params' => array(3,100),
+ *                        'error' => 'length must be from 3 to 100 characters',
+ *                    ),
+ *                    // database params
+ *                    'db' => array (
+ *                        'pass' => 'Xss',  // do XSS clear before getting this value, see BxDolFormCheckerHelper class for all pass* functions
+ *                    ),
+ *                ),
+ *
+ *                'Description' => array(
+ *                    'type' => 'textarea',
+ *                    'name' => 'Description', // the same as key and database field name
+ *                    'caption' => 'Some caption',
+ *                    'required' => true,
+ *
+ *                    // checker params
+ *                    'checker' => array (
+ *                        'func' => 'length',
+ *                        'error' => 'enter at least 3 characters',
+ *                        'params' => array(3,64000),
+ *                    ),
+ *                    'db' => array (
+ *                        'pass' => 'XssHtml',  // do XSS clear, but keep HTML before getting this value
+ *                    ),
+ *                ),
+ *            );
+ * @endcode
+ *
+ * Using of above array:
+ *
+ * @code
+ *        $oForm = new BxTemplFormView ($aForm);
+ *        $oForm->initChecker();
+ *
+ *        if ($oForm->isSubmittedAndValid ()) {
+ *
+ *            // add additional vars to database, in this case creation date field is added
+ *            $aValsAdd = array (
+ *                'Date' => time(),
+ *            );
+ *
+ *            echo 'insert last id: ' . $oForm->insert ($aValsAdd); // insert validated data to database
+ *
+ *        } else {
+ *
+ *            echo $oForm->getCode (); // show form
+ *
+ *        }
+ *
+ * @endcode
+ *
+ */
+class BxDolForm extends BxDol implements iBxDolReplaceable
+{
+    static $TYPES_SKIP = array('files' => 1, 'location' => 1, 'nested_form' => 1);
+    static $TYPES_CHECKBOX = array('checkbox' => 1, 'switcher' => 1);
+    static $TYPES_TEXT = array('text' => 1, 'textarea' => 1);
+    static $TYPES_FILE = array('file' => 1);
+
+    static $FUNC_SKIP_DOMAIN_CHECK = array('email' => 1, 'emails' => 1, 'emailexist' => 1, 'emailuniq' => 1, 'emailexistorempty' => 1, 'hostdomain' => 1, 'hostdomainchat' => 1, 'emailorempty' => 1);
+    static $FUNC_SKIP_MACROS_CHECK = array();
+
+    static $LOCATION_INDEXES = array ('lat', 'lng', 'country', 'state', 'city', 'zip', 'street', 'street_number');
+    static $LOCATION_INDEXES_MANDATORY = array ('lat', 'lng');
+
+    protected $_aMarkers = array ();
+
+    protected $oTemplate;
+
+    protected $_isValid = true;
+    protected $_bForceSetToPending = false;
+    protected $_sChecker;
+    protected $_sCheckerHelper;
+    protected $_aSpecificValues;
+
+    public $aFormAttrs; ///< form html element attributes
+    public $aInputs; ///< form inputs
+    public $aParams; ///< additional form parameters
+    public $id; ///< Form element id
+
+    protected $_aFieldsCheckForSpam = array(); ///< additional fields names to check for spam(profanity filter), now only fields with 'textarea' and 'text' are checked for spam, 'textarea' fields are checked for spam and filter for profanity, while 'text' fields are filtetered for profanity only
+    protected $_aFieldsExcludeFromCheckForSpam = array(); ///<  fields names('textarea' and 'text') to exclude from  check for spam(profanity filter)
+
+    
+    protected $_iAuthorId;
+    protected $_sAuthorKey; ///< array key to get author ID from Values array provided in BxDolForm::initChecker.
+    protected $_sPrivacyObjectView;
+    protected $_sPrivacyGroupDefault;
+
+    public function __construct ($aInfo, $oTemplate)
+    {
+        parent::__construct();
+
+        if ($oTemplate)
+            $this->oTemplate = $oTemplate;
+        else
+            $this->oTemplate = BxDolTemplate::getInstance();
+
+        $this->aFormAttrs    = isset($aInfo['form_attrs'])   ? $aInfo['form_attrs']  : array();
+        $this->aInputs       = isset($aInfo['inputs'])       ? $aInfo['inputs']      : array();
+        $this->aParams       = isset($aInfo['params'])       ? $aInfo['params']      : array();
+
+        // get form element id
+        $this->id = $this->aFormAttrs['id'] = (!empty($this->aFormAttrs['id']) ? $this->aFormAttrs['id'] : (!empty($this->aFormAttrs['name']) ? $this->aFormAttrs['name'] : 'form_advanced'));
+
+        // set default method
+        if (!isset($this->aFormAttrs['method']))
+            $this->aFormAttrs['method'] = BX_DOL_FORM_METHOD_DEFAULT;
+
+        // set default action
+        if (!isset($this->aFormAttrs['action']))
+            $this->aFormAttrs['action'] = '';
+
+        $this->_sChecker = isset($this->aParams['checker']) ? $this->aParams['checker'] : 'BxDolFormChecker';
+        $this->_sCheckerHelper = isset($this->aParams['checker_helper']) ? $this->aParams['checker_helper'] : '';
+
+        $this->_sAuthorKey = 'author';
+        $this->_sPrivacyObjectView = 'sys_form_inputs_allow_view_to';
+        $this->_sPrivacyGroupDefault = BX_DOL_PG_ALL;
+    }
+
+    /**
+     * Get form object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    static public function getObjectInstance($sObject, $sDisplayName, $oTemplate = false, $sParam = '')
+    {
+        $sKey = 'BxDolForm!'.$sObject.'!'.$sDisplayName.'!'.$sParam;
+
+        if (isset($GLOBALS['bxDolClasses'][$sKey]))
+            return $GLOBALS['bxDolClasses'][$sKey];
+
+        $aObject = BxDolFormQuery::getFormArray($sObject, $sDisplayName);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        $sClass = 'BxTemplFormView';
+        if (!empty($aObject['override_class_name'])) {
+            $sClass = $aObject['override_class_name'];
+            if (!empty($aObject['override_class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aObject['override_class_file']);
+        }
+
+        $o = new $sClass($aObject, $oTemplate);
+
+        return ($GLOBALS['bxDolClasses'][$sKey] = $o);
+    }
+
+    /**
+     * Get data items array
+     * @param $sKey data items identifier
+     * @param $isUseForSet convert data items keys to use in set fields, make it power of 2 (1,2,4,8,16,etc).
+     * @param $sUseValues use default(BX_DATA_VALUES_DEFAULT) or additional(BX_DATA_VALUES_ADDITIONAL) value titles, if additinal value title is missing default title is used
+     * @return data items array
+     */
+    public static function getDataItems($sKey, $isUseForSet = false, $sUseValues = BX_DATA_VALUES_DEFAULT)
+    {
+        return BxDolFormQuery::getDataItems($sKey, $isUseForSet, $sUseValues);
+    }
+
+    function initChecker ($aValues = array (), $aSpecificValues = array())
+    {
+        $this->_aSpecificValues = $aSpecificValues;
+
+        $oChecker = new $this->_sChecker($this->_sCheckerHelper);
+        $oChecker->setFormMethod($this->aFormAttrs['method'], $aSpecificValues);
+        $oChecker->setFormParams($this->aParams);
+        $oChecker->setFieldsCheckForSpam($this->_aFieldsCheckForSpam, $this->_aFieldsExcludeFromCheckForSpam);
+
+        // init form with default values
+
+        $aValuesDef = array ();
+        foreach ($this->aInputs as $k => $a) {
+            if (!isset($a['value']) || !isset($a['db']['pass']) || isset(self::$TYPES_CHECKBOX[$a['type']]) || isset(self::$TYPES_SKIP[$a['type']]))
+                continue;
+            $aValuesDef[$k] = $a['value'];
+        }
+
+        $oChecker->fillWithValues($this->aInputs, $aValuesDef);
+
+        // init form with provided values
+
+        if ($aValues)
+            $oChecker->fillWithValues($this->aInputs, $aValues);
+
+        // init author from provided values if it's available
+
+        if (isset($aValues[$this->_sAuthorKey]))
+            $this->_iAuthorId = (int)$aValues[$this->_sAuthorKey];
+
+
+        if ($this->isSubmitted ()) {
+
+            // init form with submitted data, overwrite prevously declared values
+
+            $oChecker->enableFormCsrfChecking(isset($this->aParams['csrf']['disable']) && $this->aParams['csrf']['disable'] === true ? false : true);
+
+            $this->_isValid = $oChecker->check($this->aInputs, $aValues);
+
+            if (!$this->_initCheckerNestedForms ())
+                $this->_isValid = false;
+
+        }
+
+        bx_alert('system', 'form_check', 0, 0, array(
+            'object' => &$this,
+        ));
+    }
+
+    function getKeyValuesPair ()
+    {
+        $aRet = array ();
+        foreach ($this->aInputs as $k => $a) {
+            if (!isset($a['value']) || isset(self::$TYPES_CHECKBOX[$a['type']]) || isset(self::$TYPES_SKIP[$a['type']]))
+                continue;
+            $aRet[$k] = $a['value'];
+        }
+        return $aRet;
+    }
+
+    function insert ($aValsToAdd = array(), $isIgnore = false)
+    {
+        $this->autoSetToPending($aValsToAdd);
+
+        $oChecker = new $this->_sChecker($this->_sCheckerHelper);
+        $oChecker->setFormMethod($this->aFormAttrs['method'], $this->_aSpecificValues);
+        $sSql = $oChecker->dbInsert($this->aParams['db'], $this->aInputs, $aValsToAdd, $isIgnore);
+        if (!$sSql)
+            return false;
+        $oDb = BxDolDb::getInstance();
+        if ($oDb->query($sSql)) {
+            $iRes = $oDb->lastId();
+
+            bx_alert('system', 'form_submitted', $iRes, bx_get_logged_profile_id(), array(
+                'module' => isset($this->aParams['module']) ? $this->aParams['module'] : '',
+                'entry_id' => $iRes,
+                'action' => 'insert',
+                'form_object' => &$this,
+            ));
+
+            return $iRes;
+        }
+        return false;
+    }
+
+    function update ($val, $aValsToAdd = array(), &$aTrackTextFieldsChanges = null)
+    {
+        $this->autoSetToPending($aValsToAdd);
+
+        $oChecker = new $this->_sChecker($this->_sCheckerHelper);
+        $oChecker->setFormMethod($this->aFormAttrs['method'], $this->_aSpecificValues);
+        $sSql = $oChecker->dbUpdate($val, $this->aParams['db'], $this->aInputs, $aValsToAdd, $aTrackTextFieldsChanges);
+        if (!$sSql)
+            return false;
+        $bRes = BxDolDb::getInstance()->query($sSql);
+
+        bx_alert('system', 'form_submitted', $val, bx_get_logged_profile_id(), array(
+            'module' => isset($this->aParams['module']) ? $this->aParams['module'] : '',
+            'entry_id' => $val,
+            'action' => 'update',
+            'form_object' => &$this,
+        ));
+
+        return $bRes;
+    }
+
+    function isStatusFieldSupported() {
+        // check whether we have a status field available for this type of content
+        if (isset($this->aParams['module']) && $this->aParams['module'] && $oModule = BxDolModule::getInstance($this->aParams['module'])) {
+            if (isset($oModule->_oConfig->CNF)) {
+                $CNF = &$oModule->_oConfig->CNF;
+                if (isset($CNF['FIELD_STATUS_ADMIN']) &&
+                    isset($this->aParams['db']['table']) && isset($CNF['TABLE_ENTRIES']) && $this->aParams['db']['table'] == $CNF['TABLE_ENTRIES']) {
+                    return true;
+                }
+            }
+        }
+
+        //if this is a comments form then we support it since V13.A3 as a special case
+        if (isset($this->aParams['module']) && $this->aParams['module'] == 'system' && isset($this->aParams['object']) && $this->aParams['object'] == 'sys_comment')
+            return true;
+
+        return false;
+    }
+
+    function autoSetToPending(&$aValsToAdd) {
+        if ($this->isStatusFieldSupported() && $this->_bForceSetToPending) {
+            $oModule = BxDolModule::getInstance($this->aParams['module']);
+            $CNF = &$oModule->_oConfig->CNF;
+
+            if (isset($oModule->_oConfig->CNF) && isset($CNF['FIELD_STATUS_ADMIN'])) $aValsToAdd[$CNF['FIELD_STATUS_ADMIN']] = 'pending';
+        }
+    }
+
+    function delete ($val)
+    {
+        $oChecker = new $this->_sChecker($this->_sCheckerHelper);
+        $oChecker->setFormMethod($this->aFormAttrs['method'], $this->_aSpecificValues);
+        $sSql = $oChecker->dbDelete($val, $this->aParams['db'], $this->aInputs);
+        if (!$sSql)
+            return false;
+        return BxDolDb::getInstance()->query($sSql);
+    }
+
+    function generateUri ()
+    {
+        $f = &$this->aParams['db'];
+        $sUri = $this->getCleanValue ($f['uri_title']);
+        return uriGenerate($sUri, $f['table'], $f['uri']);
+    }
+
+    function getCleanValue ($sName)
+    {
+        $oChecker = new $this->_sChecker($this->_sCheckerHelper);
+        $oChecker->setFormMethod($this->aFormAttrs['method'], $this->_aSpecificValues);
+        $a = isset($this->aInputs[$sName]) ? $this->aInputs[$sName] : false;
+        if ($a && isset($a['db']['pass']))
+            return $oChecker->get ($a['name'], $a['db']['pass'], isset($a['db']['params']) && $a['db']['params'] ? $a['db']['params'] : array());
+        else
+           return $oChecker->get ($sName);
+    }
+
+    function isSubmitted ()
+    {
+        $aSubmitName = $this->aParams['db']['submit_name'];
+        if(!is_array($aSubmitName))
+            $aSubmitName = array($aSubmitName);
+
+        foreach($aSubmitName as $sSubmitName)
+            if(BxDolForm::getSubmittedValue($sSubmitName, $this->aFormAttrs['method'], $this->_aSpecificValues))
+                return true;
+        
+        return false;
+    }
+
+    function getTemplate ()
+    {
+        return $this->oTemplate;
+    }
+
+    function getId ()
+    {
+        return $this->id;
+    }
+
+    function getName ()
+    {
+        return $this->aFormAttrs['name'];
+    }
+
+    function setId ($sId)
+    {
+        $this->id = $sId;
+        $this->aFormAttrs['id'] = $sId;
+    }
+
+    function setName($sName)
+    {
+        $this->aFormAttrs['name'] = $sName;
+    }
+
+    function setAction($sAction)
+    {
+        $this->aFormAttrs['action'] = $sAction;
+    }
+
+    function setForceSetToPending ($isValid)
+    {
+        $this->_bForceSetToPending = $isValid;
+    }
+
+    function setValid ($isValid)
+    {
+        $this->_isValid = $isValid;
+    }
+
+    function isValid ()
+    {
+        return $this->_isValid;
+    }
+
+    function isSubmittedAndValid ()
+    {
+        return ($this->isSubmitted() && $this->isValid());
+    }
+
+    function getSpecificValues()
+    {
+        return $this->_aSpecificValues;
+    }
+
+    function setSpecificValue($sName, $sVal)
+    {
+        $this->_aSpecificValues[$sName] = $sVal;
+    }
+
+    function getAuthorId()
+    {
+        return $this->_iAuthorId;
+    }
+
+    function setAuthorId($iAuthorId)
+    {
+        $this->_iAuthorId = (int)$iAuthorId;
+        $this->aParams['params']['author_id'] = $this->_iAuthorId;
+    }
+
+    public static function getSubmittedValue($sKey, $sMethod, &$aSpecificValues = false)
+    {
+        $aData = array();
+        if($sMethod == BX_DOL_FORM_METHOD_GET) {
+            $aData = &$_GET;
+        } else if($sMethod == BX_DOL_FORM_METHOD_POST) {
+            $aData = &$_POST;
+        } else if($sMethod == BX_DOL_FORM_METHOD_SPECIFIC) {
+            $aData = &$aSpecificValues;
+        }
+
+        return isset($aData[$sKey]) ? $aData[$sKey] : false;
+    }
+
+    public static function setSubmittedValue($sKey, $mixedValue, $sMethod, &$aSpecificValues = false)
+    {
+        $aData = array();
+        if($sMethod == BX_DOL_FORM_METHOD_GET) {
+            $aData = &$_GET;
+        } else if($sMethod == BX_DOL_FORM_METHOD_POST) {
+            $aData = &$_POST;
+        } else if($sMethod == BX_DOL_FORM_METHOD_SPECIFIC) {
+            $aData = &$aSpecificValues;
+        }
+
+        return $aData[$sKey] = $mixedValue;
+    }
+
+    /**
+     * Check if field exists
+     * @param $sName field name 
+     * @param $bCheckAllFields true - check all fields; false - all fields except: password, captcha, hidden, file, button, image, reset, submit, block_header, input_set
+     * @return true if field exists or false otherwise
+     */
+    public function isFieldExist($sName, $bCheckAllFields = false)
+    {
+        if (!isset($this->aInputs[$sName]))
+            return false;
+        return $bCheckAllFields ? true : !in_array($this->aInputs[$sName]['type'], array('password', 'captcha', 'hidden', 'button', 'reset', 'submit', 'block_header', 'input_set', 'location'));
+    }
+
+
+    // Static Methods related to CSRF Tocken
+    public static function genCsrfToken($bReturn = false)
+    {
+        // TODO: remove
+    }
+    
+    public static function getCsrfToken()
+    {
+        if (getParam('sys_security_form_token_enable') != 'on')
+            return false;
+
+        if (!($oKeys = BxDolKey::getInstance()))
+            return false;
+
+        return $oKeys->getNewKey (false, (int)getParam('sys_security_form_token_lifetime'), BxDolSession::getInstance()->getId());
+    }
+    
+    public static function isCsrfTokenValid($s, $bDeleteToken = true)
+    {
+        if (getParam('sys_security_form_token_enable') != 'on')
+            return true;
+
+        if (!($oKeys = BxDolKey::getInstance()))
+            return true;
+
+        if ($oKeys->isKeyExists($s)) {
+            $bRet = $oKeys->isKeyExists($s, BxDolSession::getInstance()->getId());
+            if ($bDeleteToken)
+                $oKeys->removeKey($s);
+            return $bRet;
+        }
+        return false;
+    }
+
+    public static function onModuleUninstall ($sModuleName)
+    {
+        BxDolFormQuery::removeFormFields($sModuleName);
+    }
+    
+    function _initCheckerNestedForms ()
+    {
+        $isValid = true;
+
+        // process nested forms
+        foreach ($this->aInputs as $sKey => $aInput) {
+
+            if (!isset($aInput['ghost_template']))
+                continue;
+
+            if (!(is_array($aInput['ghost_template']) && isset($aInput['ghost_template']['inputs'])) && !(is_object($aInput['ghost_template']) && $aInput['ghost_template'] instanceof BxDolFormNestedGhost))
+                continue;
+
+            $sName = $aInput['name'];
+            $aIds = $this->getSubmittedValue($sName, $this->aFormAttrs['method']);
+            if (!$aIds)
+                continue;
+
+            $aNestedForms = array ();
+            foreach ($aIds as $i => $iFileId) {
+
+                // create separate form instance for each file
+                $oFormNested = false;
+                if (is_object($aInput['ghost_template'])) {
+                    $oFormNested = clone($aInput['ghost_template']);
+                } else {
+                    $oFormNested = new BxDolFormNestedGhost($aInput['name'], $aInput['ghost_template'], $this->aParams['db']['submit_name'], $this->oTemplate);
+                }
+                if ($iFileId && 0 != $iFileId)
+                    $aNestedForms[$iFileId] = $oFormNested;
+                else
+                    $aNestedForms[] = $oFormNested;
+
+                // collect nested form values
+                $aSpecificValues = array ();
+                if (isset($this->aParams['db']['submit_name'])) {
+                    $sSubmitName = $this->aParams['db']['submit_name'];
+                    if(is_array($sSubmitName))
+                        $sSubmitName = reset($sSubmitName);
+        
+                    $aSpecificValues = array ($sSubmitName => 1);
+                }
+
+                foreach ($oFormNested->aInputs as $r) {
+                    $sName = str_replace('[]', '', $r['name']);
+                    $aValue = $this->getSubmittedValue($sName, $this->aFormAttrs['method']);
+                    $aSpecificValues[$sName] = isset($aValue[$i]) ? $aValue[$i] : '';
+                }
+                $oFormNested->initChecker(array(), $aSpecificValues);
+
+                // if nested form in invalid - then the whole worm is failed
+                if (!$oFormNested->isValid ())
+                    $isValid = false;
+            }
+
+            if ($aNestedForms)
+                $this->aInputs[$sKey]['ghost_template'] = $aNestedForms;
+
+        }
+
+        return $isValid;
+    }
+
+    /**
+     * Check if form field is visible.
+     * @param $aInput form field array
+     * @return boolean
+     */
+    static public function isVisible ($aInput)
+    {
+        return isset($aInput['visible_for_levels']) ? BxDolAcl::getInstance()->isMemberLevelInSet($aInput['visible_for_levels']) : true;
+    }
+
+    protected function _genMethodName ($s)
+    {
+        return bx_gen_method_name($s);
+    }
+
+    /**
+     * Add replace markers. Curently markers are replaced in action, form_attrs fields.
+     * @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 form array
+     * @param $a form description array
+     * @return array where markes are replaced with real values
+     */
+    protected function _replaceMarkers ($a)
+    {
+        return bx_replace_markers($a, $this->_aMarkers);
+    }
+
+    public function getFormErrors ()
+    {
+        $s = '';
+        foreach ($this->aInputs as $k => $a) {
+            if (isset($this->aInputs[$k]['error']) && $this->aInputs[$k]['error'])
+                $s .= $k . ': ' . $this->aInputs[$k]['error'] . " \n";
+        }
+        return $s;
+    }
+
+    protected function _getPrivacyIcon($mixedPrivacy)
+    {
+        switch($mixedPrivacy) {
+            case BX_DOL_PG_MEONLY:
+                $sResult = 'lock';
+                break;
+
+            case BX_DOL_PG_ALL:
+                $sResult = 'globe';
+                break;
+
+            default:
+                $sResult = 'user';
+                break;
+        }
+
+        return $sResult;
+    }
+    
+    protected function _getPrivacyGroup($sPrivacyObject, $iInputId, $iAuthorId = false)
+    {
+        if(!$iAuthorId)
+            $iAuthorId = bx_get_logged_profile_id();
+
+        $mixedPrivacyGroup = BxDolFormQuery::getInputPrivacy($iInputId, $iAuthorId, BxDolPrivacy::getFieldName($sPrivacyObject));
+        if(empty($mixedPrivacyGroup))
+            $mixedPrivacyGroup = $this->_sPrivacyGroupDefault;
+
+        return $mixedPrivacyGroup;
+    }
+}
+
+class BxDolFormChecker
+{
+    protected $_oChecker;
+    protected $_sFormMethod;
+    protected $_aFormParams;
+    protected $_bFormCsrfChecking;
+    protected $_aSpecificValues;
+    protected $_aFieldsCheckForSpam;
+    protected $_aFieldsExcludeFromCheckForSpam;
+
+    function __construct ($sHelper = '')
+    {
+        $this->_sFormMethod = BX_DOL_FORM_METHOD_GET;
+        $this->_bFormCsrfChecking = true;
+
+        $sCheckerName = !empty($sHelper) ? $sHelper : 'BxDolFormCheckerHelper';
+        $this->_oChecker = new $sCheckerName();
+    }
+
+    function setFormMethod($sMethod, $aSpecificValues = array())
+    {
+        $this->_sFormMethod = $sMethod;
+        $this->_aSpecificValues = $aSpecificValues;
+    }
+
+    function setFormParams($aParams)
+    {
+        $this->_aFormParams = $aParams;
+    }
+
+    function setFieldsCheckForSpam($aFieldsCheckForSpam, $aFieldsExcludeFromCheckForSpam)
+    {
+        $this->_aFieldsCheckForSpam = $aFieldsCheckForSpam;
+        $this->_aFieldsExcludeFromCheckForSpam = $aFieldsExcludeFromCheckForSpam;
+    }
+    
+    function enableFormCsrfChecking($bFormCsrfChecking)
+    {
+        $this->_bFormCsrfChecking = $bFormCsrfChecking;
+    }
+
+    // check function
+    function check (&$aInputs, $aValues = array())
+    {
+        $oChecker = $this->_oChecker;
+        $iErrors = 0;
+
+        $sSubmitName = false;
+
+        foreach ($aInputs as $k => $a) {
+            if (isset($a['visible_for_levels']) && !BxDolForm::isVisible($a))
+                continue;
+
+            if (empty($a['name']) || 'submit' == $a['type'] || 'reset' == $a['type'] || 'button' == $a['type'] || 'value' == $a['type']) {
+                if (isset($a['type']) && 'submit' == $a['type'])
+                    $sSubmitName = $k;
+                continue;
+            }
+
+            if ('input_set' == $a['type'])
+                foreach ($a as $r)
+                    if (isset($r['type']) && 'submit' == $r['type'])
+                        $sSubmitName = $k;
+
+            $a['name'] = str_replace('[]', '', $a['name']);
+
+            $sMethodGetSubmittedValue = 'getSubmittedValue' . bx_gen_method_name($a['name']);
+            if(method_exists($this, $sMethodGetSubmittedValue))
+                $val = $this->$sMethodGetSubmittedValue($a['name'], $this->_sFormMethod, $this->_aSpecificValues);
+            else
+                $val = BxDolForm::getSubmittedValue($a['name'], $this->_sFormMethod, $this->_aSpecificValues);
+
+            if (isset(BxDolForm::$TYPES_FILE[$a['type']]))
+                $val = isset($_FILES[$a['name']]['name']) ? $_FILES[$a['name']]['name'] : '';
+
+            // check for unique
+            if(isset($aInputs[$k]['unique']) && $aInputs[$k]['unique'] && !empty($this->_aFormParams['db']['key']) && !empty($this->_aFormParams['db']['table'])) {
+                $sKey = $this->_aFormParams['db']['key'];
+                $sTable = $this->_aFormParams['db']['table'];
+                $sError = _t('_sys_form_err_not_unique_value');
+
+                if(!empty($aValues[$sKey])) {
+                    $sValDb = BxDolFormQuery::fieldGetValue($sTable, $aInputs[$k]['name'], $sKey, $aValues[$sKey]);
+                    if($val != $sValDb && !BxDolFormQuery::fieldCheckUnique($sTable, $aInputs[$k]['name'], $val)) {
+                        ++$iErrors;
+                        $aInputs[$k]['error'] = $sError;
+                    }
+                }
+                else if(!BxDolFormQuery::fieldCheckUnique($sTable, $aInputs[$k]['name'], $val)) {
+                    ++$iErrors;
+                    $aInputs[$k]['error'] = $sError;
+                }
+            }
+
+            // check for links in text fields
+            $sCheckerFunc = isset($a['checker']['func']) ? strtolower($a['checker']['func']) : '';
+            if (
+                !isset(BxDolForm::$FUNC_SKIP_DOMAIN_CHECK[$sCheckerFunc]) && 
+                (!isset($a['skip_domain_check']) || false == $a['skip_domain_check']) && 
+                isset(BxDolForm::$TYPES_TEXT[$a['type']]) && 
+                bx_is_url_in_content($val, true) && 
+                !isAdmin())
+            {
+                $aCheck = checkActionModule(bx_get_logged_profile_id(), 'post links', 'system');
+                if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED) {
+                    ++$iErrors;
+                    $aInputs[$k]['error'] = $aCheck[CHECK_ACTION_MESSAGE];
+                }
+                else {
+                    checkActionModule(bx_get_logged_profile_id(), 'post links', 'system', true);
+                }
+            }
+
+            // check for macros in text fields
+            $sCheckerFunc = isset($a['checker']['func']) ? strtolower($a['checker']['func']) : '';
+            if (
+                !isset(BxDolForm::$FUNC_SKIP_MACROS_CHECK[$sCheckerFunc]) && 
+                (!isset($a['skip_macros_check']) || false == $a['skip_macros_check']) && 
+                isset(BxDolForm::$TYPES_TEXT[$a['type']]) && 
+                bx_is_macros_in_content($val) && 
+                !isAdmin())
+            {
+                $aCheck = checkActionModule(bx_get_logged_profile_id(), 'use macros', 'system');
+                if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED) {
+                    ++$iErrors;
+                    $aInputs[$k]['error'] = $aCheck[CHECK_ACTION_MESSAGE];
+                }
+                else {
+                    checkActionModule(bx_get_logged_profile_id(), 'use macros', 'system', true);
+                }
+            }
+
+            if (!isset ($a['checker']))  {
+                if (isset(BxDolForm::$TYPES_CHECKBOX[$a['type']]))
+                    $aInputs[$k]['checked'] = (isset($aInputs[$k]['value']) && $aInputs[$k]['value'] == $val);
+                elseif (!isset(BxDolForm::$TYPES_FILE[$a['type']]) && !isset(BxDolForm::$TYPES_SKIP[$a['type']]))
+                    $aInputs[$k]['value'] = bx_process_input($val);
+                continue;
+            }
+
+            $sCheckFunction = array($oChecker, 'check' . bx_gen_method_name($a['checker']['func']));
+
+            if (is_callable($sCheckFunction))
+                $bool = call_user_func_array ($sCheckFunction, !empty($a['checker']['params']) ? array_merge(array($val), array_values($a['checker']['params'])) : array ($val));
+            else
+                $bool = true;
+
+            if (is_string($bool)) {
+                ++$iErrors;
+                $aInputs[$k]['error'] = $bool;
+            } elseif (!$bool) {
+                ++$iErrors;
+				if (isset($a['checker']['error']))
+                	$aInputs[$k]['error'] = $a['checker']['error'];
+            }
+
+            if (isset(BxDolForm::$TYPES_CHECKBOX[$a['type']]))
+                $aInputs[$k]['checked'] = ($aInputs[$k]['value'] == $val);
+            elseif (!isset(BxDolForm::$TYPES_FILE[$a['type']]) && !isset(BxDolForm::$TYPES_SKIP[$a['type']]))
+                $aInputs[$k]['value'] = bx_process_input($val);
+        }
+
+        // check CSRF token if it's needed.
+        if (getParam('sys_security_form_token_enable') == 'on' && $this->_bFormCsrfChecking === true) {
+            $mixedCsrfTokenUsr = BxDolForm::getSubmittedValue('csrf_token', $this->_sFormMethod, $this->_aSpecificValues);
+            unset($aInputs['csrf_token']);
+
+            if ($mixedCsrfTokenUsr === false || !BxDolForm::isCsrfTokenValid($mixedCsrfTokenUsr)) {
+                $aInputs[$sSubmitName]['error'] = _t('_sys_txt_form_submission_error_csrf_expired');
+                return false;
+            }
+        }
+
+        // check for spam
+        if (!$iErrors) {
+
+            foreach ($aInputs as $k => $a) {
+
+                if (!isset($a['name']))
+                    continue;
+
+                $a['name'] = str_replace('[]', '', $a['name']);
+
+                if ($a['type'] != 'textarea' && $a['type'] != 'text' && !in_array($a['name'], $this->_aFieldsCheckForSpam) || in_array($a['name'], $this->_aFieldsExcludeFromCheckForSpam))
+                    continue;
+                
+                $val = BxDolForm::getSubmittedValue($a['name'], $this->_sFormMethod, $this->_aSpecificValues);
+                if (!$val)
+                    continue;
+
+                if (!$oChecker->checkIsSpam($val, $a['type'])){
+					BxDolForm::setSubmittedValue($a['name'], $val, $this->_sFormMethod, $this->_aSpecificValues);
+                    continue;
+				}
+                
+                ++$iErrors;
+
+                $sErr = _t('_sys_spam_detected');
+                if (BxDolRequest::serviceExists('bx_contact', 'get_contact_page_url') && ($sUrl = BxDolService::call('bx_contact', 'get_contact_page_url')))
+                    $sErr = _t('_sys_spam_detected_contact', $sUrl);
+                $aInputs[$k]['error'] = $sErr;
+            }
+        }
+
+        // add error message near submit button
+        if ($iErrors && $sSubmitName)
+            $aInputs[$sSubmitName]['error'] = _t('_sys_txt_form_submission_error');
+    
+        return $iErrors ? false : true;
+    }
+
+    // get clean value from GET/POST
+    function get ($sName, $sPass = 'Xss', $aParams = array())
+    {
+        if (!$sPass)
+            $sPass = 'Xss';
+        $this->_oChecker;
+        $val = BxDolForm::getSubmittedValue($sName, $this->_sFormMethod, $this->_aSpecificValues);
+        return call_user_func_array (array($this->_oChecker, 'pass'.ucfirst($sPass)), $aParams ? array_merge(array($val), $aParams) : array ($val));
+    }
+
+    // db functions
+    function serializeDbValues (&$aInputs, &$aValsToAdd, &$aTrackTextFieldsChanges = null, $aUnset = null)
+    {
+        $aValsToUpdate = array();
+
+        if (null !== $aTrackTextFieldsChanges && isset($aTrackTextFieldsChanges['data']))
+            $aTrackTextFieldsChanges['changed_fields'] = array();
+
+        // get values from form description array
+        foreach ($aInputs as $k => $a) {
+            if (!isset ($a['db']) || !BxDolForm::isVisible($a)) continue;
+            $sKey = str_replace('[]', '', $a['name']);
+            $valClean = $this->get ($sKey, $a['db']['pass'], !empty($a['db']['params']) ? $a['db']['params'] : array());
+            $aValsToUpdate[$sKey] = $valClean;
+            $aInputs[$k]['db']['value'] = $valClean;
+
+            if (null !== $aTrackTextFieldsChanges && isset(BxDolForm::$TYPES_TEXT[$aInputs[$k]['type']]) && isset($aTrackTextFieldsChanges['data'][$a['name']]) && $aTrackTextFieldsChanges['data'][$a['name']] != $valClean)
+                $aTrackTextFieldsChanges['changed_fields'][] = $a['name'];
+        }
+
+        // get values which are provided manually
+        foreach ($aValsToAdd as $k => $val) {
+            $aValsToUpdate[$k] = $val;
+        }
+
+        // don't update id field for update statement
+        if ($aUnset) {
+            foreach ($aUnset as $sKey) {
+                if (isset($aValsToUpdate[$sKey]))
+                    unset($aValsToUpdate[$sKey]);
+            }
+        }
+
+        // build SQL query part
+        return BxDolDb::getInstance()->arrayToSQL($aValsToUpdate);
+    }
+
+    function dbInsert (&$aDb, &$aInputs, $aValsToAdd = array(), $isIgnore = false)
+    {
+        if (!$aDb['table'])
+            return '';
+
+        $sFields = $this->serializeDbValues ($aInputs, $aValsToAdd);
+        if (!$sFields)
+            return '';
+
+        return "INSERT " . ($isIgnore ? 'IGNORE' : '') . " INTO `{$aDb['table']}` SET $sFields";
+    }
+
+    function dbUpdate ($val, &$aDb, &$aInputs, $aValsToAdd = array(), &$aTrackTextFieldsChanges = null)
+    {
+        if (!$aDb['table'] || !$aDb['key'])
+            return '';
+
+        $oDb = BxDolDb::getInstance();
+
+        if (null !== $aTrackTextFieldsChanges && !isset($aTrackTextFieldsChanges['data'])) {
+            // get row values to compare old and new values
+            $sQuery = $oDb->prepare("SELECT * FROM `{$aDb['table']}` WHERE `{$aDb['key']}` = ?", $val);
+            $aTrackTextFieldsChanges['data'] = $oDb->getRow($sQuery);
+        }
+
+        $sFields = $this->serializeDbValues ($aInputs, $aValsToAdd, $aTrackTextFieldsChanges, array($aDb['key']));
+        if (!$sFields)
+            return '';
+
+        return $oDb->prepare("UPDATE `{$aDb['table']}` SET $sFields WHERE `{$aDb['key']}` = ?", $val);
+    }
+
+    function dbDelete ($val, &$aDb, &$aInputs)
+    {
+        if (!$aDb['table'] || !$aDb['key'])
+            return '';
+
+        $oDb = BxDolDb::getInstance();
+
+        return $oDb->prepare("DELETE FROM `{$aDb['table']}` WHERE `{$aDb['key']}` = ?", $val);
+    }
+
+    function fillWithValues (&$aInputs, &$aValues)
+    {
+        foreach ($aInputs as $k => $a) {
+            if (!isset($aValues[$k]) || isset(BxDolForm::$TYPES_SKIP[$aInputs[$k]['type']])) 
+                continue;
+
+            if (isset(BxDolForm::$TYPES_CHECKBOX[$aInputs[$k]['type']])) {
+                $aInputs[$k]['checked'] = isset($aInputs[$k]['value']) ? ($aInputs[$k]['value'] == $aValues[$k]) : false;
+            } else {
+                $sMethod = 'display' . (isset($a['db']['pass']) ? ucfirst($a['db']['pass']) : 'Undefined');
+                if (method_exists($this->_oChecker, $sMethod))
+                    $aInputs[$k]['value'] = call_user_func_array (array($this->_oChecker, $sMethod), !empty($a['db']['params']) ? array_merge(array($aValues[$k]), $a['db']['params']) : array ($aValues[$k]));
+                else
+                    $aInputs[$k]['value'] = $aValues[$k];
+            }
+        }
+    }
+}
+
+class BxDolFormCheckerHelper
+{
+    // check functions - check values for limits or patterns
+
+    static public function checkLength ($s, $iLenMin, $iLenMax)
+    {
+        if (is_array($s)) {
+            foreach ($s as $k => $v) {
+                $iLen = get_mb_len ($v);
+                if ($iLen < $iLenMin || $iLen > $iLenMax)
+                    return false;
+            }
+            return true;
+        }
+        $iLen = get_mb_len ($s);
+        return $iLen >= $iLenMin && $iLen <= $iLenMax ? true : false;
+    }
+    static public function checkDate ($s)
+    {
+        return self::checkPreg ($s, '#^\d+\-\d+\-\d+$#');
+    }
+    static public function checkDateRange ($s, $iMin, $iMax, $bRequired = false)
+    {
+        if (!$bRequired && (empty($s) || '0000-00-00' == $s || '0000-00-00Z' == $s))
+            return true;   
+        if (self::checkPreg ($s, '#^\d+\-\d+\-\d+$#')){
+            $dDate = new DateTime($s);
+            $dDateMax = new DateTime("-" . $iMin . " years");
+            $dDateMin = new DateTime("-" . $iMax . " years");
+            if ($dDate > $dDateMin && $dDate < $dDateMax){
+                return true;
+            }
+        }
+        return false;
+    }
+    static public function checkDateTime ($s)
+    {
+        // remove unnecessary opera's input value;
+        $s = str_replace('T', ' ', $s);
+        $s = str_replace('Z', ':00', $s);
+
+        return self::checkPreg ($s, '#^\d+\-\d+\-\d+[\sT]{1}\d+:\d+(:\d+)?(\+\d+:\d+)?$#');
+    }
+    static public function checkPreg ($s, $r)
+    {
+        if (is_array($s)) {
+            foreach ($s as $k => $v)
+                if (!preg_match($r, $v))
+                    return false;
+            return true;
+        }
+        return preg_match($r, $s) ? true : false;
+    }
+    static public function checkAvail ($s)
+    {
+        if (is_array($s)) {
+            return !self::_isEmptyArray($s);
+        }
+        return $s ? true : false;
+    }
+    static public function checkProfileName($s)
+    {
+        if (!self::checkAvail($s))
+            return false;
+        
+        if (self::checkPreg ($s, '#^@.+$#')){
+            return false;
+        }
+        return true;
+    }
+
+    static public function checkLocation($s, $sName, $sMethod = BX_DOL_FORM_METHOD_DEFAULT, $bAll = false)
+    {
+        $aIndexes = BxDolForm::$LOCATION_INDEXES;
+        foreach($aIndexes as $sIndex) {
+            $sValue = BxDolForm::getSubmittedValue($sName . '_' . $sIndex, $sMethod);
+            $bValue = self::checkAvail($sValue);
+
+            if(in_array($sIndex, BxDolForm::$LOCATION_INDEXES_MANDATORY) && !$bValue)
+                return false;
+            if($bAll && !$bValue)
+                return false;
+            if(!$bAll && $bValue)
+                return true;
+        }
+
+        return $bAll ? true : false;
+    }
+
+    static public function checkEmailOrEmpty($s)
+    {
+        if (empty($s))
+            return true;
+        return self::checkEmail($s);
+    }
+    static public function checkEmail($s)
+    {
+        if (false === strpos($s, '@') || strpos($s, '@') != strrpos($s, '@')) // simple check
+            return false;
+
+        if (!preg_match("/^[\pL\pNd]/u", $s)) // must start with letter or number
+            return false;
+
+        if (!preg_match("/@[\pL\pNd\.\-]+$/u", $s)) // validate domain
+            return false;
+
+        if (!getParam('sys_account_allow_plus_in_email') && false !== strpos($s, '+'))
+            return false;
+
+        $s = str_replace(array('@', '.', '-', '+', '_'), '', $s); // allowed symbols
+        if (preg_match("/[^\pL^\pNd]/u", $s)) // check for undesirable chars
+            return false;
+
+        return true;
+        // return self::checkPreg ($s, "/(([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?/");
+    }
+    static public function checkCaptcha($s)
+    {
+        $oCaptcha = BxDolCaptcha::getObjectInstance();
+        if (!$oCaptcha)
+            return true;
+        return $oCaptcha->check ();
+    }
+    static public function checkIsSpam(&$val, $sType = 'textarea')
+    {
+        $bSpam = false;
+        bx_alert('system', 'check_spam', 0, getLoggedId(), array('is_spam' => &$bSpam, 'content' => &$val, 'where' => 'form', 'type' => $sType));
+        return $bSpam;
+    }
+
+    // pass functions, prepare values to insert to database
+    static public function passInt ($s)
+    {
+        if (is_array($s)) {
+            $a = array ();
+            foreach ($s as $k => $v) {
+                $a[$k] = (int)trim($v);
+            }
+            return $a;
+        }
+        return (int)$s;
+    }
+    static public function passFloat ($s)
+    {
+        if (is_array($s)) {
+            $a = array ();
+            foreach ($s as $k => $v) {
+                $a[$k] = (float)$v;
+            }
+            return $a;
+        }
+        return (float)$s;
+    }
+    static public function passDate ($s)
+    {
+        if (is_array($s)) {
+            $a = array ();
+            foreach ($s as $k => $v) {
+                $a[$k] = self::_passDate ($v, BX_DATA_DATE);
+            }
+            return $a;
+        }
+        return self::_passDate ($s, BX_DATA_DATE);
+    }
+    static public function passDateTs ($s)
+    {
+        if (is_array($s)) {
+            $a = array ();
+            foreach ($s as $k => $v) {
+                $a[$k] = self::_passDate ($v, BX_DATA_DATE_TS);
+            }
+            return $a;
+        }
+        return self::_passDate ($s, BX_DATA_DATE_TS);
+    }
+    static public function passDateUtc ($s)
+    {
+        if (is_array($s)) {
+            $a = array ();
+            foreach ($s as $k => $v) {
+                $a[$k] = self::_passDate ($v, BX_DATA_DATE_TS_UTC);
+            }
+            return $a;
+        }
+        return self::_passDate ($s, BX_DATA_DATE_TS_UTC);
+    }
+    static public function passDateTime ($s)
+    {
+        if (is_array($s)) {
+            $a = array ();
+            foreach ($s as $k => $v) {
+                $a[$k] = self::_passDate ($v, BX_DATA_DATETIME);
+            }
+            return $a;
+        }
+        return self::_passDate ($s, BX_DATA_DATETIME);
+    }
+    static public function passDateTimeTs ($s)
+    {
+        if (is_array($s)) {
+            $a = array ();
+            foreach ($s as $k => $v) {
+                $a[$k] = self::_passDate ($v, BX_DATA_DATETIME_TS);
+            }
+            return $a;
+        }
+        return self::_passDate ($s, BX_DATA_DATETIME_TS);
+    }
+    static public function passDateTimeUtc ($s)
+    {
+        if (is_array($s)) {
+            $a = array ();
+            foreach ($s as $k => $v) {
+                $a[$k] = self::_passDate ($v, BX_DATA_DATETIME_TS_UTC);
+            }
+            return $a;
+        }
+        return self::_passDate ($s, BX_DATA_DATETIME_TS_UTC);
+    }
+    static public function _passDate ($s, $iFormat)
+    {
+        $iRet = bx_process_input ($s, $iFormat);
+        if (false === $iRet)
+            return 0;
+        return $iRet;
+    }    
+    static public function passXss ($s)
+    {
+        if (is_array($s)) {
+            $a = array ();
+            foreach ($s as $k => $v) {
+                $a[$k] = bx_process_input ($v, BX_DATA_TEXT); // "strip tags" option was here in 7.0
+            }
+            return $a;
+        }
+        return bx_process_input ($s, BX_DATA_TEXT); // "strip tags" option was here in 7.0
+    }
+
+	static public function passXssMultiline ($s)
+    {
+        if (is_array($s)) {
+            $a = array ();
+            foreach ($s as $k => $v) {
+                $a[$k] = bx_process_input ($v, BX_DATA_TEXT_MULTILINE);
+            }
+            return $a;
+        }
+        return bx_process_input ($s, BX_DATA_TEXT_MULTILINE);
+    }
+
+    static public function passXssHtml ($s)
+    {
+        if (is_array($s)) {
+            $a = array ();
+            foreach ($s as $k => $v) {
+                $a[$k] = bx_process_input ($v, BX_DATA_HTML);
+            }
+            return $a;
+        }
+        return bx_process_input ($s, BX_DATA_HTML);
+    }
+
+    static public function passAll ($s)
+    {
+        if (is_array($s)) {
+            $a = array ();
+            foreach ($s as $k => $v) {
+                $a[$k] = bx_process_input ($v);
+            }
+            return $a;
+        }
+        return bx_process_input ($s);
+    }
+
+    static public function passPreg ($s, $r)
+    {
+        if (is_array($s)) {
+            $a = array ();
+            foreach ($s as $k => $v) {
+                $a[$k] = self::_passPreg ($v, $r);
+            }
+            return $a;
+        }
+        return self::_passPreg($s, $r);
+    }
+    static public function _passPreg ($s, $r)
+    {
+        if (preg_match ($r, $s, $m)) {
+            return $m[1];
+        }
+        return '';
+    }
+    static public function passBoolean ($s)
+    {
+        if (is_array($s)) {
+            $a = array ();
+            foreach ($s as $k => $v) {
+                $a[$k] = $v == 'on' ? true : false;
+            }
+            return $a;
+        }
+        return $s == 'on' ? true : false;
+    }
+
+    static public function passSet ($s)
+    {
+        if (is_array($s)) {
+            $i = 0;
+            foreach ($s as $v)
+                $i |= pow (2, $v - 1);
+            return $i;
+        }
+        return (int)$s;
+    }
+
+    // display functions, prepare values to output to the screen
+
+    static public function displayDate ($i)
+    {
+        return bx_process_output ($i, BX_DATA_DATE_UTC);
+    }
+    static public function displayDateTs ($i)
+    {
+        return bx_process_output ($i, BX_DATA_DATETIME_TS_UTC);
+    }
+    static public function displayDateUtc ($i)
+    {
+        return bx_process_output ($i, BX_DATA_DATETIME_TS_UTC);
+    }    
+    static public function displayDateTime ($i)
+    {
+        return bx_process_output ($i, BX_DATA_DATETIME_UTC);
+    }
+    static public function displayDateTimeTs ($i)
+    {
+        return bx_process_output ($i, BX_DATA_DATETIME_TS_UTC);
+    }
+    static public function displayDateTimeUtc ($i)
+    {
+        return bx_process_output ($i, BX_DATA_DATETIME_TS_UTC);
+    }    
+    static public function displaySet ($i)
+    {
+        $i = (int)$i;
+
+        $bit = 1;
+        $bitpos = 1;
+        $a = array();
+        while ($bit < BX_DOL_INT_MAX && $bit > 0) {
+            if ($bit & $i)
+                $a[] = $bitpos;
+            $bit <<= 1;
+            $bitpos++;
+        }
+        return $a;
+    }
+
+    // for internal usage only
+
+    static public function _isEmptyArray ($a)
+    {
+        return bx_is_empty_array($a);
+    }
+
+    static public function _isFullArray ($a)
+    {
+        return bx_is_full_array($a);
+    }
+}
+
+/** @} */

+ 325 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/classes/BxDolLiveUpdates.php

@@ -0,0 +1,325 @@
+<?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 BxDolLiveUpdates extends BxDolFactory implements iBxDolSingleton
+{
+    protected $_oQuery;
+
+    /*
+     * List of all systems (active and inactive) with full description.
+     */
+    protected $_aSystems;
+
+    /*
+     * List of active systems without any additional info.
+     */
+    protected $_aSystemsActive;
+
+    /*
+     * List of dynamically added transient systems without any additional info. 
+     * It depends on the page where a user is.  
+     */
+    protected $_aSystemsTransient;  
+
+    protected $_iProfileId;
+
+    protected $_iInterval;
+
+    protected $_oCacheObject;
+    protected $_sCacheKeySystems;
+    protected $_sCacheKeyData;
+    protected $_iCacheTTL;
+
+    protected $_sJsClass;
+    protected $_sJsObject;
+
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+
+        /**
+         * Note. Currently Live Updates are associated with profiles (Profile ID) and therefore they are used for logged in members only.
+         * If it's needed Session ID can be used instead of Profile ID. In this case Live Updates can be used for visitors too. Don't forget
+         * to update BxBaseLiveUpdates::init if Session ID will be used.
+         */
+        $this->_iProfileId = (int)bx_get_logged_profile_id();
+        if(!$this->_iProfileId)
+            return;
+
+        $this->_oQuery = new BxDolLiveUpdatesQuery();
+
+        $this->_iInterval = (int)$this->_oQuery->getParam('sys_live_updates_interval');
+        
+        $this->_oCacheObject = $this->_oQuery->getDbCacheObject();
+        $this->_sCacheKeySystems = 'sys_live_updates_systems';
+        $this->_sCacheKeyData = 'sys_live_updates_data_' . $this->_iProfileId;
+        $this->_iCacheTTL = 86400;
+
+        $this->_sJsClass = 'BxDolLiveUpdates';
+    	$this->_sJsObject = 'oLiveUpdates';
+
+    	$this->_aSystems = $this->_getCachedSystems();
+    	$this->_aSystemsActive = array_keys($this->_aSystems);
+    	$this->_aSystemsTransient = array();
+    }
+
+    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 live updates instanse
+     * @return ready to use class instance
+     */
+    public static function getInstance()
+    {
+        if(!isLogged())
+            return false;
+
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxTemplLiveUpdates();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    /**
+     * 
+     * Is called via Ajax Requests only. 
+     */
+    public function perform()
+    {
+        $iIndex = (int)bx_get('index');
+
+        $mixedCacheKey = bx_get('hash');
+        if($mixedCacheKey !== false)
+            $this->_sCacheKeySystems = $this->_decodeHash(bx_process_input($mixedCacheKey));
+
+        $mixedSystemsActive = bx_get('systems_active');
+        if($mixedSystemsActive !== false)
+            $this->_aSystemsActive = array_keys($mixedSystemsActive);
+
+        $mixedSystemsTransient = bx_get('systems_transient');
+        if($mixedSystemsTransient !== false)
+            $this->_aSystemsTransient = array_keys($mixedSystemsTransient);
+
+        $this->_aSystems = $this->_getCachedSystems();
+    	if(empty($this->_aSystems) || !is_array($this->_aSystems))
+            return array();
+
+        $aCurrent = $mixedSystemsActive;
+        if(empty($aCurrent) || !is_array($aCurrent))
+            $aCurrent = $this->_getCachedData();
+
+        $aRequested = $this->_getRequestedData($iIndex, true, $aCurrent);
+
+        $aResult = array();
+        $bUpdateCache = false;
+        foreach($aRequested as $sName => $aData) {
+            if(isset($aCurrent[$sName])) {
+                if((int)$aCurrent[$sName] == (int)$aData['count'])			    
+                    continue;
+
+                $aResultData = array('count_new' => (int)$aData['count'], 'count_old' => (int)$aCurrent[$sName]);
+                if(isset($aData['data']))
+                    $aResultData = array_merge($aResultData, $aData['data']);
+
+                $aResult[] = array(
+                    'system' => $sName, 
+                    'data' => $aResultData,
+                    'method' => $aData['method']
+                );
+            }
+
+            $aCurrent[$sName] = $aData['count'];
+            $bUpdateCache = true;
+        }
+
+        if($bUpdateCache)
+            $this->_updateCached('data', $aCurrent);
+
+    	return $aResult;
+    }
+
+    protected function _addSystem($sName, $iFrequency, $sServiceCall, $bActive = true)
+    {
+        if(!$this->_iProfileId)
+            return false;
+
+        if(empty($this->_aSystemsTransient))
+            $this->_sCacheKeySystems .= $this->_getPageId();
+
+        if(!in_array($sName, $this->_aSystemsTransient))
+            $this->_aSystemsTransient[] = $sName;
+
+        if(!in_array($sName, $this->_aSystemsActive))
+            $this->_aSystemsActive[] = $sName;
+
+        if(empty($this->_aSystems[$sName])) {
+            $this->_aSystems[$sName] = array(
+                'name' => $sName,
+                'init' => 1,
+                'frequency' => $iFrequency,
+                'service_call' => $sServiceCall,
+                'active' => $bActive ? 1 : 0
+            );
+
+            $this->_updateCached('systems', $this->_aSystems);
+        }
+
+        return true;
+    }
+    
+    protected function _addData($sName, $iValue)
+    {
+        $aCachedData = $this->_getCachedData();
+        $aCachedData[$sName] = $iValue;
+        $this->_updateCached('data', $aCachedData);
+
+        return true;
+    }
+
+    protected function _getCacheInfo($sType)
+    {
+    	return array(
+            $this->_oCacheObject,
+            $this->_getCacheKey($sType),
+            $this->_iCacheTTL
+    	);
+    }
+
+    protected function _getCacheKey($sType)
+    {
+        return $this->_oQuery->genDbCacheKey($this->{'_sCacheKey' . ucfirst($sType)});
+    }
+
+    protected function _getCached($sType)
+    {
+        list($oCache, $sCacheKey, $iCacheTtl) = $this->_getCacheInfo($sType);
+
+        $aCached = $oCache->getData($sCacheKey, $iCacheTtl);
+        if(empty($aCached)) {
+            switch($sType) {
+                case 'systems':
+                    $aCached = $this->_oQuery->getSystems();
+                    break;
+
+                case 'data':
+                    if(empty($aCached))
+                        $aCached = array();
+
+                    $aRequested = $this->_getRequestedData();
+                    foreach($this->_aSystems as $sName => $aSystem)
+                        $aCached[$sName] = !empty($aRequested[$sName]['count']) ? (int)$aRequested[$sName]['count'] : 0;
+                    break;
+            }
+
+            if(!empty($aCached))
+                $oCache->setData($sCacheKey, $aCached, $iCacheTtl);
+        }
+
+        return $aCached;
+    }
+
+    protected function _getCachedSystems()
+    {
+        $aSystems = $this->_getCached('systems');
+        if(!empty($aSystems) && !empty($this->_aSystemsActive))
+            $aSystems = array_intersect_key($aSystems, array_flip($this->_aSystemsActive));
+
+        return $aSystems;
+    }
+
+    protected function _getCachedData($bInit = false)
+    {
+        $aData = $this->_getCached('data');
+        if($bInit)
+            foreach($this->_aSystems as $aSystem)
+                if(isset($aData[$aSystem['name']]) && (int)$aSystem['init'] == 0)
+                    $aData[$aSystem['name']] = 0;
+
+    	return $aData;
+    }
+
+    protected function _clearCached()
+    {
+        $this->_oCacheObject->delData($this->_getCacheKey('systems'));
+        $this->_oCacheObject->delData($this->_getCacheKey('data'));
+    }
+
+    protected function _updateCached($sType, $aData)
+    {
+    	list($oCache, $sCacheKey, $iCacheTtl) = $this->_getCacheInfo($sType);
+
+    	$aCached = $oCache->getData($sCacheKey, $iCacheTtl);
+        $aCached = array_merge($aCached ? $aCached : array(), $aData);
+
+    	$oCache->setData($sCacheKey, $aCached, $iCacheTtl);
+    }
+
+    protected function _getRequestedData($iIndex = 0, $bIndexCheck = false, $aCachedData = array())
+    {
+        $bInit = !$bIndexCheck && empty($aCachedData);
+
+        $aResult = array();
+        foreach($this->_aSystems as $sName => $aSystem) {
+            if(empty($aSystem) || !is_array($aSystem) || (int)$aSystem['active'] != 1 || ($bInit && (int)$aSystem['init'] == 0))
+                continue;
+
+            if($bIndexCheck && $iIndex % (int)$aSystem['frequency'] != 0)
+                continue;
+
+            $bCachedDataBySystem = !empty($aCachedData) && isset($aCachedData[$sName]);
+            $mixedResponce = $this->_getRequestedDataBySystem($aSystem, ($bCachedDataBySystem ? (int)$aCachedData[$sName] : 0), !$bCachedDataBySystem);
+            if($mixedResponce === false)
+                continue;
+
+            $aResult[$sName] = $mixedResponce;
+        }
+
+        return $aResult;
+    }
+
+    protected function _getRequestedDataBySystem($aSystem, $iCachedData = 0, $bInit = true)
+    {
+        if(!BxDolService::isSerializedService($aSystem['service_call']))
+            return false;
+
+        $aResponce = BxDolService::callSerialized($aSystem['service_call'], array('count' => (int)$iCachedData, 'init' => ($bInit ? 1 : 0)));
+        if(empty($aResponce) || !is_array($aResponce) || !isset($aResponce['count']) || (!$bInit && !isset($aResponce['method'])))
+            return false;
+
+        return $aResponce;
+    }
+
+    protected function _getPageId()
+    {
+        $aPageParams = array();
+        parse_str($_SERVER['QUERY_STRING'], $aPageParams);
+        $aPageParams = array_diff_assoc($aPageParams, array('start', 'per_page', 'order', 'filter'));
+
+        return '_' . $this->_iProfileId . '_' . md5(bx_append_url_params($_SERVER['PHP_SELF'], $aPageParams));
+    }
+
+    protected function _encodeHash()
+    {
+        return base64_encode($this->_sCacheKeySystems);
+    }
+
+    protected function _decodeHash($sHash)
+    {
+        return base64_decode($sHash);
+    }
+}
+
+/** @} */

+ 235 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/classes/BxDolModuleQuery.php

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

+ 839 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/classes/BxDolPage.php

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

+ 1545 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/classes/BxDolStorage.php

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

+ 3546 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/classes/BxDolTemplate.php

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

+ 545 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/classes/BxDolUploader.php

@@ -0,0 +1,545 @@
+<?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
+ * @{
+ */
+
+/**
+ * Uploaders are disigned to work as form field in forms. @see BxDolForm.
+ *
+ *
+ * To add file to any form, use the following form field array:
+ * @code
+ * 'attachment' => array(
+ *     'type' => 'files', // this is new form type, which enable upholders automatically
+ *     'storage_object' => 'sample', // the storage object, where uploaded files are going to be saved
+ *     'images_transcoder' => 'sample2', // images transcoder object to use for images preview
+ *     'uploaders' => array ('sys_simple', 'sys_html5'), // the set of uploaders to use to upload files
+ *     'upload_buttons_titles' => array('Simple' => 'Upload one by one', 'HTML5' => 'Upload several files in bulk'); // change default button titles, array with button names, or string to assign to all bnuttons
+ *     'multiple' => true, // allow to upload multiple files per one upload
+ *     'storage_private' => 0, // private or public storage (by default - private), if file is provate generated link will expire, for public storage the link is always persistent
+ *     'content_id' => 4321, // content id to associate ghost files with
+ *     'ghost_template' => $mixedGhostTemplate, // template for nested form
+ *     'name' => 'attachment', // name of file form field, resulted file id is assigned to this field name
+ *     'caption' => _t('Attachments'), // form field caption
+ * ),
+ * @endcode
+ *
+ *
+ * Available uploaders:
+ * - sys_simple - upload files using standard HTML forms.
+ * - sys_html5 - upload files using AJAX uploader with multiple files selection support (without flash),
+ *   it works in Firefox and WebKit(Safari, Chrome) browsers only, but has fallback for other browsers (IE, Opera).
+ *
+ *
+ * Uploaded files are showed as "nested" forms.
+ * You can pass nested form in 'ghost_template' parameter.
+ * If you don't pass anything in 'ghost_template' parameter, then only file id is passed upon form submission.
+ * The nested form can be declared using the different ways:
+ *
+ *
+ * 1. Pass template as string - just plain string with HTML, for example:
+ *
+ * @code
+ * <div id="bx-uploader-file-{storage_object}-{file_id}" class="bx-uploader-ghost">
+ *     <div style="border:2px dotted green; padding:10px; margin-bottom:10px;">
+ *         <input type="hidden" name="f[]" value="{file_id}" />
+ *         {file_name} <br />
+ *         <a href="javascript:void(0);" onclick="{js_instance_name}.deleteGhost('{file_id}')">delete</a>
+ *     </div>
+ * </div>
+ * @endcode
+ *
+ *
+ * 2. Pass form array - regular form array, but with inputs array only, for example:
+ *
+ * @code
+ *  array (
+ *      'inputs' => array (
+ *          'file_name' => array (
+ *              'type' => 'text',
+ *              'name' => 'file_name[]',
+ *              'value' => '{file_title}',
+ *              'caption' => _t('Caption'),
+ *          ),
+ *          'file_desc' => array (
+ *              'type' => 'textarea',
+ *              'name' => 'file_desc[]',
+ *              'caption' => _t('Description'),
+ *          ),
+ *      ),
+ *  );
+ * @endcode
+ *
+ * Array is automatically modified to add necessary form attributes to work as nested form,
+ * file id field is added automatically as hidden input as well.
+ *
+ *
+ * 3. Pass instance of BxDolFormNestedGhost class - use BxDolFormNestedGhost class or its custom subclass;
+ * to create instance use the same form array as in the previous variant, for example:
+ *
+ * @code
+ * $oFormNested = new BxDolFormNestedGhost('attachment', $aFormNested, 'do_submit');
+ * @endcode
+ *
+ * - 'attachment' is the name of file form field from main form.
+ * - $aFormNested is form array from previous example.
+ * - 'do_submit' is main form submit_name; field name of submit form input to determine if form is submitted or not.
+ *
+ *
+ * All 3 variants can have the following replace markers to substitute with real values:
+ * - {file_id} - uploaded file id
+ * - {file_name} - uploaded file name with extension
+ * - {file_title} - uploaded file name without extension
+ * - {file_icon} - URL to file icon automatically determined by file extension
+ * - {file_url} - URL to the original file
+ * - {js_instance_name} - instance of BxDolUploader javascript class
+ *
+ */
+abstract class BxDolUploader extends BxDolFactory
+{
+    protected $_oTemplate;
+
+    protected $_aObject; ///< object properties
+    protected $_sStorageObject; ///< storage object name
+
+    protected $_sUniqId; ///< uniq id used to generate UploaderJsInstance, ResultContainerId, UploadInProgressContainerId and PopupContainerId
+    protected $_sUploaderJsInstance; ///< uplooader js object instance name
+    protected $_sResultContainerId; ///< uploading/uploaded objects container id
+    protected $_sUploadInProgressContainerId; ///< container id where upload in progress element resides
+    protected $_sPopupContainerId; ///< popup container id
+
+    protected $_sUploadErrorMessages; ///< upload error message
+
+    protected $_sButtonTemplate; ///< template name for displaying upload button
+    protected $_sJsTemplate; ///< template name for displaying upload JS
+    protected $_sUploaderFormTemplate; ///< template name for displaying uploader form
+
+    protected $_aJs;
+    protected $_aCss;
+
+    /**
+     * constructor
+     */
+    protected function __construct($aObject, $sStorageObject, $sUniqId, $oTemplate)
+    {
+        parent::__construct();
+        $this->_oTemplate = $oTemplate ? $oTemplate : BxDolTemplate::getInstance();
+
+        $this->_aObject = $aObject;
+        $this->_sStorageObject = $sStorageObject;
+
+        $this->_sUniqId = $sUniqId;
+
+        $this->_sUploaderJsInstance = 'glUploader_' . $sUniqId . '_' . $this->_aObject['object'];
+        $this->_sUploadInProgressContainerId = 'bx-form-input-files-' . $sUniqId . '-upload-in-progress-' . $this->_aObject['object'];
+        $this->_sPopupContainerId = 'bx-form-input-files-' . $sUniqId . '-popup-wrapper-' . $this->_aObject['object'];
+
+        $this->_sResultContainerId = 'bx-form-input-files-' . $sUniqId . '-upload-result';
+        $this->_sErrorsContainerId = 'bx-form-input-files-' . $sUniqId . '-errors';
+        $this->_sFormContainerId = 'bx-form-input-files-' . $sUniqId . '-form-cont';
+
+        $this->_aJs = ['BxDolUploader.js'];
+        $this->_aCss = ['uploaders.css'];
+    }
+
+    static public function getObjectInstance($sObject, $sStorageObject, $sResultContainerId, $oTemplate = false)
+    {
+        $aObject = BxDolUploaderQuery::getUploaderObject($sObject);
+        if (!$aObject || !is_array($aObject) || !$aObject['active'])
+            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, $sStorageObject, $sResultContainerId, $oTemplate);
+
+        if (!$o->isInstalled() || !$o->isAvailable())
+            return false;
+
+        return $o;
+    }
+
+    /**
+     * Is uploader available?
+     * @return boolean
+     */
+    public function isAvailable()
+    {
+        return $this->_aObject['active'] ? true : false;
+    }
+
+    /**
+     * Are required php modules installed for this uploader ?
+     * @return boolean
+     */
+    public function isInstalled()
+    {
+        return true;
+    }
+
+    public function getNameJsInstanceUploader()
+    {
+        return $this->_sUploaderJsInstance;
+    }
+
+    public function getIdContainerResult()
+    {
+        return $this->_sResultContainerId;
+    }
+
+    public function getIdContainerUploadInProgress()
+    {
+        return $this->_sUploadInProgressContainerId;
+    }
+
+    public function getIdContainerPopup()
+    {
+        return $this->_sPopupContainerId;
+    }
+
+    public function getIdContainerErrors()
+    {
+        return $this->_sErrorsContainerId;
+    }
+
+    /**
+     * Handle uploads here.
+     * @param $mixedFiles as usual $_FILES['some_name'] array, but maybe some other params depending on the uploader
+     * @return nothing, but if some files failed to upload, the actual error message can be determined by calling BxDolUploader::getUploadErrorMessages()
+     */
+    public function handleUploads ($iProfileId, $mixedFiles, $isMultiple = true, $iContentId = false, $bPrivate = true)
+    {
+        $oStorage = BxDolStorage::getObjectInstance($this->_sStorageObject);
+
+        if (false == ($aMultipleFiles = $oStorage->convertMultipleFilesArray($mixedFiles)))
+            $aMultipleFiles = array($mixedFiles);
+
+        if (!$isMultiple)
+            $this->deleteGhostsForProfile($iProfileId, $iContentId);
+
+        foreach ($aMultipleFiles as $aFile) {
+
+            $iId = $oStorage->storeFileFromForm($aFile, $bPrivate, $iProfileId, $iContentId);
+            if (!$iId)
+                $this->appendUploadErrorMessage(_t('_sys_uploader_err_msg', $aFile['name'], $oStorage->getErrorString()));
+
+            if (!$isMultiple)
+                break;
+        }
+
+        echo '<script>window.parent.' . $this->getNameJsInstanceUploader() . '.onUploadCompleted(\'' . bx_js_string($this->getUploadErrorMessages(), BX_ESCAPE_STR_APOS) . '\');</script>';
+    }
+
+    public function getUploadErrorMessages ($sFormat = 'HTML')
+    {
+        if (!$this->_sUploadErrorMessages)
+            return '';
+
+        if ('HTML' == $sFormat)
+            return nl2br($this->_sUploadErrorMessages);
+        else
+            return $this->_sUploadErrorMessages;
+    }
+
+    /**
+     * Show uploader button.
+     * @return HTML string
+     */
+    public function getUploaderButton($aParams = array())
+    {
+        return $this->_oTemplate->parseHtmlByName($this->_sButtonTemplate, $aParams);
+    }
+    
+    public function getUploaderJsParams()
+    {
+        return [];
+    }
+    
+    /**
+     * Show uploader JS.
+     * @return HTML string
+     */
+    public function getUploaderJs($mixedGhostTemplate, $isMultiple = true, $aParams = array(), $bDynamic = false)
+    {
+        $sJsValue = '';
+        if(is_array($mixedGhostTemplate))
+            $sJsValue = json_encode($mixedGhostTemplate);
+        else
+            $sJsValue = "'" . bx_js_string($mixedGhostTemplate, BX_ESCAPE_STR_APOS) . "'";
+
+        $sJsObject = $this->getNameJsInstanceUploader();
+        $sJsCode = $this->_oTemplate->parseHtmlByName($this->_sJsTemplate, array_merge([
+            'uploader_instance_name' => $sJsObject,
+            'engine' => $this->_aObject['object'],
+            'storage_object' => $this->_sStorageObject,
+            'images_transcoder' => '',
+            'uniq_id' => $this->_sUniqId,
+            'template_ghost' => $sJsValue,
+            'multiple' => $isMultiple ? 1 : 0,
+            'storage_private' => isset($aParams['storage_private']) ? $aParams['storage_private'] : 1,
+            'is_init_reordering' => isset($aParams['is_init_reordering']) ? $aParams['is_init_reordering'] : 0,
+            'bx_if:restore_ghosts' => [
+                'condition' => isset($aParams['is_init_ghosts']) ? $aParams['is_init_ghosts'] : 1,
+                'content' => [
+                    'uploader_instance_name' => $sJsObject,
+                    'is_init_reordering' => isset($aParams['is_init_reordering']) ? $aParams['is_init_reordering'] : 0,
+                ]
+            ]
+        ], $aParams));
+
+        if(!$bDynamic) {
+            $this->_oTemplate->addJs($this->_aJs);
+            $sJsCode = $this->_oTemplate->addJsCodeOnLoadWrapped($sJsCode);
+        }
+        else 
+            $sJsCode = $this->_oTemplate->addJsPreloadedWrapped($this->_aJs, $sJsCode);
+
+        return $this->addCssJs($bDynamic) . $sJsCode;
+    }
+
+    /**
+     * add necessary js, css files and js translations
+     */ 
+    public function addCssJs($bDynamic = false)
+    {
+        $s = '';
+        $s .= $this->_oTemplate->addCss($this->_aCss, $bDynamic);
+        $s .= $this->_oTemplate->addJsTranslation([
+            '_sys_uploader_confirm_leaving_page',
+            '_sys_uploader_confirm_close_popup',
+            '_sys_uploader_upload_canceled',
+            '_sys_uploader_image_reposition_info',
+        ], $bDynamic);
+        return $bDynamic ? $s : '';
+    }
+
+    public function addJs($mixedFile)
+    {
+        if(!is_array($mixedFile))
+            $mixedFile = [$mixedFile];
+
+        foreach($mixedFile as $sFile)
+            if(!in_array($sFile, $this->_aJs))
+                $this->_aJs[] = $sFile;
+    }
+
+    public function addCss($mixedFile)
+    {
+        if(!is_array($mixedFile))
+            $mixedFile = [$mixedFile];
+
+        foreach($mixedFile as $sFile)
+            if(!in_array($sFile, $this->_aCss))
+                $this->_aCss[] = $sFile;
+    }
+
+    /**
+     * Get uploader button title
+     */
+    public function getUploaderButtonTitle($mixed = false)
+    {
+        // it is overrided in child classes
+    }
+
+    /**
+     * Show uploader form.
+     * @return HTML string
+     */
+    public function getUploaderForm($isMultiple = true, $iContentId = false, $isPrivate = true)
+    {
+        // it is overrided in child classes
+    }
+
+    /**
+     * Display uploaded, but not saved files - ghosts
+     * @param $iProfileId - profile id to get orphaned files from
+     * @param $sFormat - output format, only 'json' output formt is supported
+     * @param $sImagesTranscoder - transcoder object for files preview for images and videos, false by default - no preview
+     * @param $iContentId - content id to get orphaned files from, false by default
+     * @return JSON string
+     */
+    public function getGhosts($iProfileId, $sFormat, $sImagesTranscoder = false, $iContentId = false)
+    {
+        $oStorage = BxDolStorage::getObjectInstance($this->_sStorageObject);
+
+        $oImagesTranscoder = false;
+        if ($sImagesTranscoder)
+            $oImagesTranscoder = BxDolTranscoderImage::getObjectInstance($sImagesTranscoder);
+
+        $a = array();
+        $aGhosts = $oStorage->getGhosts($this->isAdmin($iContentId) && $iContentId ? false : $iProfileId, $iContentId);
+        foreach ($aGhosts as $aFile) {
+            $sFileIcon = '';
+
+            if ($this->isUseTranscoderForPreview($oImagesTranscoder, $aFile))
+                $sFileIcon = $oImagesTranscoder->getFileUrl($aFile['id']);
+
+            if (!$sFileIcon)
+                $sFileIcon = $this->_oTemplate->getIconUrl($oStorage->getIconNameByFileName($aFile['file_name']));
+
+            $aVars = array (
+            	'storage_object' => $this->_sStorageObject,
+                'file_id' => $aFile['id'],
+                'file_type' => $aFile['mime_type'],
+                'file_name' => $aFile['file_name'],
+                'file_title' => $oStorage->getFileTitle($aFile['file_name']),
+                'file_icon' => $sFileIcon,
+                'file_url' => $oStorage->getFileUrlById($aFile['id']),
+                'file_remote_id' => $aFile['remote_id'],
+                'js_instance_name' => $this->_sUploaderJsInstance,
+            );
+
+            $a[$aFile['id']] = array_merge($aVars, $this->getGhostTemplateVars($aFile, $iProfileId, $iContentId, $oStorage, $oImagesTranscoder));
+        }
+
+        if ('array' == $sFormat) {
+            return $a;
+        }
+        else if ('json' == $sFormat) {
+            return json_encode($a);
+        } else { // html format is not suported for this data type
+            return false;
+        }
+    }
+
+    public function getGhostsWithOrder($iProfileId, $sFormat, $sImagesTranscoder = false, $iContentId = false)
+    {
+        $a = $this->getGhosts($iProfileId, 'array', $sImagesTranscoder, $iContentId);
+        if(!empty($a) && is_array($a))
+            $a = ['g' => $a, 'o' => array_keys($a)];
+
+        if ('json' == $sFormat) {
+            return json_encode($a);
+        } else { // html format is not suported for this data type
+            return false;
+        }
+    }
+    
+    /**
+     * Reorder uploaded ghosts.
+     * @param $iProfileId - profile id to get orphaned files from
+     * @param $sFormat - output format, only 'json' output formt is supported
+     * @param $aGhosts - an array of ordered ghosts' IDs.
+     * @param $iContentId - content id to order orphaned files for, false by default
+     * @return JSON string
+     */
+    public function reorderGhosts($iProfileId, $sFormat, $aGhosts, $iContentId = false)
+    {
+        $bResult = true;
+        if(($oStorage = BxDolStorage::getObjectInstance($this->_sStorageObject)) !== false)
+            $bResult = $oStorage->reorderGhosts($this->isAdmin($iContentId) && $iContentId ? false : $iProfileId, $iContentId, $aGhosts);
+
+        if($sFormat == 'json')
+            return json_encode($bResult ? [] : ['msg' => _t('_error occured')]);
+        else
+            return $bResult;
+    }
+
+    /**
+     * Delete file by file id, usually ghost file
+     * @return 'ok' string on success or error string on error
+     */
+    public function deleteGhost($iFileId, $iProfileId)
+    {
+        $oStorage = BxDolStorage::getObjectInstance($this->_sStorageObject);
+
+        $aFile = $oStorage->getGhost ($iFileId);
+        if (!$aFile)
+            $aFile = $oStorage->getFile ($iFileId);
+        if (!$aFile)
+            return _t('_error occured');
+
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        $oAccount = $oProfile ? $oProfile->getAccountObject() : null;
+        $aProfiles = $oAccount ? $oAccount->getProfiles(false) : array();
+        if (!isset($aProfiles[$aFile['profile_id']]) && !$this->isAdmin($aFile['content_id']))
+            return _t('_sys_txt_access_denied');
+
+        if (!$oStorage->deleteFile($iFileId))
+            return $oStorage->getErrorString();
+
+        return 'ok';
+    }
+
+    /**
+     * Delete all ghosts files for the specified profile
+     * @return number of delete ghost files
+     */
+    public function deleteGhostsForProfile($iProfileId, $iContentId = false)
+    {
+        $iCount = 0;
+
+        $oStorage = BxDolStorage::getObjectInstance($this->_sStorageObject);
+
+        $aGhosts = $oStorage->getGhosts($iProfileId, $iContentId, $iContentId ? true : false);
+        foreach ($aGhosts as $aFile)
+            $iCount += $oStorage->deleteFile($aFile['id']);
+
+        return $iCount;
+    }
+
+    protected function cleanUploadErrorMessages ()
+    {
+        $this->_sUploadErrorMessages = '';
+    }
+
+    public function appendUploadErrorMessage ($s)
+    {
+        $this->_sUploadErrorMessages .= ($this->_sUploadErrorMessages ? "\n" : '') . $s;
+    }
+
+    protected function getRestrictionsText ()
+    {
+        $sTextRestrictions = '';
+        $oStorage = BxDolStorage::getObjectInstance($this->_sStorageObject);
+        if (!$oStorage)
+            return '';
+
+        $a = $oStorage->getRestrictionsTextArray(bx_get_logged_profile_id());
+        foreach ($a as $s)
+            $sTextRestrictions .= '<div class="bx-uploader-msg-info bx-def-font-grayed">' . $s . '</div>';
+
+        return $sTextRestrictions;
+    }
+
+    protected function getMaxUploadFileSize ()
+    {
+        $oStorage = BxDolStorage::getObjectInstance($this->_sStorageObject);
+        if (!$oStorage)
+            return 0;
+        return $oStorage->getMaxUploadFileSize(bx_get_logged_profile_id());
+    }
+
+    protected function getAcceptedFilesExtensions ()
+    {
+        $oStorage = BxDolStorage::getObjectInstance($this->_sStorageObject);
+        if (!$oStorage)
+            return null;
+        return $oStorage->getAllowedExtensions();
+    }
+
+    protected function getGhostTemplateVars($aFile, $iProfileId, $iContentId, $oStorage, $oImagesTranscoder)
+    {
+        return array();
+    }
+
+    protected function isUseTranscoderForPreview($oImagesTranscoder, $aFile)
+    {
+        if (!$oImagesTranscoder)
+            return false;
+
+        return $oImagesTranscoder->isMimeTypeSupported($aFile['mime_type']);
+    }
+
+    protected function isAdmin ($iContentId = 0)
+    {
+        return isAdmin();
+    }
+}
+
+/** @} */

+ 1043 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/js/classes/BxDolCmts.js

@@ -0,0 +1,1043 @@
+/**
+ * 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._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 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));
+    });
+};
+
+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
+                if(oData && oData.count != undefined && parseInt(oData.count) > 0) {
+                    if(oData.countf != undefined && oData.countf.length != 0) {
+                        var oCounter = $this._getCounter(oCmtForm);
+                        if(oCounter && oCounter.length > 0)
+                            oCounter.html(oData.countf);
+                    }
+
+                    var oCounter = $this._getCounter(oCmtForm, 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);
+                }
+            }
+
+            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.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.showMore = function(oLink)
+{
+	$(oLink).parent('span').next('span').show().prev('span').remove();
+};
+
+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(e, iCmtParentId, iQuote)
+{
+    var $this = this;
+    var aParams = {
+        CmtParent: iCmtParentId,
+        CmtQuote: iQuote != undefined ? parseInt(iQuote) : 0
+    };
+    var fOnShow = function() {
+        $(this).find('textarea:first').focus();
+    };
+
+    var sParentId = 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 ($(sParentId + ' > ' + sReplyIdOpst + ':visible').length)
+        $(sParentId + ' > ' + sReplyIdOpst).hide();
+
+    if ($(sParentId + ' > ' + sReplyId).length) {
+        $(sParentId + ' > ' + sReplyId).bx_anim('toggle', this._sAnimationEffect, this._iAnimationSpeed, fOnShow);
+        return;
+    }
+
+    this._getForm(e, aParams, function(sForm) {
+        var oForm = $(sForm).hide().addClass('cmt-reply-' + $this._sPostFormPosition).addClass('cmt-reply-margin');
+        var oFormSibling = $(sParentId + ' > ul.cmts:first');
+        switch($this._sPostFormPosition) {
+            case 'top':
+                oFormSibling.before(oForm);
+                break;
+
+            case 'bottom':
+                oFormSibling.after(oForm);
+                break;
+        }
+
+        $this.cmtInitFormPost(oForm.find('form'));
+
+        $(sParentId).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();
+
+            $this._loadingInButton(oLink, false);
+            $(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.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();
+	    }
+	);
+};
+
+/*----------------------------------*/
+/*--- 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)
+                    $(this).children('li.cmt:last').after($(oData.content).hide()).next('li.cmt:hidden').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);
+            });
+        },
+        '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));
+
+			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));
+
+			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._getCounter = function(oElement, bText)
+{
+    var oCounter = null;
+    var sSelector = '.' + this._sSP + '-counter' + (bText ? '-text' : '');
+
+    if($(oElement).hasClass(this._sSP))
+        oCounter = $(oElement).find(sSelector);
+    else 
+        oCounter = $(oElement).parents('.' + this._sSP + ':first').find(sSelector);
+
+    if(!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);
+};
+
+/** @} */

+ 89 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/js/classes/BxDolPage.js

@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+function BxDolPage(oOptions) {
+    this._sObjName = oOptions.sObjName == undefined ? 'oBxDolPage' : oOptions.sObjName;
+    this._sObject = oOptions.sObject; // page object
+
+    this._sActionsUri = 'page.php';
+    this._sActionsUrl = oOptions.sRootUrl; // actions url address
+    this._aHtmlIds = oOptions.aHtmlIds;
+
+    this._isStickyColumns = oOptions.isStickyColumns == undefined ? false : oOptions.isStickyColumns;
+    this._iLastSc = 0;
+
+    var $this = this;
+    $(document).ready(function() {
+        $this.init();
+    });
+}
+
+BxDolPage.prototype.init = function() {
+    var $this = this;
+    if ($this._isStickyColumns && !$('html').hasClass('bx-media-phone')) {
+        $('.bx-layout-cell').addClass('sticky top-4');
+    }
+    // process embeds
+    $(".bx-embed-link").each(function() {
+        $(this).html($(this).attr('source'));
+        $.getJSON(sUrlRoot + '/embed.php?', {a: 'get_link', l: $(this).attr('source')}, $this.embededCallback($(this)));
+    });
+    
+    bx_process_links();
+};
+
+BxDolPage.prototype.embededCallback = function(item)
+{
+    return function(oData) {
+        item.html(oData.code)
+        if (item.find('a').length > 0){
+            bx_embed_link(item.find('a')[0]);
+        }
+    };
+}
+
+BxDolPage.prototype.share = function(oLink, sUrl)
+{
+    var oData = this._getDefaultParams();
+    oData['a'] = 'get_share';
+    oData['url'] = sUrl;
+    $(".bx-popup-applied:visible").dolPopupHide();
+
+    $(window).dolPopupAjax({
+        url: bx_append_url_params(this._sActionsUri, oData),
+        displayMode: 'box',
+        closeOnOuterClick: false,
+        id: {value: 'sys_share_popup', force: true}
+    });
+}
+
+BxDolPage.prototype.showHelp = function(oLink, iBlockId)
+{
+    var oData = this._getDefaultParams();
+    oData['a'] = 'get_help';
+    oData['block_id'] = iBlockId;
+
+    $(oLink).dolPopupAjax({
+        id: {value:this._aHtmlIds['help_popup'] + iBlockId, force:1}, 
+        url: bx_append_url_params(this._sActionsUri, oData),
+        closeOnOuterClick: true,
+        removeOnClose: true,
+        onBeforeShow: function(oPopup) {
+            oPopup.addClass('bx-popup-help');
+        }
+    });
+};
+BxDolPage.prototype._getDefaultParams = function() 
+{
+    var oDate = new Date();
+    return {
+        o: this._sObject,
+        _t: oDate.getTime()
+    };
+};
+
+/** @} */

+ 1104 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/js/classes/BxDolUploader.js

@@ -0,0 +1,1104 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Simple Uploader js class
+ */
+
+
+function BxDolUploaderBase (sUploaderObject, sStorageObject, sUniqId, options) {
+
+    this.init(sUploaderObject, sStorageObject, sUniqId, options);
+
+    this._sIframeId = 'bx-form-input-files-' + sUniqId + '-iframe';
+    this._eForm = null;
+}
+
+BxDolUploaderBase.prototype.init = function (sUploaderObject, sStorageObject, sUniqId, options) {
+    this._isUploadsInProgress = false;
+
+    this._sUploaderObject = sUploaderObject;
+    this._sStorageObject = sStorageObject;
+
+    this._sUniqId = sUniqId;
+
+    this._sUploaderJsInstance = 'glUploader_' + sUniqId + '_' + this._sUploaderObject;
+    this._sUploadInProgressContainerId = 'bx-form-input-files-' + sUniqId + '-upload-in-progress-' + this._sUploaderObject;
+    this._sPopupContainerId = 'bx-form-input-files-' + sUniqId + '-popup-wrapper-' + this._sUploaderObject;
+
+    this._sResultContainerId = 'bx-form-input-files-' + sUniqId + '-upload-result';
+    this._sErrorsContainerId = 'bx-form-input-files-' + sUniqId + '-errors'; 
+    this._sProgressContainerId = 'bx-form-input-files-' + sUniqId + '-progress'; 
+
+    this._sFormContainerId = 'bx-form-input-files-' + sUniqId + '-form-cont';
+
+    this._sTemplateGhost = options.template_ghost ? options.template_ghost : '<div id="' + this._getFileContainerId('{file_id}') + '"><input type="hidden" name="f[]" value="{file_id}" />{file_name} (<a href="javascript:void(0);" onclick="{js_instance_name}.deleteGhost(\'{file_id}\')">delete</a>)</div>';
+    this._sTemplateReorder = options.template_reorder ? options.template_reorder : '<div class="bx-uploader-ghost-reorder bx-btn bx-btn-small"><i class="sys-icon bars"></i></div>';
+    this._sTemplateError = options.template_error_msg ? options.template_error_msg : '<div>{error}</div>' ;
+    this._sTemplateErrorGhosts = options.template_error_ghosts ? options.template_error_ghosts : this._sTemplateError;
+
+    this._isMultiple = undefined == options.multiple || !options.multiple ? false : true;
+    this._isReordering = undefined == options.reordering || !options.reordering ? false : true;
+
+    this._iContentId = undefined == options.content_id || '' == options.content_id ? '' : parseInt(options.content_id);
+
+    this._sImagesTranscoder = options.images_transcoder ? options.images_transcoder : '';
+
+    this._isPrivate = undefined == options.storage_private || parseInt(options.storage_private) ? 1 : 0;
+
+    this._isErrorShown = false;
+}
+
+BxDolUploaderBase.prototype.isMultiple = function () {    
+    return this._isMultiple;
+}
+
+BxDolUploaderBase.prototype.getCurrentFilesCount = function () {
+    return $('#' + $this._sResultContainerId + ' .bx-uploader-ghost').length;
+}
+
+BxDolUploaderBase.prototype.showUploaderForm = function () {
+    var $this = this;
+    var sUrl = this._getUrlWithStandardParams() + '&a=show_uploader_form&m=' + (this._isMultiple ? 1 : 0) + '&c=' + this._iContentId + '&p=' + this._isPrivate + '&_t=' + escape(new Date());
+
+    $(window).dolPopupAjax({
+        url: sUrl,
+        id: {force: true, value: this._sPopupContainerId},
+        onBeforeShow: function() {
+            if ($this.isMultiple())
+                $('#' + $this._sPopupContainerId + ' .bx-uploader-add-more-files').show();
+            else
+                $('#' + $this._sPopupContainerId + ' .bx-uploader-add-more-files').hide();
+            $('#' + $this._sPopupContainerId + ' .bx-popup-element-close').click(function() {
+                $this.onClickCancel();
+            });
+			$("#" + $this._sFormContainerId + " .bx-btn.bx-btn-primary:not(.bx-crop-upload)").hide();
+            if ('undefined' !== typeof($this.onBeforeShowPopup))
+                $this.onBeforeShowPopup();
+        },
+        onShow: function () {
+            if ('undefined' !== typeof($this.onShowPopup))
+                $this.onShowPopup();
+        },
+        closeElement: false,
+        closeOnOuterClick: false
+    });
+}
+
+BxDolUploaderBase.prototype.onClickCancel = function () {
+	var $this = this;
+    if (this._isUploadsInProgress) {
+    	bx_confirm(_t('_sys_uploader_confirm_close_popup'), function() {
+    		$this.cancelAll();
+            $('#' + $this._sPopupContainerId).dolPopupHide({});
+    	});
+    } else {
+        $('#' + this._sPopupContainerId).dolPopupHide();
+    }
+}
+
+BxDolUploaderBase.prototype.onBeforeUpload = function (params) {
+
+    this._eForm = params;
+
+    this._loading(true, false);
+        
+    this._isUploadsInProgress = true;
+    this._lockPageFromLeaving();
+    this._clearErrors();
+}
+
+BxDolUploaderBase.prototype.onProgress = function (params) {
+
+}
+
+BxDolUploaderBase.prototype.onUploadCompleted = function (sErrorMsg) {
+
+    this._isUploadsInProgress = false;
+    this._unlockPageFromLeaving();
+    this._loading(false, true);
+
+    if (sErrorMsg.length) {
+        this.restoreGhosts();        
+        this._showError(sErrorMsg);
+    } else {        
+        this.restoreGhosts();
+        $('#' + this._sPopupContainerId).dolPopupHide({});
+    }
+}
+
+BxDolUploaderBase.prototype.cancelAll = function () {
+    $('#' + this._sIframeId).attr('src', 'javascript:false;');
+    this.onUploadCompleted(_t('_sys_uploader_upload_canceled'));
+}
+
+BxDolUploaderBase.prototype.restoreGhosts = function (bInitReordering, onComplete) {
+    var sUrl = this._getUrlWithStandardParams() + '&img_trans=' + this._sImagesTranscoder + '&a=restore_ghosts&f=json' + '&c=' + this._iContentId + '&_t=' + escape(new Date());
+    var $this = this;
+
+    bInitReordering = bInitReordering !== undefined ? bInitReordering : this._isReordering;
+
+    $.getJSON(sUrl, function (aData) {
+
+        if (!$this.isMultiple())
+            $('#' + $this._sResultContainerId + ' .bx-uploader-ghost').remove();
+
+        if ('object' === typeof(aData)) {
+            if('object' === typeof(aData.g) && 'object' === typeof(aData.o)) 
+                for(var i in aData.o) {
+                    var iFileId = aData.o[i];
+                    $this.showGhost(iFileId, aData.g[iFileId]);
+                }
+            else
+                $.each(aData, function(iFileId, oVars) {
+                    $this.showGhost(iFileId, oVars);
+                });
+
+            $('#' + $this._sResultContainerId).bx_show_more_check_overflow();
+
+            if(bInitReordering) {
+                var sClassGhost = 'bx-uploader-ghost';
+                $('#' + $this._sResultContainerId).find('.' + sClassGhost).each(function() {
+                    if($(this).find('.bx-uploader-ghost-reorder').length == 0)
+                        $(this).find('.bx-base-general-uploader-ghost').prepend($this._sTemplateReorder);
+                });
+
+                var fInitReordering = function() {                    
+                    $('#' + $this._sResultContainerId).sortable({
+                        items: '.' + sClassGhost, 
+                        start: function(oEvent, oUi) {
+                            oUi.item.addClass('bx-uploader-ghost-dragging');
+                        },
+                        stop: function(oEvent, oUi) {
+                            oUi.item.removeClass('bx-uploader-ghost-dragging');
+
+                            $this.reorderGhosts(oUi.item);
+                        }
+                    });
+                };
+
+                if($.sortable !== undefined)
+                    fInitReordering();
+                else
+                    setTimeout(fInitReordering, 2000);
+            }
+        }
+
+        if(typeof onComplete === 'function')
+            return onComplete(aData);
+    });
+};
+
+BxDolUploaderBase.prototype.reorderGhosts = function(oDraggable) {
+    var sUrl = this._getUrlWithStandardParams() + '&a=reorder_ghosts&f=json' + '&c=' + this._iContentId + '&' + $('#' + this._sResultContainerId).sortable('serialize', {key: 'ghosts[]'}) + '&_t=' + escape(new Date());
+
+    $.getJSON(sUrl, function (aData) {
+        processJsonData(aData);
+    });
+};
+
+BxDolUploaderBase.prototype.showGhost = function(iId, oVars) {
+    var oFileContainer = $('#' + this._getFileContainerId(iId));
+    if(oFileContainer.length > 0)
+        return;
+
+    var sHTML;
+    if (typeof this._sTemplateGhost == 'object')
+        sHTML = this._sTemplateGhost[iId];
+    else
+        sHTML = this._sTemplateGhost;
+
+    for(var i in oVars)
+        sHTML = sHTML.replace (new RegExp('{' + i + '}', 'g'), oVars[i]);
+    
+    $('#' + this._sResultContainerId).append(sHTML).trigger( "contentchange" );
+
+    oFileContainer.find('.bx-uploader-ghost-preview img').hide().fadeIn(1000);
+};
+
+BxDolUploaderBase.prototype.deleteGhost = function (iFileId) {
+    var sUrl = this._getUrlWithStandardParams() + '&a=delete&id=' + iFileId;
+    var $this = this;
+
+    var sFileContainerId = $this._getFileContainerId(iFileId);
+    bx_loading(sFileContainerId, true);
+
+    $.post(sUrl, {_t: escape(new Date())}, function (sMsg) {
+        bx_loading(sFileContainerId, false);
+        if ('ok' == sMsg) {
+            $('#' + sFileContainerId).slideUp('slow', function () {
+                $(this).remove();
+            });
+        } else {
+            $('#' + this._sResultContainerId).prepend($this._sTemplateErrorGhosts.replace('{error}', sMsg));
+        }        
+    });
+};
+
+BxDolUploaderBase.prototype._clearErrors = function () {
+    $(' #' + this._sErrorsContainerId).html('');
+    this._isErrorShown = false;
+}
+
+BxDolUploaderBase.prototype._showError = function (s, bAppend) {
+    if (s == undefined || !s.length)
+        return;
+    if (!bAppend)
+        $('#' + this._sErrorsContainerId).html(this._sTemplateError.replace('{error}', s));
+    else
+        $('#' + this._sErrorsContainerId).prepend(this._sTemplateError.replace('{error}', s));
+    this._isErrorShown = true;
+};
+
+BxDolUploaderBase.prototype._getFileContainerId = function (iFileId) {
+	return 'bx-uploader-file-' + this._sStorageObject + '-' + iFileId;
+};
+
+BxDolUploaderBase.prototype._getUrlWithStandardParams = function () {
+    return sUrlRoot + 'storage_uploader.php?uo=' + this._sUploaderObject + '&so=' + this._sStorageObject + '&uid=' + this._sUniqId;
+}
+
+BxDolUploaderBase.prototype._lockPageFromLeaving = function () {
+    $(window).bind('beforeunload', function (e) {
+        var e = e || window.event;
+        // for ie, ff
+        e.returnValue = _t('_sys_uploader_confirm_leaving_page');
+        // for webkit
+        return _t('_sys_uploader_confirm_leaving_page');
+    });
+}
+
+BxDolUploaderBase.prototype._unlockPageFromLeaving = function () {
+    $(window).unbind('beforeunload');
+}
+
+BxDolUploaderBase.prototype._loading = function (bShowProgress, bShowForm) {
+
+    var eForm = $('#' + this._sFormContainerId + ' .bx-uploader-files-list');
+    var eBtn = $('#' + this._sFormContainerId + ' .bx-btn-primary');
+
+    if (bShowForm) {
+        if (null != this._eForm) {
+            eForm.find('.bx-uploader-simple-file').filter(':not(:first)').remove();
+            if ('undefined' !== typeof(this._eForm.reset)) 
+                this._eForm.reset();
+        }
+        eForm.show();
+        eBtn.show();
+    } else {
+        eForm.hide();
+        eBtn.hide();
+    }
+
+    bx_loading($('#' + this._sFormContainerId + ' .bx-uploader-loading').get(0), bShowProgress);
+}
+
+BxDolUploaderBase.prototype.getMimeTypefromString  = function (ext) {
+    
+    var mimeTypes = 
+    {
+        
+        '3gp'    : 'video/3gpp',
+        '3g2'    : 'video/3gpp2',
+        'a'      : 'application/octet-stream',
+        'ai'     : 'application/postscript',
+        'aif'    : 'audio/x-aiff',
+        'aifc'   : 'audio/x-aiff',
+        'aiff'   : 'audio/x-aiff',
+        'asf'   : 'video/x-ms-asf',
+        'au'     : 'audio/basic',
+        'avi'    : 'video/x-msvideo',
+        'avi'    : 'video/avi',
+        'bat'    : 'text/plain',
+        'bin'    : 'application/octet-stream',
+        'bmp'    : 'image/x-ms-bmp',
+        'c'      : 'text/plain',
+        'cdf'    : 'application/x-cdf',
+        'csh'    : 'application/x-csh',
+        'css'    : 'text/css',
+        'divx'   : 'video/divx',
+        'dll'    : 'application/octet-stream',
+        'doc'    : 'application/msword',
+        'dot'    : 'application/msword',
+        'drc'    : 'video/drc',
+        'dvi'    : 'application/x-dvi',
+        'eml'    : 'message/rfc822',
+        'eps'    : 'application/postscript',
+        'etx'    : 'text/x-setext',
+        'exe'    : 'application/octet-stream',
+        'gif'    : 'image/gif',
+        'gtar'   : 'application/x-gtar',
+        'h'      : 'text/plain',
+        'hdf'    : 'application/x-hdf',
+        'htm'    : 'text/html',
+        'html'   : 'text/html',
+        'jpe'    : 'image/jpeg',
+        'jpeg'   : 'image/jpeg',
+        'jpg'    : 'image/jpeg',
+        'js'     : 'application/x-javascript',
+        'ksh'    : 'text/plain',
+        'latex'  : 'application/x-latex',
+        'm1v'    : 'video/mpeg',
+        'man'    : 'application/x-troff-man',
+        'me'     : 'application/x-troff-me',
+        'mht'    : 'message/rfc822',
+        'mhtml'  : 'message/rfc822',
+        'mkv'    : 'video/x-matroska',
+        'mif'    : 'application/x-mif',
+        'mov'    : 'video/quicktime',
+        'movie'  : 'video/x-sgi-movie',
+        'mp2'    : 'audio/mpeg',
+        'mp3'    : 'audio/mpeg',
+        'mp4'    : 'video/mp4',  
+        'm4v'    : 'video/mp4', 
+        'mpa'    : 'video/mpeg',
+        'mpe'    : 'video/mpeg',
+        'mpeg'   : 'video/mpeg',
+        'mpg'    : 'video/mpeg',
+        'ms'     : 'application/x-troff-ms',
+        'nc'     : 'application/x-netcdf',
+        'nws'    : 'message/rfc822',
+        'o'      : 'application/octet-stream',
+        'obj'    : 'application/octet-stream',
+        'oda'    : 'application/oda',
+        'ogv'    : 'video/ogg',
+        'ogg'    : 'video/ogg',
+        'pbm'    : 'image/x-portable-bitmap',
+        'pdf'    : 'application/pdf',
+        'pfx'    : 'application/x-pkcs12',
+        'pgm'    : 'image/x-portable-graymap',
+        'png'    : 'image/png',
+        'pnm'    : 'image/x-portable-anymap',
+        'pot'    : 'application/vnd.ms-powerpoint',
+        'ppa'    : 'application/vnd.ms-powerpoint',
+        'ppm'    : 'image/x-portable-pixmap',
+        'pps'    : 'application/vnd.ms-powerpoint',
+        'ppt'    : 'application/vnd.ms-powerpoint',
+        'pptx'   : 'application/vnd.ms-powerpoint',
+        'ps'     : 'application/postscript',
+        'pwz'    : 'application/vnd.ms-powerpoint',
+        'py'     : 'text/x-python',
+        'pyc'    : 'application/x-python-code',
+        'pyo'    : 'application/x-python-code',
+        'qt'     : 'video/quicktime',
+        'ra'     : 'audio/x-pn-realaudio',
+        'ram'    : 'application/x-pn-realaudio',
+        'ras'    : 'image/x-cmu-raster',
+        'rdf'    : 'application/xml',
+        'rgb'    : 'image/x-rgb',
+        'rm'     : 'application/vnd.rn-realmedia',
+        'rmvb'   : 'application/vnd.rn-realmedia-vbr',
+        'roff'   : 'application/x-troff',
+        'rtx'    : 'text/richtext',
+        'sgm'    : 'text/x-sgml',
+        'sgml'   : 'text/x-sgml',
+        'sh'     : 'application/x-sh',
+        'shar'   : 'application/x-shar',
+        'snd'    : 'audio/basic',
+        'so'     : 'application/octet-stream',
+        'src'    : 'application/x-wais-source',
+        'swf'    : 'application/x-shockwave-flash',
+        't'      : 'application/x-troff',
+        'tar'    : 'application/x-tar',
+        'tcl'    : 'application/x-tcl',
+        'tex'    : 'application/x-tex',
+        'texi'   : 'application/x-texinfo',
+        'texinfo': 'application/x-texinfo',
+        'tif'    : 'image/tiff',
+        'tiff'   : 'image/tiff',
+        'tr'     : 'application/x-troff',
+        'tsv'    : 'text/tab-separated-values',
+        'txt'    : 'text/plain',
+        'ustar'  : 'application/x-ustar',
+        'vcf'    : 'text/x-vcard',
+        'wav'    : 'audio/x-wav',
+        'wav'    : 'audio/wav',
+        'webm'   : 'video/webm',
+        'wmv'    : 'video/wmv',
+        'wiz'    : 'application/msword',
+        'wsdl'   : 'application/xml',
+        'xbm'    : 'image/x-xbitmap',
+        'xlb'    : 'application/vnd.ms-excel',
+        'xls'    : 'application/vnd.ms-excel',
+        'xlsx'    : 'application/vnd.ms-excel',
+        'xml'    : 'text/xml',
+        'xpdl'   : 'application/xml',
+        'xpm'    : 'image/x-xpixmap',
+        'xsl'    : 'application/xml',
+        'xvid'   : 'video/xvid',
+        'webp'   : 'image/webp',
+        'xwd'    : 'image/x-xwindowdump',
+        'svg'    : 'image/svg+xml',
+        'zip'    : ['application/zip', 'application/x-zip-compressed']
+       
+    }
+    return mimeTypes[ext.replace('.', '')];
+}
+
+/**
+ * HTML5 Uploader js class
+ */
+function BxDolUploaderHTML5 (sUploaderObject, sStorageObject, sUniqId, options) {
+
+    this.init(sUploaderObject, sStorageObject, sUniqId, options);
+
+    this._sIframeId = 'bx-form-input-files-' + sUniqId + '-iframe';
+    this._eForm = null;    
+    this._sDivId = 'bx-form-input-files-' + sUniqId + '-div-' + this._sUploaderObject;
+    this._sFocusDivId = 'bx-form-input-files-' + sUniqId + '-focus-' + this._sUploaderObject;
+    this._uploader = null;
+    this._aFiles = [];
+  
+
+    this.initUploader = function (o) {
+
+        var $this = this;
+
+        if (null != this._uploader)
+            this._uploader = null;
+
+        var $this = this;
+        var _options = {
+            allowProcess: false,
+            allowPaste: false,
+            allowRevert: false,
+            allowRemove: false,
+            imagePreviewHeight: 100,
+            credits: {},
+            allowMultiple: $this._isMultiple ? true : false,
+            maxFiles: $this.isMultiple() ? 50 : 1,
+            maxFileSize: o.maxFilesize +'MB',
+            instantUpload: true,
+            onaddfile: (error, file) => { 
+                if (error){
+                    $this._uploader.removeFile(file);
+                    if($this._uploader.status != 3)
+                        $this.onUploadCompleted(''); 
+                }
+                else{
+                    $this.onBeforeUpload(''); 
+                }
+            },
+            onerror: (error) => { 
+                if (error.main)
+                    $this._showError(error.main + '. ' + error.sub);
+            },
+            onprocessfiles: (files) => { 
+                $this.onUploadCompleted(''); 
+                oProgress.hide();
+                $this._aFiles = [];
+            },
+            onprocessfileprogress(file, progress) {
+                $this._aFiles[file.source.lastModified+'-'+file.source.size+'-'+file.source.name] = progress * file.source.size;
+                iTotal = 0;
+                iUploaded = 0; 
+                for (const propertyName in $this._aFiles) {
+                    iUploaded += $this._aFiles[propertyName];
+                }
+                
+                for (i = 0; i < $this._uploader.getFiles().length; i++){
+                    iTotal += $this._uploader.getFiles()[i].source.size;
+                }
+            
+                iProgress = iUploaded / iTotal * 100;
+            
+                oProgress =  $('#' + $this._sProgressContainerId);
+                
+                if (oProgress.parents('.bx-db-container').find('.uploader_progress').length > 0){
+                    oProgress = oProgress.parents('.bx-db-container').find('.uploader_progress');
+                }
+                
+                oProgress.show();
+                oProgress.find('.progress_line').css('width', iProgress + '%');
+            },
+            server: {
+				process: (fieldName, file, metadata, load, error, progress, abort) => {
+					const formData = new FormData();
+					formData.append('file', file, file.name);
+					formData.append('uo', this._sUploaderObject);
+					formData.append('so', this._sStorageObject);
+					formData.append('uid', this._sUniqId);
+					formData.append('m', this._isMultiple ? 1 : 0);
+					formData.append('c', this._iContentId);
+					formData.append('p', this._isPrivate);
+					formData.append('a', 'upload');
+
+					const request = new XMLHttpRequest();
+					request.open('POST', sUrlRoot + 'storage_uploader.php');
+
+					request.upload.onprogress = (e) => {
+						progress(e.lengthComputable, e.loaded, e.total);
+					};
+
+					request.onload = function(res) {
+						if (request.status >= 200 && request.status < 300) {
+							var o = null;
+                            try {
+                                o = JSON.parse(request.responseText);
+                            } 
+                            catch (e) {}
+                            if (o && 'undefined' !== typeof(o.error)) {
+                                $this._showError(o.error, true);
+                            }
+                            
+                            load(request.responseText);
+						}
+						else {
+							error('error');
+						}
+					};
+					request.send(formData);
+				}
+			},
+        };
+        
+        aAcceptableFiles = [];
+        if (o.acceptedFiles && o.acceptedFiles != ''){
+            a = o.acceptedFiles.trim().split(/\s*,\s*/);
+            a.forEach(function(item, i, arr) {
+                if ($this.getMimeTypefromString(item)){
+                    oT = $this.getMimeTypefromString(item);
+                    if (Array.isArray(oT)){
+                        oT.forEach(function(oTs, j, ar) {
+                            aAcceptableFiles.push(oTs);
+                        });
+                    }
+                    else{
+                        aAcceptableFiles.push(oT);
+                    }
+                }
+                    
+            });
+        }
+        
+        if (aAcceptableFiles.length){
+            _options.acceptedFileTypes = aAcceptableFiles;
+        }
+        
+      
+        
+        if (o.resizeWidth || o.resizeHeight){
+            _options.allowImageResize = true;
+            _options.imageResizeTargetWidth = o.resizeWidth;
+            _options.imageResizeTargetHeight = o.resizeHeight;
+            _options.imageResizeMode = o.resizeMethod;
+        }
+        else{
+            _options.allowImageResize = false;
+            _options.allowImageTransform = false;
+        }
+        
+        FilePond.registerPlugin(
+            FilePondPluginImagePreview,
+            FilePondPluginFileValidateType,
+            FilePondPluginFileValidateSize,
+            FilePondPluginImageTransform,
+            FilePondPluginImageCrop,
+            FilePondPluginImageResize,
+            
+        );
+        this._uploader = FilePond.create(
+            document.querySelector('#' + this._sDivId),
+            $.extend({}, _options, o)
+        );    
+        // need to be activated if catch paste needed
+        //this.initPasteEditor();
+    }
+
+    this.onUploadCompleted = function (sErrorMsg) {        
+        if (sErrorMsg.length)
+            this._showError(sErrorMsg);        
+        
+        var $this = this;
+        if($this._uploader.status != 3){
+            this._isUploadsInProgress = false;
+            this.restoreGhosts();
+            if (!this._isErrorShown) {
+                $('#' + this._sPopupContainerId).dolPopupHide({});
+                this.removeFiles();
+            }
+        }
+    }
+
+    this.removeFiles = function () {
+        oFiles = this._uploader.getFiles();
+        for (i = 0; i < oFiles.length; i++){
+            this._uploader.removeFile(i);
+        }
+    }
+
+    this.cancelAll = function () {
+        this.removeFiles();
+    }
+
+    this.onBeforeUpload = function (params) {
+        this._isUploadsInProgress = true;
+        this._clearErrors();
+    }
+
+    this.onProgress = function (params) {
+    }
+
+    this.onClickCancel = function () {
+        var $this = this;
+        if (this._isUploadsInProgress) {
+            bx_confirm(_t('_sys_uploader_confirm_close_popup'), function() {
+                $this.removeFiles();
+                $this.cancelAll();
+                $('#' + $this._sPopupContainerId).dolPopupHide({});
+            });
+        } else {
+            $this.removeFiles();
+            $('#' + this._sPopupContainerId).dolPopupHide();
+        }
+        
+        BxDolUploaderBase.prototype._clearErrors.call(this);
+    }
+
+    this.onShowPopup = function () {
+        var $this = this;
+        setTimeout(function () {
+            $this.focusPasteEditor();
+        }, 200);
+    }
+
+    this.focusPasteEditor = function () {
+        $('#' + this._sFocusDivId).focus();
+    }
+
+    this.initPasteEditor = function () {
+        var $this = this;
+        $('#' + this._sFocusDivId).on('paste', function (e) {
+            var aFiles = [];
+            if (e.type == 'paste') {
+                var aItems = (e.clipboardData || e.originalEvent.clipboardData).items;
+                
+                for (var i in aItems) {
+                    var oItem = aItems[i];
+                    if (oItem.kind === 'file')
+                        aFiles.push(oItem.getAsFile());
+                }
+                $this._uploader.addFiles(aFiles);
+            }
+        });
+    }
+    
+    this.showUploaderForm = function() {
+        this._uploader.browse();
+    }
+    
+    this.initUploader({
+        'maxFilesize': options.maxFilesize,
+        'acceptedFiles': options.acceptedFiles,
+        'resizeWidth': options.resizeWidth,
+        'resizeHeight': options.resizeHeight,
+        'resizeMethod': options.resizeMethod,
+        'dictDefaultMessage': options.dictDefaultMessage,
+        'dictFileTooBig': options.dictFileTooBig,
+        'dictMaxFilesExceeded': options.dictMaxFilesExceeded,
+        'dictInvalidFileType': options.dictInvalidFileType,
+    });
+}
+
+BxDolUploaderHTML5.prototype = BxDolUploaderBase.prototype;
+
+function BxDolUploaderSimple(sUploaderObject, sStorageObject, sUniqId, options)
+{
+    BxDolUploaderHTML5.call(this, sUploaderObject, sStorageObject, sUniqId, options);
+}
+
+BxDolUploaderSimple.prototype = Object.create(BxDolUploaderHTML5.prototype);
+BxDolUploaderSimple.prototype.constructor = BxDolUploaderSimple;
+
+/**
+ * Crop Image Uploader js class
+ */
+function BxDolUploaderCrop (sUploaderObject, sStorageObject, sUniqId, options) {
+
+    this.init(sUploaderObject, sStorageObject, sUniqId, options);
+
+    this._eForm = null;
+
+    this.initUploader = function (oOptions) {
+
+        var $this = this;
+
+        var aExt = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
+
+        var eCroppie = $("#" + this._sFormContainerId + " .bx-croppie-element").croppie(oOptions);
+
+		$("#" + this._sFormContainerId + ' .bx-crop-rotate').on('click', function(ev) {
+			eCroppie.croppie('rotate', parseInt($(this).data('deg')));
+		});
+        
+        $("#" + this._sFormContainerId + " input[name=f]").on("change", function() {
+            var input = this;            
+
+            if (input.files && input.files[0]) {
+
+                var m = input.files[0].name.match(/\.([A-Za-z0-9]+)$/);
+
+                if (2 != m.length || -1 == aExt.indexOf(m[1].toLowerCase())) {                    
+                    $(input).replaceWith($(input).val('').clone(true));
+                    $this._showError(_t('_sys_uploader_crop_wrong_ext'));
+                    return;
+                }
+
+                $this._clearErrors();
+
+                var reader = new FileReader()
+
+                reader.onload = function(e) {
+                    eCroppie.croppie('bind', {
+                        url: e.target.result
+                    });
+                    $("#" + $this._sFormContainerId + " .bx-croppie-element").addClass('ready');
+                    $("#" + $this._sFormContainerId + " .bx-crop-action").removeClass('bx-btn-disabled');
+                    $("#" + $this._sFormContainerId + " .bx-croppie-element").data('bx-filename', input.files[0].name.replace(/(\.[A-Za-z0-9]+)$/, '.jpg'));
+                }
+                reader.readAsDataURL(input.files[0]);
+            }
+        });
+
+        $("#" + this._sFormContainerId + " .bx-crop-upload").on('click', function(ev) {
+            eCroppie.croppie('result', {
+                type: 'canvas',
+                size: 'original',
+                format: 'jpeg',
+                quality: '0.85',
+            }).then(function(resp) {
+                var fd = new FormData();
+
+                fd.append("f", dataURItoBlob(resp), $("#" + $this._sFormContainerId + " .bx-croppie-element").data('bx-filename'));
+                $.each(oOptions.bx_form, function (sName) {
+                    fd.append(sName, this);
+                });
+                
+                $this.onBeforeUpload(fd);
+
+                $.ajax({
+                    url: sUrlRoot + 'storage_uploader.php',
+                    type: "POST",
+                    processData: false,
+                    contentType: false,
+                    data: fd,
+                    success: function(data) {
+                        eval(data);
+                    },
+                    error: function() {
+                        $this._showError(_t('_sys_uploader_crop_err_upload'));
+                    }
+                })
+
+            });
+        });
+
+        function dataURItoBlob(dataURI) {
+            var split = dataURI.split(','),
+                dataTYPE = split[0].match(/:(.*?);/)[1],
+                binary = atob(split[1]),
+                array = [];
+                
+            for (var i = 0; i < binary.length; i++) 
+                array.push(binary.charCodeAt(i));
+
+            return new Blob([new Uint8Array(array)], {
+                type: dataTYPE
+            });
+        }
+
+        
+    };
+}
+
+BxDolUploaderCrop.prototype = BxDolUploaderBase.prototype;
+
+
+/**
+ * Record Video Uploader js class
+ */
+function BxDolUploaderRecordVideo (sUploaderObject, sStorageObject, sUniqId, options) {
+    this._camera = null;
+    this._blob = null;
+    this._recorder = null;
+    this._camera_type = 'user';
+
+    this._audio_bitrate = undefined !== options.audio_bitrate ? parseInt(options.audio_bitrate) : 128000;
+    this._video_bitrate = undefined !== options.video_bitrate ? parseInt(options.video_bitrate) : 1000000;
+
+    this.init(sUploaderObject, sStorageObject, sUniqId, options);
+
+    this.showUploaderForm = function() {
+        if (typeof MediaRecorder == 'undefined' || !('requestData' in MediaRecorder.prototype)) {
+            bx_alert(_t('_sys_uploader_unsupported_browser'));
+            return;
+        }
+
+        BxDolUploaderBase.prototype.showUploaderForm.call(this);
+    }
+
+    this.onBeforeShowPopup = function() {
+        this._blob = null;
+        this._camera = null;
+        this._recorder = null
+        this._clearErrors();
+
+        $('#bx-upoloader-recording-preview').hide();
+        $('#bx-upoloader-camera-capture').show();
+
+        $('#' + this._sFormContainerId + ' .bx-uploader-record-video-controls').hide();
+    }
+
+    this.switchCamera = function () {
+        this._camera_type = this._camera_type == 'user' ? 'environment' : 'user';
+
+        this.releaseCamera();
+        this.onShowPopup();
+    }
+
+    this.onShowPopup = function () {
+        var $this = this;
+        try {
+            navigator.mediaDevices.getUserMedia({ audio: true, video: {facingMode: this._camera_type} }).then(function(camera) {
+                $this._camera = camera;
+                $this.showCameraCapture();
+
+                $('#' + $this._sFormContainerId + ' .bx-uploader-recording-start').show();
+                $('#' + $this._sFormContainerId + ' .bx-uploader-recording-stop').hide();
+                $('#' + $this._sFormContainerId + ' .bx-uploader-record-video-controls').show();
+
+                navigator.mediaDevices.enumerateDevices().then(function(mediaDevices){
+                    let constraints = navigator.mediaDevices.getSupportedConstraints();
+                    if ($this.getDevicesNum(mediaDevices) > 1 && typeof constraints.facingMode != 'undefined' && constraints.facingMode) {
+                        $('#' + $this._sFormContainerId + ' .bx-record-camera-switch').show();
+                    } else {
+                        $('#' + $this._sFormContainerId + ' .bx-record-camera-switch').hide();
+                    }
+                });
+
+            })
+        } catch (err) {
+            $this._showError(_t('_sys_uploader_camera_capture_failed'));
+        }
+    }
+
+    this.onClickCancel = function () {
+        this.releaseCamera();
+
+        BxDolUploaderBase.prototype.onClickCancel.call(this);
+    }
+
+    this.startRecording = function() {
+        this._recorder = RecordRTC(this._camera, {
+            type: 'video',
+            audioBitsPerSecond: this._audio_bitrate,
+            videoBitsPerSecond: this._video_bitrate,
+            disableLogs: true
+        });
+
+        if (!this._recorder) {
+            this._showError(_t('_sys_uploader_unsupported_browser'));
+            return;
+        }
+
+        $("#" + this._sFormContainerId + " .bx-btn.bx-btn-primary:not(.bx-crop-upload)").hide();
+        $('#' + this._sFormContainerId + ' .bx-uploader-recording-start').hide();
+        $('#' + this._sFormContainerId + ' .bx-uploader-recording-stop').show();
+
+        this._recorder.startRecording();
+
+        this.showCameraCapture();
+    }
+
+    this.stopRecording = function(bSubmitWhenReady) {
+        $('#' + this._sFormContainerId + ' .bx-uploader-recording-start').show();
+        $('#' + this._sFormContainerId + ' .bx-uploader-recording-stop').hide();
+
+        var $this = this;
+        this._recorder.stopRecording(function(){
+            $this._blob = $this._recorder.getBlob();
+            $this.showRecordingPreview();
+
+            $this._recorder.destroy();
+            $this._recorder = null;
+
+            $("#" + $this._sFormContainerId + " .bx-btn.bx-btn-primary:not(.bx-crop-upload)").show();
+            if (bSubmitWhenReady)
+                $this.submitRecording($('#' + $this._sFormContainerId + ' form').get(0));
+        });
+    }
+
+    this.showCameraCapture = function() {
+        var video = $('#bx-upoloader-recording-preview').hide().get(0);
+        if (video.pause !== 'undefined') video.pause();
+        video.removeAttribute('src');
+
+        video = $('#bx-upoloader-camera-capture').show().get(0);
+        video.srcObject = this._camera;
+        video.muted = true;
+        video.volume = 0;
+        $('#' + this._sFormContainerId + ' .bx-record-video-preview .bx-record-video-preview-filesize').html('');
+    }
+
+    this.showRecordingPreview = function() {
+        $('#bx-upoloader-camera-capture').hide();
+        $('#bx-upoloader-recording-preview').show().get(0).src = URL.createObjectURL(this._blob);
+
+        var mbytes = (this._blob.size/1024/1024).toFixed(2);
+        $('#' + this._sFormContainerId + ' .bx-record-video-preview .bx-record-video-preview-filesize').html(mbytes + ' ' + _t('_sys_uploader_record_video_mb'));
+    }
+
+    this.submitRecording = function(form) {
+        if (this._recorder) {
+            this.stopRecording(true);
+            return;
+        }
+
+        this.onBeforeUpload(form);
+
+        var data = new FormData(form);
+        if (this._blob != null) data.append("f[]", this._blob, new Date().toISOString() + '.webm');
+
+        var $this = this;
+        $.ajax({
+            url: $(form).attr('action'),
+            type: "POST",
+            data: data,
+            processData: false,
+            contentType: false,
+            success:function(sErrorMsg, textStatus, jqXHR) {
+                $this.onUploadCompleted(sErrorMsg);
+                if (!sErrorMsg.length) {
+                    $this.releaseCamera();
+                }
+            },
+        });
+    }
+
+    this.releaseCamera = function() {
+        //stop playing recorded file
+        var video = $('#bx-upoloader-recording-preview').get(0);
+        if (video.pause != 'undefined') video.pause();
+        video.removeAttribute('src');
+
+        if (this._recorder) {
+            this._recorder.destroy();
+            this._recorder = null;
+        }
+
+        if (this._camera) {
+            if (typeof this._camera.stop != 'undefined') this._camera.stop();
+            this._camera.getTracks().forEach(function (track) {
+                if (track.readyState == 'live') {
+                    track.stop();
+                }
+            });
+        }
+    }
+
+    this.getDevicesNum = function(mediaDevices) {
+        let count = 0;
+        mediaDevices.forEach(mediaDevice => {
+            if (mediaDevice.kind === 'videoinput') count++;
+        });
+        return count;
+    }
+}
+
+BxDolUploaderRecordVideo.prototype = BxDolUploaderBase.prototype;
+
+
+function BxDolImageTweak (unique_id, action_url, content_id, field, js_object, allow_tweak) {
+    var $this = this;
+    this._sUniqueId = unique_id;
+    this._sActionUrl = action_url;
+    this._sContentId = content_id;
+    this._sField = field;
+    this._sJsObject = js_object;
+    this._bAllowTweak = allow_tweak;
+    this._oContainerButtons = $('.bx-image-edit-buttons-' + unique_id);
+    this._oContainerButtons.removeClass('hidden');
+    $("#bx-form-input-files-" + this._sUniqueId + "-upload-result").bind( "contentchange", function(){ 
+        $this.uploadComplete($(this));
+    });
+    
+    if (this._bAllowTweak){
+        this._oContainerButtons.find('.bx-image-edit-buttons-edit').removeClass('hidden');
+    }
+}
+
+BxDolImageTweak.prototype.showUploaderForm = function(){
+    var $this = this;
+    $('#bx-form-input-files-' + $this._sUniqueId + '-upload-result').html('');
+    eval($this._sJsObject + '.showUploaderForm()');
+}
+    
+BxDolImageTweak.prototype.uploadComplete = function(obj){
+    var $this = this;
+    var sUrl = $this._sActionUrl + "updateImage/" + $this._sField + "/" + $this._sContentId + "/" + obj.html() + "/"; 
+    
+    $this._oContainerButtons.removeClass('bx-image-edit-buttons-no-image');
+    $.post(sUrl, function (aData) {
+        if ($this._bAllowTweak){
+            $(".bx-image-edit-source-" + $this._sUniqueId).css("background-image", "url(" + aData + ")").css('background-position', '');
+            $(".bx-image-edit-source-" + $this._sUniqueId).parents('.bx-base-pofile-cover-image.bx-bpci-holder').removeClass('bx-bpci-holder h-32 lg:h-32').addClass('h-64 lg:h-80')
+        }
+        else{
+            $(".bx-image-edit-source-" + $this._sUniqueId).attr("src", aData).show();
+            $(".bx-image-edit-source-" + $this._sUniqueId).parents('.bx-base-pofile-cover-thumb').find('p.bx-base-pofile-unit-thumb').hide();
+        }
+    });
+}
+
+BxDolImageTweak.prototype.changePosition = function (){
+    var $this = this;
+    $(".bx-image-edit-source-" + $this._sUniqueId).parent().append("<div class='bx-image-edit-move-info'><i class='sys-icon sys-colored arrows-alt '></i>" + _t('_sys_uploader_image_reposition_info') + "</div>");
+    $(".bx-image-edit-source-" + $this._sUniqueId).addClass('bx-image-edit-move').bind('dragover', function(e){
+        $(".bx-image-edit-source-" + $this._sUniqueId).parent().find('.bx-image-edit-move-info').remove();
+        $(".bx-image-edit-source-" + $this._sUniqueId).css('background-position', " 0px " + e.offsetY / $(e.currentTarget).height() * 100 + '%');
+    });
+    with ($this._oContainerButtons) {
+        find('.bx-image-edit-buttons-cancel').removeClass('hidden');
+        find('.bx-image-edit-buttons-save').removeClass('hidden');
+        find('.bx-image-edit-buttons-edit').addClass('hidden');
+        find('.bx-image-edit-buttons-upload').addClass('hidden');
+    }
+}
+
+BxDolImageTweak.prototype.cancelPosition = function (){
+    var $this = this;
+    $(".bx-image-edit-source-" + $this._sUniqueId).parent().find('.bx-image-edit-move-info').remove();
+    $(".bx-image-edit-source-" + $this._sUniqueId).addClass('bx-image-edit-move').unbind('dragover'); 
+    
+    with ($this._oContainerButtons) {
+        find('.bx-image-edit-buttons-cancel').addClass('hidden');
+        find('.bx-image-edit-buttons-save').addClass('hidden');
+        find('.bx-image-edit-buttons-edit').removeClass('hidden');
+        find('.bx-image-edit-buttons-upload').removeClass('hidden');
+    }
+}
+    
+BxDolImageTweak.prototype.savePosition = function (){
+    var $this = this;
+    
+    $(".bx-image-edit-source-" + $this._sUniqueId).parent().find('.bx-image-edit-move-info').remove();
+    $(".bx-image-edit-source-" + $this._sUniqueId).addClass('bx-image-edit-move').unbind('dragover'); 
+    $(".bx-image-edit-source-" + $this._sUniqueId).removeClass('bx-image-edit-move')
+    
+    aPos = $(".bx-image-edit-source-" + $this._sUniqueId).css('background-position').split(' ');
+    $.post($this._sActionUrl + 'updateImagePosition/' + $this._sContentId + '/' + $this._sField + '/' + aPos[0].replace('%','').replace('px','') + '/' + aPos[1].replace('%','').replace('px','') + '/', function () {
+        with ($this._oContainerButtons) {
+            find('.bx-image-edit-buttons-cancel').addClass('hidden');
+            find('.bx-image-edit-buttons-save').addClass('hidden');
+            find('.bx-image-edit-buttons-edit').removeClass('hidden');
+            find('.bx-image-edit-buttons-upload').removeClass('hidden');
+        }
+    });
+}
+
+/** @} */

+ 444 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/js/editor.quill.js

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

+ 1465 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/js/functions.js

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

+ 2327 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/utils.inc.php

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

+ 12 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/inc/version.inc.php

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

+ 279 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/general/template/css/main.css

@@ -0,0 +1,279 @@
+
+/* breadcrumb */
+div.bx-base-general-breadcrumb  {
+    white-space: nowrap;
+    overflow: hidden;
+}
+div.bx-base-general-breadcrumb a,
+div.bx-base-general-breadcrumb i {
+    display: inline-block;
+    vertical-align: top;
+
+    height: 2.2rem;
+    line-height: 2.2rem;
+}
+.bx-base-general-breadcrumb-home i.sys-icon {
+    font-size: 2.0rem;
+}
+
+/* view entry */
+.bx-base-general-entity-info > p:first-child {
+    margin-top:0;
+}
+
+
+/* view entry: actions */
+.bx-base-general-entity-actions ul.bx-menu-custom-hor {
+    position: relative;
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+    -webkit-justify-content: flex-start;
+    justify-content: flex-start;
+    -webkit-flex-wrap: wrap;
+    flex-wrap: wrap;
+}
+.bx-base-general-entity-actions ul.bx-menu-custom-hor.bx-menu-more-auto {
+    -webkit-flex-wrap: nowrap;
+    flex-wrap: nowrap;
+}
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item {
+    white-space: nowrap;
+}
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .cmt-element-holder,
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .cmt-counter-holder,
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .bx-view-element-holder,
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .bx-view-counter-holder,
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .bx-vote-element-holder, 
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .bx-vote-counter-holder, 
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .bx-score-element-holder, 
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .bx-score-counter-holder, 
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .bx-favorite-element-holder,
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .bx-favorite-counter-holder,
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .bx-report-element-holder,
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .bx-report-counter-holder,
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .bx-tl-repost-element-holder,
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .bx-tl-repost-counter-holder {
+    float: none;
+    display: inline-block;
+    vertical-align: top;
+}
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .cmt-counter-holder,
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .bx-view-counter-holder,
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .bx-vote-counter-holder, 
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .bx-score-counter-holder, 
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .bx-favorite-counter-holder,
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .bx-report-counter-holder,
+.bx-base-general-entity-actions .bx-menu-custom-hor li.bx-menu-item .bx-tl-repost-counter-holder {
+    line-height: normal;
+}
+.bx-base-general-entity-actions .cmt,
+.bx-base-general-entity-actions .bx-view,
+.bx-base-general-entity-actions .bx-vote,
+.bx-base-general-entity-actions .bx-score,
+.bx-base-general-entity-actions .bx-favorite,
+.bx-base-general-entity-actions .bx-feature,
+.bx-base-general-entity-actions .bx-report,
+.bx-base-general-entity-actions .bx-tl-repost,
+.bx-base-general-entity-actions .bx-menu-btns-hor.bx-menu-object-sys_social_sharing,
+.bx-base-general-entity-actions .bx-base-general-entity-action {
+    position: relative;
+    float: none;
+    display: inline-block;
+    vertical-align: top;
+
+    margin: 0;
+    padding: 0;
+}
+
+/* TODO: Remove this at the end */
+.bx-base-general-entity-share .bx-base-general-comments,
+.bx-base-general-entity-share .bx-view,
+.bx-base-general-entity-share .bx-vote,
+.bx-base-general-entity-share .bx-score,
+.bx-base-general-entity-share .bx-favorite,
+.bx-base-general-entity-share .bx-feature,
+.bx-base-general-entity-share .bx-report,
+.bx-base-general-entity-share .bx-tl-repost,
+.bx-base-general-entity-share .bx-menu-btns-hor.bx-menu-object-sys_social_sharing {
+	position: relative;
+	float: none;
+	display: inline-block;
+	vertical-align: top;
+}
+
+/* attachments */
+.bx-base-general-attachments .bx-base-general-attachment video,
+.flickity-enabled .flickity-viewport .flickity-slider .bx-base-general-attachment video {
+    max-width:100%;
+    max-height:100%;
+}
+.bx-base-general-attachment a div {
+    text-overflow: ellipsis;
+    overflow: hidden;
+}
+.bx-base-general-attachment a canvas {
+    position:absolute;
+    top:0;
+    left:0;
+}
+
+.bx-base-general-attachment-sound {
+    position:relative;
+    height:100%;
+}
+.bx-base-general-attachment-sound-file-name,
+.bx-base-general-attachment-sound-player {
+  display: flex;
+  justify-content: center;
+  align-content: center;
+  flex-direction: column;
+}
+.bx-base-general-attachment-sound-file-name {
+    height:9.25rem;
+}
+.bx-base-general-attachment-sound-player {
+    height:3.25rem;
+}
+
+/* comments item */
+div.bx-base-general-comments div.bx-base-general-comments-element-holder,
+div.bx-base-general-comments div.bx-base-general-comments-counter-holder {
+    position: relative;
+    float: left;
+}
+.bx-base-general-comments-element-holder
+.bx-base-general-comments-counter-holder {
+    line-height: 20px;
+    line-height: 1.25rem;
+}
+.bx-base-general-comments-button .bx-base-general-comments-element-holder,
+.bx-base-general-comments-button .bx-base-general-comments-counter-holder {
+    line-height: 36px;
+    line-height: 2.2rem;
+}
+.bx-base-general-comments-button .bx-base-general-comments-counter-holder {
+    font-size: 1.5rem;
+}
+.bx-base-general-comments-link .bx-base-general-comments-counter-holder.bx-btn-border {
+    border-width: 0px;
+}
+
+/* unit meta */
+.bx-base-general-unit-meta {
+    white-space: normal;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+.sys-author ul.bx-menu-custom.bx-menu-custom-hor,
+.bx-base-general-unit-meta ul.bx-menu-custom.bx-menu-custom-hor {
+    position: relative;
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: flex-start;
+    align-items: flex-start;
+    -webkit-justify-content: flex-start;
+    justify-content: flex-start;
+    -webkit-flex-wrap: wrap;
+    flex-wrap: wrap;
+
+    margin: 0.1rem 0 0.2rem 0;
+}
+.sys-author ul.bx-menu-custom.bx-menu-custom-hor > li,
+.bx-base-general-unit-meta ul.bx-menu-custom.bx-menu-custom-hor > li {
+    -webkit-flex: 0 1 auto; 
+    -ms-flex:  0 1 auto; 
+    flex: 0 1 auto;
+
+    white-space: nowrap;
+}
+.sys-author ul.bx-menu-custom.bx-menu-custom-hor > li,
+.bx-base-general-unit-meta ul.bx-menu-custom.bx-menu-custom-hor > li {
+    margin-left: 0.5rem;
+}
+.sys-author ul.bx-menu-custom.bx-menu-custom-hor > li:first-child,
+.bx-base-general-unit-meta ul.bx-menu-custom.bx-menu-custom-hor > li:first-child,
+.sys-author ul.bx-menu-custom.bx-menu-custom-hor > .bx-menu-item-nl + li,
+.bx-base-general-unit-meta ul.bx-menu-custom.bx-menu-custom-hor > .bx-menu-item-nl + li {
+    margin-left: 0;
+}
+.sys-author ul.bx-menu-custom.bx-menu-custom-hor > li.bx-menu-item-nl,
+.bx-base-general-unit-meta ul.bx-menu-custom.bx-menu-custom-hor > li.bx-menu-item-nl {
+    display: none;
+    width: 100%;
+    height: 0.5rem;
+}
+.bx-base-pofile-unit-with-cover .bx-base-general-unit-meta ul.bx-menu-custom.bx-menu-custom-hor > li.bx-menu-item-nl {
+    display: block;
+}
+
+.sys-author ul.bx-menu-custom.bx-menu-custom-hor > li > span,
+.bx-base-general-unit-meta ul.bx-menu-custom.bx-menu-custom-hor > li > span:not(.bx-menu-meta-item-ex) {
+    margin-left: 0;
+    margin-right: 0;
+}
+
+.sys-author ul.bx-menu-custom li:first-child .bx-base-general-unit-meta-div,
+.bx-base-general-unit-meta ul.bx-menu-custom li:first-child .bx-base-general-unit-meta-div {
+    display: none;
+}
+
+.sys-author ul.bx-menu-custom li .bx-base-general-unit-meta-item,
+.sys-author ul.bx-menu-custom li .bx-base-general-unit-meta-div,
+.sys-author ul.bx-menu-custom li .bx-btn,
+.sys-author ul.bx-menu-custom li .bx-vote.bx-vote-stars,
+.bx-base-general-unit-meta ul.bx-menu-custom li .bx-base-general-unit-meta-item,
+.bx-base-general-unit-meta ul.bx-menu-custom li .bx-base-general-unit-meta-div,
+.bx-base-general-unit-meta ul.bx-menu-custom li .bx-btn,
+.bx-base-general-unit-meta ul.bx-menu-custom li .bx-vote.bx-vote-stars {
+    position: relative;
+    float: none;
+    display: inline-block;
+    vertical-align: middle;
+}
+
+.sys-author ul.bx-menu-custom li .bx-vote.bx-vote-stars,
+.bx-base-general-unit-meta ul.bx-menu-custom li .bx-vote.bx-vote-stars {
+    margin-top: 0px;
+    margin-bottom: 0px;
+}
+
+.sys-author ul.bx-menu-custom li .bx-metatags-keyword,
+.bx-base-general-unit-meta ul.bx-menu-custom li .bx-metatags-keyword {
+    margin: 0px;
+    line-height: normal;
+}
+
+/* Browsing by labels */
+.bx-base-general-lables-tree-parent-0, 
+.bx-base-general-lables-tree-parent-0 ul {
+    list-style-type: none;
+    padding-left: 2rem;
+}
+.bx-base-general-lables-tree-parent-0 {
+    margin: 0px;
+}
+.bx-base-general-lables-tree-parent-0 li {
+    padding: 0px;
+}
+.bx-base-general-lables-tree-parent-0 li i.sys-icon {
+    display: inline-block;
+    width: 0.625rem;
+    margin-left: -1rem;
+}
+
+
+#bx-popup-ajax-wrapper-sys_set_badges li span{
+    display: flex;
+    margin-left: 0px;
+    align-items: baseline;
+}
+#bx-popup-ajax-wrapper-sys_set_badges li INPUT[type="checkbox"] {
+    margin-right: 0.5rem;
+}
+.bx-base-unit-showcase-wrapper {
+    display: block;
+    white-space: nowrap;
+    overflow: hidden;
+}

+ 1943 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/groups/classes/BxBaseModGroupsModule.php

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

+ 43 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/groups/template/cover_block.html

@@ -0,0 +1,43 @@
+<div class="bx-base-profile-cover-block-wrapper">
+    <div class="bx-base-pofile-cover-wrapper __class__ relative bg-gradient-to-b from-gray-100 to-white dark:from-black dark:to-gray-900 rounded-md">
+        <div class="bx-base-pofile-cover">
+            <bx_if:show_cover>
+                <div class="bx-base-pofile-cover-image-wrp relative overflow-hidden">
+                    <div class="absolute w-full h-2/3">
+                        <div class="absolute top-6 -left-1/4 w-2/3 h-2/3 bg-blue-700 filter blur-3xl z-0 opacity-50 animate-goo"></div>
+                        <div class="absolute top-6 -right-1/4 w-2/3 h-2/3 bg-green-700 filter blur-3xl z-0 opacity-50 animate-goo"></div>
+                    </div>
+                    <div class="bx-base-pofile-cover-image relative h-64 lg:h-40 max-w-7xl mx-auto opacity-100 __add_class__">
+                        <p class="bx-base-pofile-cover-image box-border absolute h-full w-full" >
+                            <img class="bx-base-pofile-cover-image h-64 mx-auto w-full object-cover bg-cover lg:h-40 rounded-t z-10 __img_class__ bx-image-edit-source-__unique_id__" style="background-image:url(__cover_url__);__cover_settins__" src="<bx_icon_url:spacer.gif />" />
+                        </p>
+                    </div>
+                </div>
+            </bx_if:show_cover>
+            <div class="bx-base-pofile-cover-info relative mx-auto px-6 ">
+                <div class="bx-base-pofile-cover-user relative flex flex-col flex-colmt-6 pt-6 sm:pb-1">
+                    <div class="bx-base-profile-cover-title-bages min-w-0 flex-1 mb-4">
+                        <bx_if:show_title_as_tag>
+                            <h1 class="bx-def-unit-info text-4xl font-bold text-gray-900 dark:text-gray-50 text-center sm:truncate">__title__</h1>
+                        </bx_if:show_title_as_tag>
+                        <bx_if:show_title_as_text>
+                            <div class="bx-def-unit-info text-4xl font-bold text-gray-900 dark:text-gray-50 text-center sm:truncate">__title__</div>
+                        </bx_if:show_title_as_text>
+                        <div class="bx-base-profile-cover-meta-holder">
+                            <div class="bx-base-profile-cover-meta flex flex-row flex-wrap justify-center mt-6 md:mt-2 -ml-1  text-base font-medium">__meta__</div>
+                        </div>
+                    </div>
+                    <div class="bx-base-pofile-cover-actions relative flex lg:flex-1 h-14 md:h-10 mb-4" onclick="event.stopPropagation();">__action_menu__</div>
+                    <bx_if:show_text>
+                        <div class="flex justify-between mb-4">
+                            <div class="font-normal text-sm lg:text-base text-gray-500 group-hover:text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-300">__text__</div>
+                        </div>
+                    </bx_if:show_text>
+                </div>
+                __show_data__
+            </div>
+            __cover_tweak__
+        </div>
+    </div>
+</div>
+__additional_code__

+ 225 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/payment/classes/BxBaseModPaymentGridOrders.php

@@ -0,0 +1,225 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BasePayment Base classes for Payment like modules
+ * @ingroup     UnaModules
+ * 
+ * @{
+ */
+
+class BxBaseModPaymentGridOrders extends BxTemplGrid
+{
+    protected $MODULE;
+    protected $_oModule;
+
+    protected $_sOrdersType;
+
+    protected $_bSingleSeller;
+    protected $_iSingleSeller;
+
+    protected $_sJsObject;
+    protected $_sLangsPrefix;
+    protected $_sCurrencySign;
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        parent::__construct ($aOptions, $oTemplate);
+
+        $this->_oModule = BxDolModule::getInstance($this->MODULE);
+
+        $this->_bSingleSeller = $this->_oModule->_oConfig->isSingleSeller();
+
+        $this->_iSingleSeller = 0;
+        if($this->_bSingleSeller)
+            $this->_iSingleSeller = $this->_oModule->_oConfig->getSiteAdmin();
+
+        $this->_sLangsPrefix = $this->_oModule->_oConfig->getPrefix('langs');
+        $this->_sCurrencySign = $this->_oModule->_oConfig->getDefaultCurrencySign();
+
+        $this->_sDefaultSortingOrder = 'DESC';
+
+        $iSellerId = bx_get('seller_id');
+        if($iSellerId !== false) {
+            $iSellerId = (int)$iSellerId;
+            $this->_aQueryAppend['seller_id'] = $iSellerId;
+
+            if(!$this->_bSingleSeller)
+                $this->_sCurrencySign = $this->_oModule->getVendorCurrencySign($iSellerId);
+        }
+
+        $iClientId = bx_get('client_id');
+        if($iClientId !== false)
+            $this->_aQueryAppend['client_id'] = (int)$iClientId;
+    }
+
+    public function addQueryParam($sKey, $sValue)
+    {
+        if(empty($sKey) || !isset($sValue))
+            return;
+
+        $this->_aQueryAppend[$sKey] = $sValue;
+    }
+
+    public function performActionViewOrder()
+    {
+        $aIds = bx_get('ids');
+        if(!$aIds || !is_array($aIds)) 
+            return echoJson(array());
+
+        $iId = (int)$aIds[0];
+
+        $sKey = 'order_' . $this->_sOrdersType . '_view';
+        $sId = $this->_oModule->_oConfig->getHtmlIds($this->_sOrdersType, $sKey);
+        $sTitle = _t($this->_sLangsPrefix . 'popup_title_ods_' . $sKey);
+        $sContent = $this->_oModule->getObjectOrders()->getOrder($this->_sOrdersType, $iId);
+
+        return echoJson(array('popup' => BxTemplFunctions::getInstance()->popupBox($sId, $sTitle, $sContent)));
+    }
+
+    public function performActionCancel()
+    {
+    	$aIds = bx_get('ids');
+        if(!$aIds || !is_array($aIds)) 
+            return echoJson(array());
+
+        $oOrders = $this->_oModule->getObjectOrders();
+
+        $iAffected = 0;
+        $aAffected = array();
+        foreach($aIds as $iId)
+            if($oOrders->cancel($this->_sOrdersType, $iId)) {
+                $aAffected[] = $iId;
+                $iAffected++;
+            }
+
+        echoJson($iAffected ? array('grid' => $this->getCode(false), 'blink' => $aAffected) : array('msg' => _t($this->_sLangsPrefix . 'err_cannot_perform')));
+    }
+
+    protected function _getCellHeaderAuthorId ($sKey, $aField)
+    {
+        if(!$this->_bSingleSeller)
+            return '';
+
+        if($this->_aQueryAppend['seller_id'] != $this->_iSingleSeller)
+            $aField['title'] = _t('_bx_payment_grid_column_title_ods_seller_id');
+
+        return parent::_getCellHeaderDefault($sKey, $aField);
+    }
+
+    protected function _getCellClientId($mixedValue, $sKey, $aField, $aRow)
+    {
+        return parent::_getCellDefault($this->_oModule->_oTemplate->displayProfileLink($mixedValue), $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellSellerId($mixedValue, $sKey, $aField, $aRow)
+    {
+        return parent::_getCellDefault($this->_oModule->_oTemplate->displayProfileLink($mixedValue), $sKey, $aField, $aRow);
+    }
+    
+    protected function _getCellAuthorId($mixedValue, $sKey, $aField, $aRow)
+    {
+        if(!$this->_bSingleSeller)
+            return '';
+
+        if($this->_aQueryAppend['seller_id'] != $this->_iSingleSeller)
+            $mixedValue = $aRow['seller_id'];
+        
+        $sResult = '';
+        if(!empty($mixedValue) && is_numeric($mixedValue))
+            $sResult = $this->_oModule->_oTemplate->displayProfileLink($mixedValue);
+
+        return parent::_getCellDefault($sResult, $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellItem($mixedValue, $sKey, $aField, $aRow)
+    {
+        $sTxtUnknown = _t('_uknown');
+
+        if(empty($aRow['module_id']) || empty($aRow['item_id']))
+            return parent::_getCellDefault($sTxtUnknown, $sKey, $aField, $aRow);
+
+        $aItemInfo = $this->_oModule->callGetCartItem((int)$aRow['module_id'], array((int)$aRow['item_id'], $aRow['client_id']));
+        if(empty($aItemInfo) || !is_array($aItemInfo))
+            return parent::_getCellDefault($sTxtUnknown, $sKey, $aField, $aRow);
+
+        return parent::_getCellDefault($this->_oModule->_oTemplate->displayLink('link', array(
+            'href' => $aItemInfo['url'],
+            'title' => bx_html_attribute($aItemInfo['title']),
+            'content' => $aItemInfo['title']
+    	)), $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellAmount($mixedValue, $sKey, $aField, $aRow)
+    {
+        $sSign = '';
+        if(!empty($aRow['currency']))
+            $sSign = $this->_oModule->_oConfig->retrieveCurrencySign($aRow['currency']);
+        else
+            $sSign = !$this->_bSingleSeller ? $this->_oModule->getVendorCurrencySign((int)$aRow['seller_id']) : $this->_sCurrencySign;
+
+        return parent::_getCellDefault(_t_format_currency_ext($mixedValue, ['sign' => $sSign]), $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellDate($mixedValue, $sKey, $aField, $aRow)
+    {
+        return $this->_getCellDefaultDateTime($mixedValue, $sKey, $aField, $aRow);
+    }
+    
+    protected function _getCellDefaultDate($mixedValue, $sKey, $aField, $aRow)
+    {
+        return parent::_getCellDefault(bx_time_js($mixedValue, BX_FORMAT_DATE, true), $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellDefaultDateTime($mixedValue, $sKey, $aField, $aRow)
+    {
+        return parent::_getCellDefault(bx_time_js($mixedValue, BX_FORMAT_DATE_TIME, true), $sKey, $aField, $aRow);
+    }
+
+    protected function _getFilterSelectAll($sName, $aParams = array())
+    {
+        $sJsObject = !empty($aParams['js_object']) ? $aParams['js_object'] : $this->_sJsObject;
+        $sJsOnChange = !empty($aParams['js_onchange']) ? $aParams['js_onchange'] : 'onChangeFilter(this)';
+        
+        $aAttrs = array(
+            'id' => 'bx-grid-' . $sName . '-' . $this->_sObject,
+            'onChange' => 'javascript:$(this).off(\'keyup focusout\'); ' . $sJsObject . '.' . $sJsOnChange,
+            'onBlur' => 'javascript:' . $sJsObject . '.' . $sJsOnChange,
+        );
+        if(!empty($aParams['attrs']) && is_array($aParams['attrs']))
+            $aAttrs = array_merge($aAttrs, $aParams['attrs']);
+
+        $aInput = array(
+            'type' => 'select',
+            'name' => $sName,
+            'tr_attrs' => array(
+                'class' => 'bx-grid-filter-' . $sName,
+            ),
+            'attrs' => $aAttrs,
+            'value' => !empty($aParams['value']) ? $aParams['value'] : '',
+            'values' => !empty($aParams['values']) && is_array($aParams['values']) ? $aParams['values'] : array()
+        );
+
+        $oForm = new BxTemplFormView(array());
+        return $oForm->genRow($aInput);
+    }
+
+    protected function _getSearchInput()
+    {
+        $aInput = array(
+            'type' => 'text',
+            'name' => 'keyword',
+            'attrs' => array(
+                'id' => 'bx-grid-search-' . $this->_sObject,
+                'onKeyup' => 'javascript:$(this).off(\'keyup\'); ' . $this->_sJsObject . '.onChangeFilter(this)',
+                'placeholder' => bx_html_attribute(_t('_sys_grid_search'))
+            )
+        );
+
+        $oForm = new BxTemplFormView(array());
+        return $oForm->genRow($aInput);
+    }
+}
+
+/** @} */

+ 759 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/profile/classes/BxBaseModProfileTemplate.php

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

+ 38 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/profile/template/cover.html

@@ -0,0 +1,38 @@
+<div class="bx-menu-toolbar-padding">
+    <div class="bx-base-pofile-cover-wrapper __class__ bx-def-centered bx-def-padding-leftright bx-def-page-width bx-def-box-sizing">
+        <div class="bx-base-pofile-cover bx-def-cover-color-bg bx-def-box-sizing">
+            <div class="bx-base-pofile-cover-image __add_class__">
+                <p class="bx-base-pofile-cover-image bx-def-box-sizing flex justify-center items-center" >
+                    <img class="bx-base-pofile-cover-image __img_class__ bx-image-edit-source-__unique_id__" style="background-image:url(__cover_url__); __cover_settins__" src="<bx_icon_url:spacer.gif />"/>
+                </p>
+                <div class="bx-cover-gradient"></div>  
+            </div>
+            <div class="bx-base-pofile-cover-info">
+                <div class="bx-base-pofile-ci-cnt bx-def-padding-leftright">
+                    <bx_if:show_avatar>
+                        <div class="bx-base-pofile-cover-thumb __add_class__">
+                            <a class="bx-base-pofile-cover-thumb" href="javascript:" <bx_if:is_avatar>onclick="$('#__picture_popup_id__').dolPopupImage('__picture_url__', $(this).parent()); event.stopPropagation();"</bx_if:is_avatar>>
+                                <img class="bx-base-pofile-unit-thumb bx-def-ava-big bx-def-ava-big-size rounded-full ring-4 mx-auto ring-white dark:ring-gray-900 __img_class__ bx-image-edit-source-__unique_id__" src="__ava_url__" <bx_if:show_ava_letter> style="display:none" </bx_if:show_ava_letter> />
+                                <p class="bx-base-pofile-unit-thumb bx-def-ava-big bx-def-ava-big-size flex items-center justify-center box-border m-0 rounded-full ring-4 mx-auto ring-white dark:ring-gray-900 text-white font-bold text-5xl __img_class__-placeholder" style="background-color:rgba(__color__);<bx_if:show_ava_image>display:none</bx_if:show_ava_image>">__letter__</p>
+                            </a>
+                            <bx_if:show_online>
+                                <div class="bx-base-pofile-unit-online"></div>
+                            </bx_if:show_online>
+                            __picture_tweak__
+                        </div>
+                    </bx_if:show_avatar>
+                    <div class="bx-base-pofile-cover-user bx-def-margin-sec-topbottom">
+                        <div class="bx-base-profile-cover-title-bages">
+                            <h1 class="bx-def-unit-info">__title__ </h1>
+                            <div class="bx-base-profile-cover-meta flex flex-row flex-wrap justify-center lg:justify-start text-sm">__meta__</div>
+                        </div>
+                        <div class="bx-base-pofile-cover-actions bx-def-margin-sec-leftright-neg" onclick="event.stopPropagation();">__action_menu__</div>
+                    </div>
+                </div>
+            </div>
+            __cover_tweak__
+        </div>
+    </div>
+    __picture_popup__
+</div>
+__additional_code__

+ 50 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/profile/template/cover_block.html

@@ -0,0 +1,50 @@
+<div class="bx-base-profile-cover-block-wrapper">
+    <div class="bx-base-pofile-cover-wrapper __class__ bx-def-centered bx-def-box-sizing">
+        <div class="bx-base-pofile-cover bx-def-cover-color-bg bx-def-box-sizing <bx_if:show_clickable>bx-lnk-cnt</bx_if:show_clickable>">
+            <a href="__content_url__" class="bx-lnk-src hidden"></a>
+            <bx_if:show_cover>
+                <div class="bx-base-pofile-cover-image __add_class__">
+                    <p class="bx-base-pofile-cover-image bx-def-box-sizing flex justify-center items-center">
+                        <img class="bx-base-pofile-cover-image h-full object-cover bg-cover w-full __img_class__ bx-image-edit-source-__unique_id__" style="background-image:url(__cover_url__);__cover_settins__" src="<bx_icon_url:spacer.gif />"/>
+                    </p>
+                    
+                </div>
+            </bx_if:show_cover>
+            <div class="bx-base-pofile-cover-info">
+                <div class="bx-base-pofile-ci-cnt bx-def-padding-leftright">
+                    <bx_if:show_avatar>
+                        <div class="bx-base-pofile-cover-thumb __add_class__">
+                            <a class="bx-base-pofile-cover-thumb" href="javascript:" <bx_if:is_avatar>onclick="$('#__picture_popup_id__').dolPopupImage('__picture_url__', $(this).parent()); event.stopPropagation();"</bx_if:is_avatar>>
+                                <img class="bx-base-pofile-unit-thumb bx-def-ava-big bx-def-ava-big-size rounded-full ring-4 mx-auto ring-white dark:ring-gray-900 __img_class__ bx-image-edit-source-__unique_id__" src="__ava_url__" <bx_if:show_ava_letter> style="display:none" </bx_if:show_ava_letter> />
+                                <p class="bx-base-pofile-unit-thumb bx-def-ava-big bx-def-ava-big-size flex items-center justify-center box-border m-0 rounded-full ring-4 mx-auto ring-white dark:ring-gray-900 text-white font-bold text-5xl __img_class__-placeholder" style="background-color:rgba(__color__);<bx_if:show_ava_image>display:none</bx_if:show_ava_image>" >__letter__</p>
+                            </a>
+                            <bx_if:show_online>
+                                <div class="bx-base-pofile-unit-online"></div>
+                            </bx_if:show_online>
+                            __picture_tweak__
+                        </div>
+                    </bx_if:show_avatar>
+                </div>
+                <div class="bx-base-pofile-cover-user bx-def-margin-sec-topbottom">
+                    <div class="bx-base-profile-cover-title-bages">
+                        <bx_if:show_title_as_tag>
+                            <h1 class="bx-def-unit-info">__title__</h1>
+                        </bx_if:show_title_as_tag>
+                        <bx_if:show_title_as_text>
+                            <div class="bx-def-unit-info bx-def-font-h1">__title__</div>
+                        </bx_if:show_title_as_text>
+                        <div class="bx-base-profile-cover-bages bx-base-bages-container">__badges__</div>
+                    </div>
+                    <div class="bx-base-pofile-cover-actions bx-def-padding-sec-leftright bx-def-box-sizing" onclick="event.stopPropagation();">__action_menu__</div>
+                    <bx_if:show_text>
+                        <div class="bx-def-padding-leftright bx-def-padding-sec-bottom bx-def-unit-desc">__text__</div>
+                    </bx_if:show_text>
+                </div>
+                __show_data__
+            </div>
+            __cover_tweak__
+        </div>
+    </div>
+    __picture_popup__
+</div>
+__additional_code__

+ 130 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/template/js/sidebar.js

@@ -0,0 +1,130 @@
+const isSidebarExpanded = toggleSidebarEl => {
+    return toggleSidebarEl.getAttribute('aria-expanded') === 'true' ? true : false;
+}
+
+const toggleSidebar = (sidebarEl, expand, setExpanded = false) => {
+    const bottomMenuEl = document.querySelector('[sidebar-bottom-menu]');
+    const mainContentEl = document.getElementById('main-content');
+    if (expand) {
+        sidebarEl.classList.remove('bx-mc-narrowed');
+        sidebarEl.classList.add('bx-mc-expanded');
+        sidebarEl.classList.add('xl:w-64');
+        sidebarEl.classList.remove('xl:w-16');
+        mainContentEl.classList.add('xl:ml-64');
+        mainContentEl.classList.remove('xl:ml-16');
+
+        document.querySelectorAll('#' + sidebarEl.getAttribute('id') + ' [sidebar-toggle-item]').forEach(sidebarToggleEl => {
+            sidebarToggleEl.classList.remove('xl:hidden');
+            sidebarToggleEl.classList.remove('xl:absolute');
+            sidebarToggleEl.classList.remove('bx-mpi-toggle-hidden');
+        });
+
+        // toggle multi level menu item initial and full text
+        document.querySelectorAll('#' + sidebar.getAttribute('id') + ' ul > li > ul > li > a').forEach(e => {
+            e.classList.add('pl-11');
+            e.classList.remove('px-4');
+            e.childNodes[0].classList.remove('hidden');
+            e.childNodes[1].classList.add('hidden');
+        });
+
+        bottomMenuEl.classList.remove('flex-col', 'space-y-4', 'p-2');
+        bottomMenuEl.classList.add('space-x-4', 'p-4');
+        setExpanded ? toggleSidebarEl.setAttribute('aria-expanded', 'true') : null;
+    } 
+    else {
+        sidebarEl.classList.add('bx-mc-narrowed');
+        sidebarEl.classList.remove('bx-mc-expanded');
+        sidebarEl.classList.remove('xl:w-64');
+        sidebarEl.classList.add('xl:w-16');
+        mainContentEl.classList.remove('xl:ml-64');
+        mainContentEl.classList.add('xl:ml-16');
+        document.querySelectorAll('#' + sidebarEl.getAttribute('id') + ' [sidebar-toggle-item]').forEach(sidebarToggleEl => {
+            sidebarToggleEl.classList.add('xl:hidden');
+            sidebarToggleEl.classList.add('xl:absolute');
+            sidebarToggleEl.classList.add('bx-mpi-toggle-hidden');
+        });
+
+        // toggle multi level menu item initial and full text
+        document.querySelectorAll('#' + sidebar.getAttribute('id') + ' ul > li > ul > li > a').forEach(e => {
+            e.classList.remove('pl-11');
+            e.classList.add('px-4');
+            e.childNodes[0].classList.add('hidden');
+            e.childNodes[1].classList.remove('hidden');
+        });
+
+        bottomMenuEl.classList.add('flex-col', 'space-y-4', 'p-2');
+        bottomMenuEl.classList.remove('space-x-4', 'p-4');
+        setExpanded ? toggleSidebarEl.setAttribute('aria-expanded', 'false') : null;
+    }
+}
+
+const toggleSidebarMobile = (sidebar, sidebarBackdrop, toggleSidebarMobileHamburger, toggleSidebarMobileClose) => {
+    sidebar.classList.remove('bx-mc-narrowed');
+    sidebar.classList.add('bx-mc-expanded');
+    sidebar.classList.toggle('hidden');
+    sidebarBackdrop.classList.toggle('hidden');
+    toggleSidebarMobileHamburger.classList.toggle('hidden');
+    toggleSidebarMobileClose.classList.toggle('hidden');
+}
+
+const sidebar = document.getElementById('sidebar');
+const toggleSidebarEl = document.getElementById('toggleSidebar');
+if(sidebar) {
+    document.querySelectorAll('#' + sidebar.getAttribute('id') + ' ul > li > ul > li > a').forEach(e => {
+        var fullText = e.textContent;
+        var firstLetter = fullText.substring(0, 1);
+
+        var fullTextEl = document.createElement('span');
+        var firstLetterEl = document.createElement('span');
+        firstLetterEl.classList.add('hidden');
+        fullTextEl.textContent = fullText;
+        firstLetterEl.textContent = firstLetter;
+
+        e.textContent = '';
+        e.appendChild(fullTextEl);
+        e.appendChild(firstLetterEl);
+    });
+
+    // initialize sidebar
+    if (localStorage.getItem('sidebarExpanded') !== null) {
+        if (localStorage.getItem('sidebarExpanded') === 'true') {
+            toggleSidebar(sidebar, true, false);
+        } else {
+            toggleSidebar(sidebar, false, true);
+        }
+    }
+
+    if(toggleSidebarEl) {
+        toggleSidebarEl.addEventListener('click', () => {
+            localStorage.setItem('sidebarExpanded', !isSidebarExpanded(toggleSidebarEl));
+            toggleSidebar(sidebar, !isSidebarExpanded(toggleSidebarEl), true);
+        });
+
+        sidebar.addEventListener('mouseenter', () => {
+            if (!isSidebarExpanded(toggleSidebarEl)) {
+                toggleSidebar(sidebar, true);
+            }
+        });
+
+        sidebar.addEventListener('mouseleave', () => {
+            if (!isSidebarExpanded(toggleSidebarEl)) {
+                toggleSidebar(sidebar, false);
+            }
+        });
+    }    
+}
+
+const toggleSidebarMobileEl = document.getElementById('toggleSidebarMobile');
+if(toggleSidebarMobileEl) {
+    const sidebarBackdrop = document.getElementById('sidebarBackdrop');
+    const toggleSidebarMobileHamburger = document.getElementById('toggleSidebarMobileHamburger');
+    const toggleSidebarMobileClose = document.getElementById('toggleSidebarMobileClose');
+
+    toggleSidebarMobileEl.addEventListener('click', () => {
+        toggleSidebarMobile(sidebar, sidebarBackdrop, toggleSidebarMobileHamburger, toggleSidebarMobileClose);
+    });
+
+    sidebarBackdrop.addEventListener('click', () => {
+        toggleSidebarMobile(sidebar, sidebarBackdrop, toggleSidebarMobileHamburger, toggleSidebarMobileClose);
+    });
+}

+ 1064 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/text/classes/BxBaseModTextTemplate.php

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

+ 25 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/modules/base/text/template/unit_gallery.html

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

+ 7 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins/autoload.php

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

+ 15 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins/composer/autoload_files.php

@@ -0,0 +1,15 @@
+<?php
+
+// autoload_files.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+    '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
+    '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
+    'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
+    '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
+    '2cffec82183ee1cea088009cef9a6fc3' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php',
+    'c65d09b6820da036953a371c8c73a9b1' => $vendorDir . '/facebook/graph-sdk/src/Facebook/polyfills.php',
+);

+ 75 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins/composer/autoload_real.php

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

+ 224 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins/composer/autoload_static.php

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

+ 348 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins/composer/installed.php

@@ -0,0 +1,348 @@
+<?php return array(
+    'root' => array(
+        'pretty_version' => 'dev-master',
+        'version' => 'dev-master',
+        'type' => 'library',
+        'install_path' => __DIR__ . '/../../',
+        'aliases' => array(),
+        'reference' => 'b394e193f81dc259ca737293aa587d5dd3df35e3',
+        'name' => 'unaio/una',
+        'dev' => true,
+    ),
+    'versions' => array(
+        'achingbrain/php5-akismet' => array(
+            'pretty_version' => '0.5',
+            'version' => '0.5.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../achingbrain/php5-akismet',
+            'aliases' => array(),
+            'reference' => 'master',
+            'dev_requirement' => false,
+        ),
+        'akeeba/s3' => array(
+            'pretty_version' => 'dev-development',
+            'version' => 'dev-development',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../akeeba/s3',
+            'aliases' => array(
+                0 => '9999999-dev',
+            ),
+            'reference' => '6448ddf99541f3ba50f97e373ceeb5bfd42a0167',
+            'dev_requirement' => false,
+        ),
+        'avto-dev/composer-cleanup-plugin' => array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'type' => 'composer-plugin',
+            'install_path' => __DIR__ . '/../avto-dev/composer-cleanup-plugin',
+            'aliases' => array(
+                0 => '9999999-dev',
+            ),
+            'reference' => '75187ca4854265174021765e3b67cb973dc37723',
+            'dev_requirement' => false,
+        ),
+        'bshaffer/oauth2-server-php' => array(
+            'pretty_version' => 'v1.12.1',
+            'version' => '1.12.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../bshaffer/oauth2-server-php',
+            'aliases' => array(),
+            'reference' => '2bfaf9d7bbebe2ba1c1deb48e756ba0b3af4e985',
+            'dev_requirement' => false,
+        ),
+        'chargebee/chargebee-php' => array(
+            'pretty_version' => 'v3.11.0',
+            'version' => '3.11.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../chargebee/chargebee-php',
+            'aliases' => array(),
+            'reference' => '2b74d71005693f8f11cc2ef1aea07c48a8f8e155',
+            'dev_requirement' => false,
+        ),
+        'curl/cacert' => array(
+            'pretty_version' => '0.0.1',
+            'version' => '0.0.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../curl/cacert',
+            'aliases' => array(),
+            'reference' => NULL,
+            'dev_requirement' => false,
+        ),
+        'erusev/parsedown' => array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../erusev/parsedown',
+            'aliases' => array(
+                0 => '9999999-dev',
+            ),
+            'reference' => '6598f3860c2698fe2f0f1bc98212fc01d0a1893c',
+            'dev_requirement' => false,
+        ),
+        'ezyang/htmlpurifier' => array(
+            'pretty_version' => 'v4.16.0',
+            'version' => '4.16.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../ezyang/htmlpurifier',
+            'aliases' => array(),
+            'reference' => '523407fb06eb9e5f3d59889b3978d5bfe94299c8',
+            'dev_requirement' => false,
+        ),
+        'facebook/graph-sdk' => array(
+            'pretty_version' => '5.7.0',
+            'version' => '5.7.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../facebook/graph-sdk',
+            'aliases' => array(),
+            'reference' => '2d8250638b33d73e7a87add65f47fabf91f8ad9b',
+            'dev_requirement' => false,
+        ),
+        'ffmpeg/ffmpeg' => array(
+            'pretty_version' => '4.4.0',
+            'version' => '4.4.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../ffmpeg/ffmpeg',
+            'aliases' => array(),
+            'reference' => NULL,
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/guzzle' => array(
+            'pretty_version' => '7.5.0',
+            'version' => '7.5.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/guzzle',
+            'aliases' => array(),
+            'reference' => 'b50a2a1251152e43f6a37f0fa053e730a67d25ba',
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/promises' => array(
+            'pretty_version' => '1.5.2',
+            'version' => '1.5.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/promises',
+            'aliases' => array(),
+            'reference' => 'b94b2807d85443f9719887892882d0329d1e2598',
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/psr7' => array(
+            'pretty_version' => '2.4.1',
+            'version' => '2.4.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/psr7',
+            'aliases' => array(),
+            'reference' => '69568e4293f4fa993f3b0e51c9723e1e17c41379',
+            'dev_requirement' => false,
+        ),
+        'intervention/image' => array(
+            'pretty_version' => '2.7.2',
+            'version' => '2.7.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../intervention/image',
+            'aliases' => array(),
+            'reference' => '04be355f8d6734c826045d02a1079ad658322dad',
+            'dev_requirement' => false,
+        ),
+        'lusitanian/oauth' => array(
+            'pretty_version' => 'v0.8.11',
+            'version' => '0.8.11.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../lusitanian/oauth',
+            'aliases' => array(),
+            'reference' => 'fc11a53db4b66da555a6a11fce294f574a8374f9',
+            'dev_requirement' => false,
+        ),
+        'matthiasmullie/minify' => array(
+            'pretty_version' => '1.3.69',
+            'version' => '1.3.69.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../matthiasmullie/minify',
+            'aliases' => array(),
+            'reference' => 'a61c949cccd086808063611ef9698eabe42ef22f',
+            'dev_requirement' => false,
+        ),
+        'matthiasmullie/path-converter' => array(
+            'pretty_version' => '1.1.3',
+            'version' => '1.1.3.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../matthiasmullie/path-converter',
+            'aliases' => array(),
+            'reference' => 'e7d13b2c7e2f2268e1424aaed02085518afa02d9',
+            'dev_requirement' => false,
+        ),
+        'mpratt/embera' => array(
+            'pretty_version' => '2.0.27',
+            'version' => '2.0.27.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../mpratt/embera',
+            'aliases' => array(),
+            'reference' => '9c16a9559fa75d9a9d5efddb17cd34555a6dff5e',
+            'dev_requirement' => false,
+        ),
+        'paypal/paypal-checkout-sdk' => array(
+            'pretty_version' => '1.0.2',
+            'version' => '1.0.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../paypal/paypal-checkout-sdk',
+            'aliases' => array(),
+            'reference' => '19992ce7051ff9e47e643f28abb8cc1b3e5f1812',
+            'dev_requirement' => false,
+        ),
+        'paypal/paypalhttp' => array(
+            'pretty_version' => '1.0.1',
+            'version' => '1.0.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../paypal/paypalhttp',
+            'aliases' => array(),
+            'reference' => '7b09c89c80828e842c79230e7f156b61fbb68d25',
+            'dev_requirement' => false,
+        ),
+        'phpmailer/phpmailer' => array(
+            'pretty_version' => 'v6.6.4',
+            'version' => '6.6.4.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../phpmailer/phpmailer',
+            'aliases' => array(),
+            'reference' => 'a94fdebaea6bd17f51be0c2373ab80d3d681269b',
+            'dev_requirement' => false,
+        ),
+        'psr/http-client' => array(
+            'pretty_version' => '1.0.1',
+            'version' => '1.0.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-client',
+            'aliases' => array(),
+            'reference' => '2dfb5f6c5eff0e91e20e913f8c5452ed95b86621',
+            'dev_requirement' => false,
+        ),
+        'psr/http-client-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        'psr/http-factory' => array(
+            'pretty_version' => '1.0.1',
+            'version' => '1.0.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-factory',
+            'aliases' => array(),
+            'reference' => '12ac7fcd07e5b077433f5f2bee95b3a771bf61be',
+            'dev_requirement' => false,
+        ),
+        'psr/http-factory-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        'psr/http-message' => array(
+            'pretty_version' => '1.0.1',
+            'version' => '1.0.1.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-message',
+            'aliases' => array(),
+            'reference' => 'f6561bf28d520154e4b0ec72be95418abe6d9363',
+            'dev_requirement' => false,
+        ),
+        'psr/http-message-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        'ralouphie/getallheaders' => array(
+            'pretty_version' => '3.0.3',
+            'version' => '3.0.3.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../ralouphie/getallheaders',
+            'aliases' => array(),
+            'reference' => '120b605dfeb996808c31b6477290a714d356e822',
+            'dev_requirement' => false,
+        ),
+        'snipe/banbuilder' => array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../snipe/banbuilder',
+            'aliases' => array(
+                0 => '9999999-dev',
+            ),
+            'reference' => '621d6d857400983d0fd6cd291f105a835a317748',
+            'dev_requirement' => false,
+        ),
+        'spatie/calendar-links' => array(
+            'pretty_version' => '1.8.0',
+            'version' => '1.8.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../spatie/calendar-links',
+            'aliases' => array(),
+            'reference' => '654fc0174e6279539b9bf0abdc3339e34c5286d7',
+            'dev_requirement' => false,
+        ),
+        'stripe/stripe-php' => array(
+            'pretty_version' => 'v7.128.0',
+            'version' => '7.128.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../stripe/stripe-php',
+            'aliases' => array(),
+            'reference' => 'c704949c49b72985c76cc61063aa26fefbd2724e',
+            'dev_requirement' => false,
+        ),
+        'symfony/deprecation-contracts' => array(
+            'pretty_version' => 'v2.5.2',
+            'version' => '2.5.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
+            'aliases' => array(),
+            'reference' => 'e8b495ea28c1d97b5e0c121748d6f9b53d075c66',
+            'dev_requirement' => false,
+        ),
+        'tpyo/amazon-s3-php-class' => array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../tpyo/amazon-s3-php-class',
+            'aliases' => array(
+                0 => '9999999-dev',
+            ),
+            'reference' => '7c62b0e9b7d956a04e377ca236979a78dbac4e66',
+            'dev_requirement' => false,
+        ),
+        'twilio/sdk' => array(
+            'pretty_version' => '5.42.2',
+            'version' => '5.42.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../twilio/sdk',
+            'aliases' => array(),
+            'reference' => '0cfcb871b18a9c427dd9e8f0ed7458d43009b48a',
+            'dev_requirement' => false,
+        ),
+        'unaio/amazon-s3-php-class-hmac-v2' => array(
+            'pretty_version' => '0.6',
+            'version' => '0.6.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../unaio/amazon-s3-php-class-hmac-v2',
+            'aliases' => array(),
+            'reference' => '3687dec0af7be2cb11f21828aa4b12d20c4c5935',
+            'dev_requirement' => false,
+        ),
+        'unaio/una' => array(
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../../',
+            'aliases' => array(),
+            'reference' => 'b394e193f81dc259ca737293aa587d5dd3df35e3',
+            'dev_requirement' => false,
+        ),
+        'wikimedia/less.php' => array(
+            'pretty_version' => 'v3.1.0',
+            'version' => '3.1.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../wikimedia/less.php',
+            'aliases' => array(),
+            'reference' => 'a486d78b9bd16b72f237fc6093aa56d69ce8bd13',
+            'dev_requirement' => false,
+        ),
+    ),
+);

Разница между файлами не показана из-за своего большого размера
+ 5 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/index.global.min.js


+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/af.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"af",week:{dow:1,doy:4},buttonText:{prev:"Vorige",next:"Volgende",today:"Vandag",year:"Jaar",month:"Maand",week:"Week",day:"Dag",list:"Agenda"},allDayText:"Heeldag",moreLinkText:"Addisionele",noEventsText:"Daar is geen gebeurtenisse nie"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ar-dz.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"ar-dz",week:{dow:0,doy:4},direction:"rtl",buttonText:{prev:"السابق",next:"التالي",today:"اليوم",month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},weekText:"أسبوع",allDayText:"اليوم كله",moreLinkText:"أخرى",noEventsText:"أي أحداث لعرض"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ar-kw.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"ar-kw",week:{dow:0,doy:12},direction:"rtl",buttonText:{prev:"السابق",next:"التالي",today:"اليوم",month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},weekText:"أسبوع",allDayText:"اليوم كله",moreLinkText:"أخرى",noEventsText:"أي أحداث لعرض"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ar-ly.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"ar-ly",week:{dow:6,doy:12},direction:"rtl",buttonText:{prev:"السابق",next:"التالي",today:"اليوم",month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},weekText:"أسبوع",allDayText:"اليوم كله",moreLinkText:"أخرى",noEventsText:"أي أحداث لعرض"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ar-ma.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"ar-ma",week:{dow:6,doy:12},direction:"rtl",buttonText:{prev:"السابق",next:"التالي",today:"اليوم",month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},weekText:"أسبوع",allDayText:"اليوم كله",moreLinkText:"أخرى",noEventsText:"أي أحداث لعرض"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ar-sa.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"ar-sa",week:{dow:0,doy:6},direction:"rtl",buttonText:{prev:"السابق",next:"التالي",today:"اليوم",month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},weekText:"أسبوع",allDayText:"اليوم كله",moreLinkText:"أخرى",noEventsText:"أي أحداث لعرض"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ar-tn.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"ar-tn",week:{dow:1,doy:4},direction:"rtl",buttonText:{prev:"السابق",next:"التالي",today:"اليوم",month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},weekText:"أسبوع",allDayText:"اليوم كله",moreLinkText:"أخرى",noEventsText:"أي أحداث لعرض"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ar.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"ar",week:{dow:6,doy:12},direction:"rtl",buttonText:{prev:"السابق",next:"التالي",today:"اليوم",month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},weekText:"أسبوع",allDayText:"اليوم كله",moreLinkText:"أخرى",noEventsText:"أي أحداث لعرض"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/az.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var t={code:"az",week:{dow:1,doy:4},buttonText:{prev:"Əvvəl",next:"Sonra",today:"Bu Gün",month:"Ay",week:"Həftə",day:"Gün",list:"Gündəm"},weekText:"Həftə",allDayText:"Bütün Gün",moreLinkText:e=>"+ daha çox "+e,noEventsText:"Göstərmək üçün hadisə yoxdur"};FullCalendar.globalLocales.push(t)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/bg.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var t={code:"bg",week:{dow:1,doy:7},buttonText:{prev:"назад",next:"напред",today:"днес",month:"Месец",week:"Седмица",day:"Ден",list:"График"},allDayText:"Цял ден",moreLinkText:e=>"+още "+e,noEventsText:"Няма събития за показване"};FullCalendar.globalLocales.push(t)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/bn.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var t={code:"bn",week:{dow:0,doy:6},buttonText:{prev:"পেছনে",next:"সামনে",today:"আজ",month:"মাস",week:"সপ্তাহ",day:"দিন",list:"তালিকা"},weekText:"সপ্তাহ",allDayText:"সারাদিন",moreLinkText:e=>"+অন্যান্য "+e,noEventsText:"কোনো ইভেন্ট নেই"};FullCalendar.globalLocales.push(t)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/bs.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var a={code:"bs",week:{dow:1,doy:7},buttonText:{prev:"Prošli",next:"Sljedeći",today:"Danas",month:"Mjesec",week:"Sedmica",day:"Dan",list:"Raspored"},weekText:"Sed",allDayText:"Cijeli dan",moreLinkText:e=>"+ još "+e,noEventsText:"Nema događaja za prikazivanje"};FullCalendar.globalLocales.push(a)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ca.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"ca",week:{dow:1,doy:4},buttonText:{prev:"Anterior",next:"Següent",today:"Avui",month:"Mes",week:"Setmana",day:"Dia",list:"Agenda"},weekText:"Set",allDayText:"Tot el dia",moreLinkText:"més",noEventsText:"No hi ha esdeveniments per mostrar"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/cs.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var n={code:"cs",week:{dow:1,doy:4},buttonText:{prev:"Dříve",next:"Později",today:"Nyní",month:"Měsíc",week:"Týden",day:"Den",list:"Agenda"},weekText:"Týd",allDayText:"Celý den",moreLinkText:e=>"+další: "+e,noEventsText:"Žádné akce k zobrazení"};FullCalendar.globalLocales.push(n)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/cy.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"cy",week:{dow:1,doy:4},buttonText:{prev:"Blaenorol",next:"Nesaf",today:"Heddiw",year:"Blwyddyn",month:"Mis",week:"Wythnos",day:"Dydd",list:"Rhestr"},weekText:"Wythnos",allDayText:"Trwy'r dydd",moreLinkText:"Mwy",noEventsText:"Dim digwyddiadau"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/da.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"da",week:{dow:1,doy:4},buttonText:{prev:"Forrige",next:"Næste",today:"I dag",month:"Måned",week:"Uge",day:"Dag",list:"Agenda"},weekText:"Uge",allDayText:"Hele dagen",moreLinkText:"flere",noEventsText:"Ingen arrangementer at vise"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/de-at.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";function t(e){return"Tag"===e||"Monat"===e?"r":"Jahr"===e?"s":""}var n={code:"de-at",week:{dow:1,doy:4},buttonText:{prev:"Zurück",next:"Vor",today:"Heute",year:"Jahr",month:"Monat",week:"Woche",day:"Tag",list:"Terminübersicht"},weekText:"KW",weekTextLong:"Woche",allDayText:"Ganztägig",moreLinkText:e=>"+ weitere "+e,noEventsText:"Keine Ereignisse anzuzeigen",buttonHints:{prev:e=>`Vorherige${t(e)} ${e}`,next:e=>`Nächste${t(e)} ${e}`,today:e=>"Tag"===e?"Heute":`Diese${t(e)} ${e}`},viewHint:e=>e+("Woche"===e?"n":"Monat"===e?"s":"es")+"ansicht",navLinkHint:"Gehe zu $0",moreLinkHint:e=>"Zeige "+(1===e?"ein weiteres Ereignis":e+" weitere Ereignisse"),closeHint:"Schließen",timeHint:"Uhrzeit",eventHint:"Ereignis"};FullCalendar.globalLocales.push(n)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/de.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";function t(e){return"Tag"===e||"Monat"===e?"r":"Jahr"===e?"s":""}var n={code:"de",week:{dow:1,doy:4},buttonText:{prev:"Zurück",next:"Vor",today:"Heute",year:"Jahr",month:"Monat",week:"Woche",day:"Tag",list:"Terminübersicht"},weekText:"KW",weekTextLong:"Woche",allDayText:"Ganztägig",moreLinkText:e=>"+ weitere "+e,noEventsText:"Keine Ereignisse anzuzeigen",buttonHints:{prev:e=>`Vorherige${t(e)} ${e}`,next:e=>`Nächste${t(e)} ${e}`,today:e=>"Tag"===e?"Heute":`Diese${t(e)} ${e}`},viewHint:e=>e+("Woche"===e?"n":"Monat"===e?"s":"es")+"ansicht",navLinkHint:"Gehe zu $0",moreLinkHint:e=>"Zeige "+(1===e?"ein weiteres Ereignis":e+" weitere Ereignisse"),closeHint:"Schließen",timeHint:"Uhrzeit",eventHint:"Ereignis"};FullCalendar.globalLocales.push(n)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/el.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"el",week:{dow:1,doy:4},buttonText:{prev:"Προηγούμενος",next:"Επόμενος",today:"Σήμερα",month:"Μήνας",week:"Εβδομάδα",day:"Ημέρα",list:"Ατζέντα"},weekText:"Εβδ",allDayText:"Ολοήμερο",moreLinkText:"περισσότερα",noEventsText:"Δεν υπάρχουν γεγονότα προς εμφάνιση"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/en-au.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var n={code:"en-au",week:{dow:1,doy:4},buttonHints:{prev:"Previous $0",next:"Next $0",today:"This $0"},viewHint:"$0 view",navLinkHint:"Go to $0",moreLinkHint:e=>`Show ${e} more event${1===e?"":"s"}`};FullCalendar.globalLocales.push(n)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/en-gb.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var n={code:"en-gb",week:{dow:1,doy:4},buttonHints:{prev:"Previous $0",next:"Next $0",today:"This $0"},viewHint:"$0 view",navLinkHint:"Go to $0",moreLinkHint:e=>`Show ${e} more event${1===e?"":"s"}`};FullCalendar.globalLocales.push(n)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/en-nz.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var n={code:"en-nz",week:{dow:1,doy:4},buttonHints:{prev:"Previous $0",next:"Next $0",today:"This $0"},viewHint:"$0 view",navLinkHint:"Go to $0",moreLinkHint:e=>`Show ${e} more event${1===e?"":"s"}`};FullCalendar.globalLocales.push(n)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/eo.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"eo",week:{dow:1,doy:4},buttonText:{prev:"Antaŭa",next:"Sekva",today:"Hodiaŭ",month:"Monato",week:"Semajno",day:"Tago",list:"Tagordo"},weekText:"Sm",allDayText:"Tuta tago",moreLinkText:"pli",noEventsText:"Neniuj eventoj por montri"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/es-us.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"es",week:{dow:0,doy:6},buttonText:{prev:"Ant",next:"Sig",today:"Hoy",month:"Mes",week:"Semana",day:"Día",list:"Agenda"},weekText:"Sm",allDayText:"Todo el día",moreLinkText:"más",noEventsText:"No hay eventos para mostrar"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/es.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var t={code:"es",week:{dow:1,doy:4},buttonText:{prev:"Ant",next:"Sig",today:"Hoy",month:"Mes",week:"Semana",day:"Día",list:"Agenda"},buttonHints:{prev:"$0 antes",next:"$0 siguiente",today:e=>"Día"===e?"Hoy":("Semana"===e?"Esta":"Este")+" "+e.toLocaleLowerCase()},viewHint:e=>"Vista "+("Semana"===e?"de la":"del")+" "+e.toLocaleLowerCase(),weekText:"Sm",weekTextLong:"Semana",allDayText:"Todo el día",moreLinkText:"más",moreLinkHint:e=>`Mostrar ${e} eventos más`,noEventsText:"No hay eventos para mostrar",navLinkHint:"Ir al $0",closeHint:"Cerrar",timeHint:"La hora",eventHint:"Evento"};FullCalendar.globalLocales.push(t)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/et.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var t={code:"et",week:{dow:1,doy:4},buttonText:{prev:"Eelnev",next:"Järgnev",today:"Täna",month:"Kuu",week:"Nädal",day:"Päev",list:"Päevakord"},weekText:"näd",allDayText:"Kogu päev",moreLinkText:e=>"+ veel "+e,noEventsText:"Kuvamiseks puuduvad sündmused"};FullCalendar.globalLocales.push(t)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/eu.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"eu",week:{dow:1,doy:7},buttonText:{prev:"Aur",next:"Hur",today:"Gaur",month:"Hilabetea",week:"Astea",day:"Eguna",list:"Agenda"},weekText:"As",allDayText:"Egun osoa",moreLinkText:"gehiago",noEventsText:"Ez dago ekitaldirik erakusteko"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/fa.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var t={code:"fa",week:{dow:6,doy:12},direction:"rtl",buttonText:{prev:"قبلی",next:"بعدی",today:"امروز",month:"ماه",week:"هفته",day:"روز",list:"برنامه"},weekText:"هف",allDayText:"تمام روز",moreLinkText:e=>"بیش از "+e,noEventsText:"هیچ رویدادی به نمایش"};FullCalendar.globalLocales.push(t)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/fi.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"fi",week:{dow:1,doy:4},buttonText:{prev:"Edellinen",next:"Seuraava",today:"Tänään",month:"Kuukausi",week:"Viikko",day:"Päivä",list:"Tapahtumat"},weekText:"Vk",allDayText:"Koko päivä",moreLinkText:"lisää",noEventsText:"Ei näytettäviä tapahtumia"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/fr-ca.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"fr",buttonText:{prev:"Précédent",next:"Suivant",today:"Aujourd'hui",year:"Année",month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},weekText:"Sem.",allDayText:"Toute la journée",moreLinkText:"en plus",noEventsText:"Aucun événement à afficher"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/fr-ch.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"fr-ch",week:{dow:1,doy:4},buttonText:{prev:"Précédent",next:"Suivant",today:"Courant",year:"Année",month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},weekText:"Sm",allDayText:"Toute la journée",moreLinkText:"en plus",noEventsText:"Aucun événement à afficher"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/fr.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"fr",week:{dow:1,doy:4},buttonText:{prev:"Précédent",next:"Suivant",today:"Aujourd'hui",year:"Année",month:"Mois",week:"Semaine",day:"Jour",list:"Planning"},weekText:"Sem.",allDayText:"Toute la journée",moreLinkText:"en plus",noEventsText:"Aucun événement à afficher"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/gl.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"gl",week:{dow:1,doy:4},buttonText:{prev:"Ant",next:"Seg",today:"Hoxe",month:"Mes",week:"Semana",day:"Día",list:"Axenda"},weekText:"Sm",allDayText:"Todo o día",moreLinkText:"máis",noEventsText:"Non hai eventos para amosar"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/he.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"he",direction:"rtl",buttonText:{prev:"הקודם",next:"הבא",today:"היום",month:"חודש",week:"שבוע",day:"יום",list:"סדר יום"},allDayText:"כל היום",moreLinkText:"אחר",noEventsText:"אין אירועים להצגה",weekText:"שבוע"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/hi.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var t={code:"hi",week:{dow:0,doy:6},buttonText:{prev:"पिछला",next:"अगला",today:"आज",month:"महीना",week:"सप्ताह",day:"दिन",list:"कार्यसूची"},weekText:"हफ्ता",allDayText:"सभी दिन",moreLinkText:e=>"+अधिक "+e,noEventsText:"कोई घटनाओं को प्रदर्शित करने के लिए"};FullCalendar.globalLocales.push(t)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/hr.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var a={code:"hr",week:{dow:1,doy:7},buttonText:{prev:"Prijašnji",next:"Sljedeći",today:"Danas",month:"Mjesec",week:"Tjedan",day:"Dan",list:"Raspored"},weekText:"Tje",allDayText:"Cijeli dan",moreLinkText:e=>"+ još "+e,noEventsText:"Nema događaja za prikaz"};FullCalendar.globalLocales.push(a)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/hu.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"hu",week:{dow:1,doy:4},buttonText:{prev:"vissza",next:"előre",today:"ma",month:"Hónap",week:"Hét",day:"Nap",list:"Lista"},weekText:"Hét",allDayText:"Egész nap",moreLinkText:"további",noEventsText:"Nincs megjeleníthető esemény"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/hy-am.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var t={code:"hy-am",week:{dow:1,doy:4},buttonText:{prev:"Նախորդ",next:"Հաջորդ",today:"Այսօր",month:"Ամիս",week:"Շաբաթ",day:"Օր",list:"Օրվա ցուցակ"},weekText:"Շաբ",allDayText:"Ամբողջ օր",moreLinkText:e=>"+ ևս "+e,noEventsText:"Բացակայում է իրադարձությունը ցուցադրելու"};FullCalendar.globalLocales.push(t)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/id.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"id",week:{dow:1,doy:7},buttonText:{prev:"mundur",next:"maju",today:"hari ini",month:"Bulan",week:"Minggu",day:"Hari",list:"Agenda"},weekText:"Mg",allDayText:"Sehari penuh",moreLinkText:"lebih",noEventsText:"Tidak ada acara untuk ditampilkan"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/is.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"is",week:{dow:1,doy:4},buttonText:{prev:"Fyrri",next:"Næsti",today:"Í dag",month:"Mánuður",week:"Vika",day:"Dagur",list:"Dagskrá"},weekText:"Vika",allDayText:"Allan daginn",moreLinkText:"meira",noEventsText:"Engir viðburðir til að sýna"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/it.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var t={code:"it",week:{dow:1,doy:4},buttonText:{prev:"Prec",next:"Succ",today:"Oggi",month:"Mese",week:"Settimana",day:"Giorno",list:"Agenda"},weekText:"Sm",allDayText:"Tutto il giorno",moreLinkText:e=>"+altri "+e,noEventsText:"Non ci sono eventi da visualizzare"};FullCalendar.globalLocales.push(t)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ja.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var t={code:"ja",buttonText:{prev:"前",next:"次",today:"今日",month:"月",week:"週",day:"日",list:"予定リスト"},weekText:"週",allDayText:"終日",moreLinkText:e=>"他 "+e+" 件",noEventsText:"表示する予定はありません"};FullCalendar.globalLocales.push(t)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ka.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var t={code:"ka",week:{dow:1,doy:7},buttonText:{prev:"წინა",next:"შემდეგი",today:"დღეს",month:"თვე",week:"კვირა",day:"დღე",list:"დღის წესრიგი"},weekText:"კვ",allDayText:"მთელი დღე",moreLinkText:e=>"+ კიდევ "+e,noEventsText:"ღონისძიებები არ არის"};FullCalendar.globalLocales.push(t)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/kk.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var t={code:"kk",week:{dow:1,doy:7},buttonText:{prev:"Алдыңғы",next:"Келесі",today:"Бүгін",month:"Ай",week:"Апта",day:"Күн",list:"Күн тәртібі"},weekText:"Не",allDayText:"Күні бойы",moreLinkText:e=>"+ тағы "+e,noEventsText:"Көрсету үшін оқиғалар жоқ"};FullCalendar.globalLocales.push(t)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/km.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"km",week:{dow:1,doy:4},buttonText:{prev:"មុន",next:"បន្ទាប់",today:"ថ្ងៃនេះ",year:"ឆ្នាំ",month:"ខែ",week:"សប្តាហ៍",day:"ថ្ងៃ",list:"បញ្ជី"},weekText:"សប្តាហ៍",allDayText:"ពេញមួយថ្ងៃ",moreLinkText:"ច្រើនទៀត",noEventsText:"គ្មានព្រឹត្តិការណ៍ត្រូវបង្ហាញ"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ko.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"ko",buttonText:{prev:"이전달",next:"다음달",today:"오늘",month:"월",week:"주",day:"일",list:"일정목록"},weekText:"주",allDayText:"종일",moreLinkText:"개",noEventsText:"일정이 없습니다"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ku.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"ku",week:{dow:6,doy:12},direction:"rtl",buttonText:{prev:"پێشتر",next:"دواتر",today:"ئەمڕو",month:"مانگ",week:"هەفتە",day:"ڕۆژ",list:"بەرنامە"},weekText:"هەفتە",allDayText:"هەموو ڕۆژەکە",moreLinkText:"زیاتر",noEventsText:"هیچ ڕووداوێك نیە"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/lb.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"lb",week:{dow:1,doy:4},buttonText:{prev:"Zréck",next:"Weider",today:"Haut",month:"Mount",week:"Woch",day:"Dag",list:"Terminiwwersiicht"},weekText:"W",allDayText:"Ganzen Dag",moreLinkText:"méi",noEventsText:"Nee Evenementer ze affichéieren"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/lt.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"lt",week:{dow:1,doy:4},buttonText:{prev:"Atgal",next:"Pirmyn",today:"Šiandien",month:"Mėnuo",week:"Savaitė",day:"Diena",list:"Darbotvarkė"},weekText:"SAV",allDayText:"Visą dieną",moreLinkText:"daugiau",noEventsText:"Nėra įvykių rodyti"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/lv.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var t={code:"lv",week:{dow:1,doy:4},buttonText:{prev:"Iepr.",next:"Nāk.",today:"Šodien",month:"Mēnesis",week:"Nedēļa",day:"Diena",list:"Dienas kārtība"},weekText:"Ned.",allDayText:"Visu dienu",moreLinkText:e=>"+vēl "+e,noEventsText:"Nav notikumu"};FullCalendar.globalLocales.push(t)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/mk.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var t={code:"mk",buttonText:{prev:"претходно",next:"следно",today:"Денес",month:"Месец",week:"Недела",day:"Ден",list:"График"},weekText:"Сед",allDayText:"Цел ден",moreLinkText:e=>"+повеќе "+e,noEventsText:"Нема настани за прикажување"};FullCalendar.globalLocales.push(t)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ms.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(a){"use strict";var e={code:"ms",week:{dow:1,doy:7},buttonText:{prev:"Sebelum",next:"Selepas",today:"hari ini",month:"Bulan",week:"Minggu",day:"Hari",list:"Agenda"},weekText:"Mg",allDayText:"Sepanjang hari",moreLinkText:a=>"masih ada "+a+" acara",noEventsText:"Tiada peristiwa untuk dipaparkan"};FullCalendar.globalLocales.push(e)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/nb.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var n={code:"nb",week:{dow:1,doy:4},buttonText:{prev:"Forrige",next:"Neste",today:"I dag",month:"Måned",week:"Uke",day:"Dag",list:"Agenda"},weekText:"Uke",weekTextLong:"Uke",allDayText:"Hele dagen",moreLinkText:"til",noEventsText:"Ingen hendelser å vise",buttonHints:{prev:"Forrige $0",next:"Neste $0",today:"Nåværende $0"},viewHint:"$0 visning",navLinkHint:"Gå til $0",moreLinkHint:e=>`Vis ${e} flere hendelse${1===e?"":"r"}`};FullCalendar.globalLocales.push(n)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ne.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"ne",week:{dow:7,doy:1},buttonText:{prev:"अघिल्लो",next:"अर्को",today:"आज",month:"महिना",week:"हप्ता",day:"दिन",list:"सूची"},weekText:"हप्ता",allDayText:"दिनभरि",moreLinkText:"थप लिंक",noEventsText:"देखाउनको लागि कुनै घटनाहरू छैनन्"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/nl.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"nl",week:{dow:1,doy:4},buttonText:{prev:"Vorige",next:"Volgende",today:"Vandaag",year:"Jaar",month:"Maand",week:"Week",day:"Dag",list:"Agenda"},allDayText:"Hele dag",moreLinkText:"extra",noEventsText:"Geen evenementen om te laten zien"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/nn.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"nn",week:{dow:1,doy:4},buttonText:{prev:"Førre",next:"Neste",today:"I dag",month:"Månad",week:"Veke",day:"Dag",list:"Agenda"},weekText:"Veke",allDayText:"Heile dagen",moreLinkText:"til",noEventsText:"Ingen hendelser å vise"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/pl.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"pl",week:{dow:1,doy:4},buttonText:{prev:"Poprzedni",next:"Następny",today:"Dziś",month:"Miesiąc",week:"Tydzień",day:"Dzień",list:"Plan dnia"},weekText:"Tydz",allDayText:"Cały dzień",moreLinkText:"więcej",noEventsText:"Brak wydarzeń do wyświetlenia"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/pt-br.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var t={code:"pt-br",buttonText:{prev:"Anterior",next:"Próximo",today:"Hoje",month:"Mês",week:"Semana",day:"Dia",list:"Lista"},weekText:"Sm",allDayText:"dia inteiro",moreLinkText:e=>"mais +"+e,noEventsText:"Não há eventos para mostrar"};FullCalendar.globalLocales.push(t)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/pt.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"pt",week:{dow:1,doy:4},buttonText:{prev:"Anterior",next:"Seguinte",today:"Hoje",month:"Mês",week:"Semana",day:"Dia",list:"Agenda"},weekText:"Sem",allDayText:"Todo o dia",moreLinkText:"mais",noEventsText:"Não há eventos para mostrar"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ro.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var t={code:"ro",week:{dow:1,doy:7},buttonText:{prev:"precedentă",next:"următoare",today:"Azi",month:"Lună",week:"Săptămână",day:"Zi",list:"Agendă"},weekText:"Săpt",allDayText:"Toată ziua",moreLinkText:e=>"+alte "+e,noEventsText:"Nu există evenimente de afișat"};FullCalendar.globalLocales.push(t)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/ru.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var t={code:"ru",week:{dow:1,doy:4},buttonText:{prev:"Пред",next:"След",today:"Сегодня",month:"Месяц",week:"Неделя",day:"День",list:"Повестка дня"},weekText:"Нед",allDayText:"Весь день",moreLinkText:e=>"+ ещё "+e,noEventsText:"Нет событий для отображения"};FullCalendar.globalLocales.push(t)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/si-lk.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"si-lk",week:{dow:1,doy:4},buttonText:{prev:"පෙර",next:"පසු",today:"අද",month:"මාසය",week:"සතිය",day:"දවස",list:"ලැයිස්තුව"},weekText:"සති",allDayText:"සියලු",moreLinkText:"තවත්",noEventsText:"මුකුත් නැත"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/sk.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var a={code:"sk",week:{dow:1,doy:4},buttonText:{prev:"Predchádzajúci",next:"Nasledujúci",today:"Dnes",month:"Mesiac",week:"Týždeň",day:"Deň",list:"Rozvrh"},weekText:"Ty",allDayText:"Celý deň",moreLinkText:e=>"+ďalšie: "+e,noEventsText:"Žiadne akcie na zobrazenie"};FullCalendar.globalLocales.push(a)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/sl.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";FullCalendar.globalLocales.push({code:"sl",week:{dow:1,doy:7},buttonText:{prev:"Prejšnji",next:"Naslednji",today:"Trenutni",month:"Mesec",week:"Teden",day:"Dan",list:"Dnevni red"},weekText:"Teden",allDayText:"Ves dan",moreLinkText:"več",noEventsText:"Ni dogodkov za prikaz"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/sm.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(a){"use strict";FullCalendar.globalLocales.push({code:"sm",buttonText:{prev:"Talu ai",next:"Mulimuli atu",today:"Aso nei",month:"Masina",week:"Vaiaso",day:"Aso",list:"Faasologa"},weekText:"Vaiaso",allDayText:"Aso atoa",moreLinkText:"sili atu",noEventsText:"Leai ni mea na tutupu"})}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/sq.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var t={code:"sq",week:{dow:1,doy:4},buttonText:{prev:"mbrapa",next:"Përpara",today:"sot",month:"Muaj",week:"Javë",day:"Ditë",list:"Listë"},weekText:"Ja",allDayText:"Gjithë ditën",moreLinkText:e=>"+më tepër "+e,noEventsText:"Nuk ka evente për të shfaqur"};FullCalendar.globalLocales.push(t)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/sr-cyrl.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var t={code:"sr-cyrl",week:{dow:1,doy:7},buttonText:{prev:"Претходна",next:"следећи",today:"Данас",month:"Месец",week:"Недеља",day:"Дан",list:"Планер"},weekText:"Сед",allDayText:"Цео дан",moreLinkText:e=>"+ још "+e,noEventsText:"Нема догађаја за приказ"};FullCalendar.globalLocales.push(t)}();

+ 6 - 0
upgrade/files/13.0.0.RC1-13.0.0.RC2/files/plugins_public/fullcalendar/locales/sr.global.min.js

@@ -0,0 +1,6 @@
+/*!
+FullCalendar Core v6.0.2
+Docs & License: https://fullcalendar.io
+(c) 2022 Adam Shaw
+*/
+!function(e){"use strict";var a={code:"sr",week:{dow:1,doy:7},buttonText:{prev:"Prethodna",next:"Sledeći",today:"Danas",month:"Mеsеc",week:"Nеdеlja",day:"Dan",list:"Planеr"},weekText:"Sed",allDayText:"Cеo dan",moreLinkText:e=>"+ još "+e,noEventsText:"Nеma događaja za prikaz"};FullCalendar.globalLocales.push(a)}();

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