Explorar o código

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

AntonLV %!s(int64=8) %!d(string=hai) anos
pai
achega
a9f4336732
Modificáronse 100 ficheiros con 31869 adicións e 2 borrados
  1. 1 1
      inc/version.inc.php
  2. 1 1
      install/index.php
  3. 247 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDol.php
  4. 481 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolAccount.php
  5. 1438 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolCmts.php
  6. 519 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolCmtsQuery.php
  7. 752 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolConnection.php
  8. 303 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolContentInfo.php
  9. 83 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolContentInfoCmts.php
  10. 18 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolCronQueueEmail.php
  11. 18 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolCronQueuePush.php
  12. 1185 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolDb.php
  13. 184 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolEmailTemplates.php
  14. 1701 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolForm.php
  15. 222 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolFormQuery.php
  16. 406 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolLanguagesQuery.php
  17. 892 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolMetatags.php
  18. 145 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolMetatagsQuery.php
  19. 516 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolProfile.php
  20. 84 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolPush.php
  21. 53 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolQueue.php
  22. 58 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolQueueEmail.php
  23. 24 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolQueueEmailQuery.php
  24. 54 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolQueuePush.php
  25. 24 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolQueuePushQuery.php
  26. 82 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolQueueQuery.php
  27. 996 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolSearch.php
  28. 2677 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolTemplate.php
  29. 229 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolXmlParser.php
  30. 460 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/js/classes/BxDolGrid.js
  31. 47 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/js/editor.tinymce.js
  32. 907 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/js/functions.js
  33. 749 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/js/jquery.dolPopup.js
  34. 1627 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/utils.inc.php
  35. 12 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/version.inc.php
  36. 93 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/classes/BxBaseModGeneralCmtsSearchResult.php
  37. 201 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/classes/BxBaseModGeneralDb.php
  38. 285 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/classes/BxBaseModGeneralFormEntry.php
  39. 439 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/classes/BxBaseModGeneralFormsEntryHelper.php
  40. 153 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/classes/BxBaseModGeneralMenuSnippetMeta.php
  41. 1316 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/classes/BxBaseModGeneralModule.php
  42. 125 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/classes/BxBaseModGeneralPageEntry.php
  43. 109 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/classes/BxBaseModGeneralSearchResult.php
  44. 304 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/classes/BxBaseModGeneralTemplate.php
  45. 8 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/js/showcase.js
  46. 101 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/groups/classes/BxBaseModGroupsDb.php
  47. 114 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/groups/classes/BxBaseModGroupsMenuSnippetMeta.php
  48. 593 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/groups/classes/BxBaseModGroupsModule.php
  49. 90 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/groups/classes/BxBaseModGroupsPrivacy.php
  50. 86 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/groups/classes/BxBaseModGroupsSearchResult.php
  51. 1 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/groups/template/unit_with_cover_showcase.html
  52. 206 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/profile/classes/BxBaseModProfileFormsEntryHelper.php
  53. 908 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/profile/classes/BxBaseModProfileModule.php
  54. 107 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/profile/classes/BxBaseModProfileSearchResult.php
  55. 329 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/profile/classes/BxBaseModProfileTemplate.php
  56. 293 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/profile/template/css/main.css
  57. 1 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/profile/template/unit_with_cover_showcase.html
  58. 28 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/text/classes/BxBaseModTextFormEntry.php
  59. 88 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/text/classes/BxBaseModTextPageEntry.php
  60. 134 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/text/classes/BxBaseModTextSearchResult.php
  61. 246 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/text/classes/BxBaseModTextTemplate.php
  62. 230 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/text/template/css/main.css
  63. 25 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/text/template/unit_gallery.html
  64. 1 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/text/template/unit_showcase.html
  65. 6 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/plugins_public/headroom.min.js
  66. 92 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/plugins_public/tinymce-mention/css/autocomplete.css
  67. 3 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/plugins_public/tinymce-mention/css/rte-content.css
  68. 0 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/plugins_public/tinymce-mention/plugin.min.js
  69. 44 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/splash.php
  70. 331 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/studio/classes/BxDolStudioNavigationQuery.php
  71. 652 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/studio/classes/BxDolStudioToolsAudit.php
  72. 57 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/studio/js/navigation_import.js
  73. 458 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/studio/js/store.js
  74. 4 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/studio/template/_footer.html
  75. 69 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/studio/template/_header.html
  76. 69 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/studio/template/scripts/BxBaseStudioFunctions.php
  77. 172 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/studio/template/scripts/BxBaseStudioNavigationImport.php
  78. 4 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/_footer.html
  79. 39 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/_header.html
  80. 773 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/css/common.css
  81. 622 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/css/default.less
  82. 156 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/css/media-phone.css
  83. 549 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/css/menu.css
  84. 159 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/form_field_location.html
  85. 91 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/grid.html
  86. 7 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/menu_toolbar.html
  87. 29 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/page_2.html
  88. 39 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/page_44.html
  89. 271 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseAccountForms.php
  90. 849 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseCmts.php
  91. 261 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseEditorTinyMCE.php
  92. 613 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseFunctions.php
  93. 651 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseGrid.php
  94. 58 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseMenuCustom.php
  95. 92 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseMenuToolbar.php
  96. 182 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseSearchExtended.php
  97. 52 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseSearchExtendedServices.php
  98. 356 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseSearchResult.php
  99. 72 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseServiceMetatags.php
  100. 178 0
      upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseServices.php

+ 1 - 1
inc/version.inc.php

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

+ 1 - 1
install/index.php

@@ -10,7 +10,7 @@
 define ('BX_DOL_INSTALL',  true);
 define ('BX_SKIP_INSTALL_CHECK', true);
 
-define ('BX_DOL_VER', '9.0.0-DEV9');
+define ('BX_DOL_VER', '9.0.0-RC5');
 
 define ('BX_INSTALL_DEFAULT_LANGUAGE', 'en');
 

+ 247 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDol.php

@@ -0,0 +1,247 @@
+<?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
+ */
+
+/**
+ * @page objects Objects
+ * Classes which represents high level programming constructions to generate ready functionality, like Comments, Votings, Forms.
+ *
+ * @section captcha CAPTCHA
+ * @ref BxDolCaptcha
+ *
+ * @section category Category
+ * @ref BxDolCategory
+ *
+ * @section comments Comments
+ * @ref BxDolCmts
+ *
+ * @section connection Connection
+ * @ref BxDolConnection
+ *
+ * @section content_info Content Info
+ * @ref BxDolContentInfo
+ *
+ * @section cover Cover
+ * @ref BxDolCover
+ *
+ * @section editor Editor
+ * @ref BxDolEditor
+ *
+ * @section email_templates Email Templates
+ * @ref BxDolEmailTemplates
+ *
+ * @section favorites Favorites
+ * @ref BxDolFavorite
+ *
+ * @section feature Feature
+ * @ref BxDolFeature
+ *
+ * @section file_handler File View Handlers
+ * @ref BxDolFileHandler
+ *
+ * @section forms Forms
+ * @ref BxDolForm
+ * 
+ * @section grid Grid
+ * @ref BxDolGrid
+ *
+ * @section informer Informer
+ * @ref BxDolInformer
+ *
+ * @section key Keys
+ * @ref BxDolKey
+ * 
+ * @section live_updates Live Updates
+ * @ref BxDolLiveUpdates
+ * 
+ * @section menu Menu
+ * @ref BxDolMenu
+ *
+ * @section metatags Meta Tags
+ * @ref BxDolMetatags
+ * 
+ * @section page Page
+ * @ref BxDolPage
+ *
+ * @section permalinks Permalinks
+ * @ref BxDolPermalinks
+ * 
+ * @section privacy Privacy
+ * @ref BxDolPrivacy
+ *
+ * @section reports Reports
+ * @ref BxDolReport
+ * 
+ * @section rss RSS
+ * @ref BxDolRss
+ *
+ * @section search Search
+ * @ref BxDolSearch
+ *
+ * @section storage Storage
+ * @ref BxDolStorage
+ * 
+ * @section transcoder_images Transcoder: Images 
+ * @ref BxDolTranscoderImage
+ *
+ * @section transcoder_proxy Transcoder: Proxy
+ * @ref BxDolTranscoderProxy
+ *
+ * @section transcoder_videos Transcoder: Videos
+ * @ref BxDolTranscoderVideo
+ * 
+ * @section uploader Uploader
+ * @ref BxDolUploader
+ * 
+ * @section views Views
+ * @ref BxDolView
+ *
+ * @section votes Votes
+ * @ref BxDolVote
+ */
+
+/**
+ * @page public_api API Public
+ * Public API for getting secure token for @ref private_api calls and other calls which don't require user authentication
+ */
+
+/**
+ * @page private_api API Private
+ * Private API, which uses secure token for communication, token can be retrived via @ref public_api
+ */ 
+
+/**
+ * @page service Service Calls
+ * Service calls
+ */
+
+/**
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Classes instances are stored here
+ */
+$GLOBALS['bxDolClasses'] = array();
+
+/**
+ * Base class for all classes
+ */
+class BxDol
+{
+    public function __construct () {}
+}
+
+/**
+ * Singleton interface, for objects with one instance only
+ */
+interface iBxDolSingleton
+{
+    public static function getInstance();
+}
+
+/**
+ * Factory interface for object instances, once instance per object name
+ */
+interface iBxDolFactoryObject
+{
+    static public function getObjectInstance($sObject);
+}
+
+/**
+ * Replacable interface, class has an ability to replace markers somewhere
+ */
+interface iBxDolReplaceable
+{
+    public function addMarkers ($a);
+}
+
+/**
+ * Profile interface, class must implement basic profile methods to be compliant with Profile interface
+ */
+interface iBxDolProfile
+{
+    public function id();
+    public function getDisplayName();
+    public function getUrl();
+    public function getUnit();
+    public function hasImage();
+    public function getPicture();
+    public function getAvatar();
+    public function getThumb();
+    public function getIcon();
+    public function getIconModule();
+    public function getEditUrl();
+    public function isActive();
+    public function isOnline();
+
+    /**
+     * Check profile visibility
+     * @return message on error, or CHECK_ACTION_RESULT_ALLOWED when allowed
+     */    
+    public function checkAllowedProfileView();
+    
+    /**
+     * Check if posting in profile is allowed
+     * @return message on error, or CHECK_ACTION_RESULT_ALLOWED when allowed
+     */    
+    public function checkAllowedPostInProfile();
+}
+
+/**
+ * Profile services module interface, module class must implement basic profile services to be compliant with Profile interface
+ */
+interface iBxDolProfileService
+{
+    public function serviceProfileUnit ($iContentId);
+    public function serviceProfilePicture ($iContentId);
+    public function serviceProfileAvatar ($iContentId);
+    public function serviceProfileThumb ($iContentId);
+    public function serviceProfileIcon ($iContentId);
+    public function serviceProfileName ($iContentId);
+    public function serviceProfileUrl ($iContentId);
+    public function serviceProfileEditUrl ($iContentId);
+
+    /**
+     * Check profile visibility
+     * @param $iContentId content ID
+     * @return message on error, or CHECK_ACTION_RESULT_ALLOWED when allowed
+     */     
+    public function serviceCheckAllowedProfileView($iContentId);
+
+    /**
+     * Check if posting in profile is allowed, for example posting in profile's timeline
+     * @param $iContentId content ID
+     * @return message on error, or CHECK_ACTION_RESULT_ALLOWED when allowed
+     */    
+    public function serviceCheckAllowedPostInProfile($iContentId);
+    
+    public function serviceProfilesSearch ($sTerm, $iLimit);
+    public function serviceFormsHelper ();
+    public function serviceActAsProfile ();
+    public function servicePrepareFields ($aFieldsProfile);    
+}
+
+/**
+ * Content info services module interface, module class must implement basic content info services to be compliant with ContentInfo interface
+ */
+interface iBxDolContentInfoService
+{
+    public function serviceGetAuthor ($iContentId);
+    public function serviceGetDateAdded ($iContentId);
+    public function serviceGetDateChanged ($iContentId);
+    public function serviceGetLink ($iContentId);
+    public function serviceGetTitle ($iContentId);
+    public function serviceGetText ($iContentId);
+    public function serviceGetThumb ($iContentId);
+    public function serviceGetInfo ($iContentId, $bSearchableFieldsOnly = true);
+    public function serviceGetSearchResultUnit ($iContentId, $sUnitTemplate = '');
+    public function serviceGetAll ($aParams = array());
+
+    public function serviceGetSearchableFieldsExtended();
+    public function serviceGetSearchResultExtended($aParams, $iStart = 0, $iPerPage = 0);
+}
+/** @} */

+ 481 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolAccount.php

@@ -0,0 +1,481 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+bx_import('BxDolAcl');
+
+class BxDolAccount extends BxDolFactory implements iBxDolSingleton
+{
+    protected $_iAccountID;
+    protected $_aInfo;
+    protected $_oQuery;
+
+    /**
+     * Constructor
+     */
+    protected function __construct ($iAccountId)
+    {
+        $iAccountId = (int)$iAccountId;
+        $sClass = get_class($this) . '_' . $iAccountId;
+        if (isset($GLOBALS['bxDolClasses'][$sClass]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+
+        $this->_iAccountID = $iAccountId; // since constructor is protected $iAccountId is always valid
+        $this->_oQuery = BxDolAccountQuery::getInstance();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        $sClass = get_class($this) . '_' . $this->_iProfileID;
+        if (isset($GLOBALS['bxDolClasses'][$sClass]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance($mixedAccountId = false)
+    {
+        if (!$mixedAccountId)
+            $mixedAccountId = getLoggedId();
+
+        $iAccountId = self::getID($mixedAccountId);
+        if (!$iAccountId)
+            return false;
+
+        $sClass = __CLASS__ . '_' . $iAccountId;
+        if(!isset($GLOBALS['bxDolClasses'][$sClass]))
+            $GLOBALS['bxDolClasses'][$sClass] = new BxDolAccount($iAccountId);
+
+        return $GLOBALS['bxDolClasses'][$sClass];
+    }
+
+    /**
+     * Get studio operator account singleton instance on the class
+     */
+    public static function getInstanceStudioOperator()
+    {
+        $oQuery = BxDolAccountQuery::getInstance();
+        if (!($iId = $oQuery->getStudioOperatorId()))
+            return false;
+
+        return self::getInstance($iId);
+    }
+
+    /**
+     * Get account id
+     */
+    public function id()
+    {
+        $a = $this->getInfo($this->_iAccountID);
+        return isset($a['id']) ? $a['id'] : false;
+    }
+
+    /**
+     * Check if account is confirmed, it is checked by email confirmation
+     */
+    public function isConfirmed($iAccountId = false)
+    {
+        if (!getParam('sys_email_confirmation')) // if email_confirmation procedure is disabled, always return true
+            return true;
+        $a = $this->getInfo((int)$iAccountId);
+        return $a['email_confirmed'] ? true : false;
+    }
+
+    /**
+     * Set account email to confirmed or unconfirmed
+     * @param  int  $isConfirmed - false: mark email as unconfirmed, true: as confirmed
+     * @param  int  $iAccountId  - optional account id
+     * @return true on success or false on error
+     */
+    public function updateEmailConfirmed($isConfirmed, $isAutoSendConfrmationEmail = true, $iAccountId = false)
+    {
+        $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+
+        if (!$isConfirmed && $isAutoSendConfrmationEmail && getParam('sys_email_confirmation')) // if email_confirmation procedure is enabled - send email confirmation letter
+            $this->sendConfirmationEmail($iId);
+
+        if ($this->_oQuery->updateEmailConfirmed($isConfirmed, $iId)) {
+            $this->_aInfo = false;
+            bx_alert('account', $isConfirmed ? 'confirm' : 'unconfirm', $iId);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Switch context automatically to the first available profile
+     * @param $iProfileIdFilterOut profile ID to exclude from the list of possible profiles
+     * @param $iAccountId account ID to use istead of current account
+     * @return true on success or false on error
+     */ 
+    public function updateProfileContextAuto($iProfileIdFilterOut = false, $iAccountId = false)
+    {
+        $oAccount = (!$iAccountId || $iAccountId == $this->_iAccountID ? $this : BxDolAccount::getInstance ($iAccountId));
+        if (!$oAccount)
+            return false;
+        $aAccountInfo = $oAccount->getInfo();
+        $aProfiles = $oAccount->getProfiles();
+        $oProfileAccount = BxDolProfile::getInstanceAccountProfile($oAccount->id());
+
+        // unset deleted profile and account profile
+        if ($iProfileIdFilterOut)
+            unset($aProfiles[$iProfileIdFilterOut]);
+        unset($aProfiles[$oProfileAccount->id()]);
+
+        if ($aProfiles) {
+            // try to use another profile
+            reset($aProfiles);
+            $iProfileId = key($aProfiles);
+        } 
+        else {
+            // if no profiles exist, use account profile
+            $iProfileId = $oProfileAccount->id();
+        }
+
+        return $oAccount->updateProfileContext($iProfileId);
+    }
+    
+    public function updateProfileContext($iSwitchToProfileId, $iAccountId = false)
+    {
+        $iId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        $aInfo = $this->getInfo((int)$iId);
+        if (!$aInfo)
+            return false;
+
+        $ret = null;
+        bx_alert('account', 'before_switch_context', $iId, $iSwitchToProfileId, array('profile_id_current' => $aInfo['profile_id'], 'override_result' => &$ret));
+        if ($ret !== null)
+            return $ret;
+
+        if (!$this->_oQuery->updateCurrentProfile($iId, $iSwitchToProfileId))
+            return false;
+
+        $this->_aInfo = false;
+            
+        bx_alert('account', 'switch_context', $iId, $iSwitchToProfileId, array('profile_id_old' => $aInfo['profile_id']));
+
+        return true;
+    }
+
+    /**
+     * Send "confirmation" email
+     */
+    public function sendConfirmationEmail($iAccountId = false)
+    {
+    	$sName = $this->getDisplayName($iAccountId); 
+        $sEmail = $this->getEmail($iAccountId);
+
+        $oKey = BxDolKey::getInstance();
+        $sConfirmationCode = $oKey->getNewKey(array('account_id' => $iAccountId));
+        $sConfirmationLink = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=confirm-email') . '&code=' . urlencode($sConfirmationCode);
+
+        $aPlus = array(
+        	'name' => $sName,
+        	'email' => $sEmail,
+        	'conf_code' => $sConfirmationCode,
+        	'conf_link' => $sConfirmationLink,
+        	'conf_form_link' => BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=confirm-email')
+        );
+
+        $aTemplate = BxDolEmailTemplates::getInstance()->parseTemplate('t_Confirmation', $aPlus);
+        return $aTemplate && sendMail($sEmail, $aTemplate['Subject'], $aTemplate['Body'], 0, array(), BX_EMAIL_SYSTEM);
+    }
+
+    /**
+     * Get account info
+     */
+    public function getInfo($iAccountId = false)
+    {        
+        if ($iAccountId && $iAccountId != $this->_iAccountID)
+            return $this->_oQuery->getInfoById((int)$iAccountId ? (int)$iAccountId : $this->_iAccountID);
+
+        if ($this->_aInfo)
+            return $this->_aInfo;
+
+        $this->_aInfo = $this->_oQuery->getInfoById($this->_iAccountID);
+        return $this->_aInfo;
+    }
+
+    /**
+     * Get account display name
+     */
+    public function getDisplayName($iAccountId = false)
+    {
+        $aInfo = $this->getInfo($iAccountId);
+        return bx_process_output(!empty($aInfo['name']) ? $aInfo['name'] : _t('_sys_txt_user_n', $aInfo['id']));
+    }
+
+    /**
+     * Get account url
+     */
+    public function getUrl($iAccountId = false)
+    {
+        return 'javascript:void(0);';
+    }
+
+    /**
+     * Get account url
+     */
+    public function getUnit($iAccountId = false)
+    {
+        $sTitle = $this->getDisplayName($iAccountId);
+        return BxDolTemplate::getInstance()->parseHtmlByName('unit_account.html', array(
+            'color' => implode(', ', BxDolTemplate::getColorCode(($iAccountId ? $iAccountId : $this->_iAccountID), 0.5)),
+			'letter' => mb_strtoupper(mb_substr($sTitle, 0, 1))
+        ));
+    }
+
+    /**
+     * Get picture url
+     */
+    public function getPicture($iAccountId = false)
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account-picture.png');
+    }
+
+    /**
+     * Get avatar url
+     */
+    public function getAvatar($iAccountId = false)
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account-avatar.png');
+    }
+
+    /**
+     * Get thumb picture url
+     */
+    public function getThumb($iAccountId = false)
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account-thumb.png');
+    }
+
+    /**
+     * Get icon picture url
+     */
+    public function getIcon($iAccountId = false)
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account-icon.png');
+    }
+
+    /**
+     * Get account email
+     */
+    public function getEmail($iAccountId = false)
+    {
+        $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        $aAccountInfo = $this->getInfo($iAccountId);
+        return $aAccountInfo['email'];
+    }
+
+    /**
+     * Is account online
+     */
+	public function isOnline($iAccountId = false)
+    {
+        $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        return $this->_oQuery->isOnline($iAccountId);
+    }
+
+    /**
+     * Validate account.
+     * @param $s - account identifier (id or email)
+     * @return account id or false if account was not found
+     */
+    static public function getID($s)
+    {
+        $oQuery = BxDolAccountQuery::getInstance();
+
+        bx_import('BxDolForm');
+        if (BxDolFormCheckerHelper::checkEmail($s)) {
+            $iId = (int)$oQuery->getIdByEmail($s);
+            return $iId ? $iId : false;
+        }
+
+        $iId = $oQuery->getIdById((int)$s);
+        return $iId ? $iId : false;
+    }
+
+    /**
+     * Check if profiles limit reached
+     */
+    public function isProfilesLimitReached ()
+    {
+        $iProfilesLimit = (int)getParam('sys_account_limit_profiles_number');
+        if ($iProfilesLimit && ($iProfilesNum = $this->getProfilesNumber()) && $iProfilesNum >= $iProfilesLimit)
+            return true;
+
+        return false;
+    }
+
+    /**
+     * Get number of profiles associated with the account
+     */
+    public function getProfilesNumber ($isFilterNonSwitchableProfiles = true)
+    {
+        $a = $this->getProfilesIds($isFilterNonSwitchableProfiles);
+        return count($a);
+    }
+    
+    /**
+     * Get all profile ids associated with the account
+     */
+    public function getProfilesIds ($isFilterNonSwitchableProfiles = true)
+    {
+        $a = $this->getProfiles ($isFilterNonSwitchableProfiles);
+        return $a ? array_keys($a) : array();
+    }
+
+    /**
+     * Get all profiles associated with the account
+     */
+    public function getProfiles ($isFilterNonSwitchableProfiles = true)
+    {
+        $oProfileQuery = BxDolProfileQuery::getInstance();
+        $aProfiles = $oProfileQuery->getProfilesByAccount($this->_iAccountID);
+
+        if ($isFilterNonSwitchableProfiles) {
+            foreach ($aProfiles as $iProfileId => $aProfile)
+                if (!BxDolService::call($aProfile['type'], 'act_as_profile'))
+                    unset($aProfiles[$iProfileId]);
+        }
+
+        return $aProfiles;
+    }
+
+    /**
+     * Delete profile.
+     * @param $bDeleteWithContent - delete associated profiles with all its contents
+     */
+    function delete($bDeleteWithContent = false)
+    {
+        $aAccountInfo = $this->_oQuery->getInfoById($this->_iAccountID);
+        if (!$aAccountInfo)
+            return false;
+
+        // create system event before deletion
+        $isStopDeletion = false;
+        bx_alert('account', 'before_delete', $this->_iAccountID, 0, array('delete_with_content' => $bDeleteWithContent, 'stop_deletion' => &$isStopDeletion));
+        if ($isStopDeletion)
+            return false;
+
+        $oAccountQuery = BxDolAccountQuery::getInstance();
+
+        $oProfileQuery = BxDolProfileQuery::getInstance();
+        $aProfiles = $oProfileQuery->getProfilesByAccount($this->_iAccountID);
+        foreach ($aProfiles as $iProfileId => $aRow) {
+            $oProfile = BxDolProfile::getInstance($iProfileId);
+            if (!$oProfile)
+                continue;
+            $oProfile->delete(false, $bDeleteWithContent, true);
+        }
+
+        // delete profile
+        if (!$oAccountQuery->delete($this->_iAccountID))
+            return false;
+
+        // unset class instance to prevent creating the instance again
+        $sClass = __CLASS__ . '_' . $this->_iAccountID;
+        unset($GLOBALS['bxDolClasses'][$sClass]);
+
+       // create system event
+        bx_alert('account', 'delete', $this->_iAccountID, 0, array ('delete_with_content' => $bDeleteWithContent));
+        
+        return true;
+    }
+
+    /**
+     * Add permament messages.
+     */
+    public function addInformerPermanentMessages ($oInformer)
+    {
+        if (!$this->isConfirmed()) {
+            $sUrl = BxDolPermalinks::getInstance()->permalink('page.php?i=confirm-email') . '&resend=1';
+            $aAccountInfo = $this->getInfo();
+            $oInformer->add('sys-account-unconfirmed', _t('_sys_txt_account_unconfirmed', $sUrl, $aAccountInfo['email']), BX_INFORMER_ALERT);
+        }
+    }
+
+    /**
+     * Get unsubscribe link for the specified mesage type
+     */
+    public function getUnsubscribeLink($iEmailType, $iAccountId = false)
+    {
+        $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        $sUrl = '';
+        switch ($iEmailType) {
+            case BX_EMAIL_NOTIFY:
+                $sUrl = 'page.php?i=unsubscribe-notifications';
+                break;
+            case BX_EMAIL_MASS:
+                $sUrl = 'page.php?i=unsubscribe-news';
+                break;
+            default:
+                return '';
+        }
+        return BxDolPermalinks::getInstance()->permalink($sUrl) . '&id=' . $iAccountId . '&code=' . $this->getEmailHash();
+    }
+
+    public function getEmailHash($iAccountId = false)
+    {
+        $iAccountId = (int)$iAccountId ? (int)$iAccountId : $this->_iAccountID;
+        $a = $this->getInfo();
+        return md5($a['email'] . $a['salt'] . BX_DOL_SECRET);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    static public function isAllowedCreate ($iProfileId, $isPerformAction = false)
+    {
+        $aCheck = checkActionModule($iProfileId, 'create account', 'system', $isPerformAction);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return MsgBox($aCheck[CHECK_ACTION_MESSAGE]);
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    static public function isAllowedEdit ($iProfileId, $aContentInfo, $isPerformAction = false)
+    {
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if (!$oProfile)
+            return _t('_sys_txt_access_denied');
+
+        $aProfileInfo = $oProfile->getInfo();
+        if (!$aProfileInfo || getLoggedId() != $aProfileInfo['account_id'])
+            return _t('_sys_txt_access_denied');
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    static public function isAllowedDelete ($iProfileId, $aContentInfo, $isPerformAction = false)
+    {
+        $iAccountId = (int)BxDolProfile::getInstance($iProfileId)->getAccountId();
+        if(isAdmin($iAccountId) && $iAccountId == (int)$aContentInfo['id'])
+            return _t('_sys_txt_access_denied');
+
+        $aCheck = checkActionModule($iProfileId, 'delete account', 'system', $isPerformAction);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+}
+
+/** @} */

+ 1438 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolCmts.php

@@ -0,0 +1,1438 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+bx_import('BxDolAcl');
+
+define('BX_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_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);
+
+/**
+ * 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
+ * - Nl2br - convert new lines to <br /> on saving
+ * - 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->getCommentsFirst ();
+ * @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
+{
+    protected $_oQuery = null;
+    protected $_oTemplate = null;
+
+    protected $_sFormObject;
+    protected $_sFormDisplayPost;
+    protected $_sFormDisplayEdit;
+
+    protected $_sConnObjFriends;
+    protected $_sConnObjSubscriptions;
+
+    protected $_sMenuObjManage;
+    protected $_sMenuObjActions;
+
+    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 $_bHtml = false; ///< whether HTML formating is used during comment posting or not. Read Only!
+
+    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->_aSystems = $this->getSystems();
+        if(!isset($this->_aSystems[$sSystem]))
+            return;
+
+        $this->_sSystem = $sSystem;
+        $this->_aSystem = $this->_aSystems[$sSystem];
+
+        $this->_aSystem['table_images'] = 'sys_cmts_images';
+        $this->_aSystem['table_images2entries'] = 'sys_cmts_images2entries';
+
+        $this->_aSystem['table_ids'] = 'sys_cmts_ids';
+
+        $this->_aSystem['is_browse_filter'] = (int)$this->_bBrowseFilter;
+
+        $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 = BX_DOL_URL_ROOT . 'cmts.php';
+        $this->_sBaseUrl = BxDolPermalinks::getInstance()->permalink($this->_aSystem['base_url']);
+        if(get_mb_substr($this->_sBaseUrl, 0, 4) != 'http')
+            $this->_sBaseUrl = BX_DOL_URL_ROOT . $this->_sBaseUrl;
+        $this->_sListAnchor = "cmts-anchor-%s-%d";
+        $this->_sItemAnchor = "cmt-anchor-%s-%d-%d";
+
+        $this->_oQuery = new BxDolCmtsQuery($this);
+
+        $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->_sMetatagsObj = 'sys_cmts';
+
+        $this->_aT = array(
+        	'block_comments_title' => '_cmt_block_comments_title'
+        );
+
+        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);
+    }
+
+    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`,
+                    `Nl2br` AS `nl2br`,
+                    `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`,
+                    `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 `sys_objects_cmts`', 'name');
+        }
+        return $GLOBALS[$sKey];
+    }
+
+    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 getStorageObjectName()
+    {
+    	return $this->_getFormObject()->getStorageObjectName();
+    }
+
+	public function getTranscoderPreviewName()
+    {
+    	return $this->_getFormObject()->getTranscoderPreviewName();
+    }
+
+    public function getSystemInfo()
+    {
+        return $this->_aSystem;
+    }
+
+    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 getViewUrl($iCmtId)
+    {
+    	if(empty($this->_aSystem['trigger_field_title']))
+    		return '';
+
+    	return bx_append_url_params($this->_sViewUrl, array(
+			'sys' => $this->_sSystem,
+			'id' => $this->_iId,
+			'cmt_id' => $iCmtId
+		));
+    }
+
+    public function getBaseUrl()
+    {
+        return $this->_replaceMarkers($this->_sBaseUrl);
+    }
+
+    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 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($iId)
+    {
+        if(empty($this->_aSystem['object_vote']))
+        	$this->_aSystem['object_vote'] = 'sys_cmts';
+
+        $oVote = BxDolVote::getObjectInstance($this->_aSystem['object_vote'], $iId, true, $this->_oTemplate);
+        if(!$oVote || !$oVote->isEnabled())
+            return false;
+
+        return $oVote;
+    }
+
+	public function getNotificationId()
+	{
+		return 'cmts-notification-' . $this->_sSystem . '-' . $this->_iId;
+	}
+
+	public function setTableNameFiles($sTable)
+    {
+    	$this->_aSystem['table_images'] = $sTable;
+    	$this->_oQuery->setTableNameFiles($sTable);
+    }
+
+	public function setTableNameFiles2Entries($sTable)
+    {
+    	$this->_aSystem['table_images2entries'] = $sTable;
+    	$this->_oQuery->setTableNameFiles2Entries($sTable);
+    }
+
+    public function isNl2br ()
+    {
+        return $this->_aSystem['nl2br'];
+    }
+
+    public function isRatable ()
+    {
+        return $this->_aSystem['is_ratable'];
+    }
+
+    public function isAttachImageEnabled()
+    {
+        return true;
+    }
+
+    /**
+     * 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 '';
+
+        return $this->_oQuery->getObjectTitle ($iObjectId ? $iObjectId : $this->getId());
+    }
+
+    public function getCommentsCountAll ($iObjectId = 0)
+    {
+        return $this->_oQuery->getCommentsCountAll ($iObjectId ? $iObjectId : $this->getId());
+    }
+
+    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 getCommentRow ($iCmtId)
+    {
+        return $this->_oQuery->getComment ($this->getId(), $iCmtId);
+    }
+
+    public function onObjectDelete ($iObjectId = 0)
+    {
+        // delete comments
+        $aFiles = $aCmtIds = array();
+        $this->_oQuery->deleteObjectComments ($iObjectId ? $iObjectId : $this->getId(), $aFiles, $aCmtIds);
+
+        // delete meta info
+        $this->deleteMetaInfo($aCmtIds);
+
+        // delete files
+        if ($aFiles) {
+            $oStorage = BxDolStorage::getObjectInstance($this->getStorageObjectName());
+            if ($oStorage)
+                $oStorage->queueFilesForDeletion($aFiles);
+        }
+    }
+
+    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 meta info
+            $o->deleteMetaInfo($aCmtIds);
+    
+            // delete files
+            $oStorage = BxDolStorage::getObjectInstance($o->getStorageObjectName());
+            if ($oStorage)
+                $oStorage->queueFilesForDeletion($aFiles);
+        }
+        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 meta info
+            $o->deleteMetaInfo($aCmtIds);
+
+            // delete files
+            $oStorage = BxDolStorage::getObjectInstance($o->getStorageObjectName());
+            if ($oStorage && $aFiles)
+                $oStorage->queueFilesForDeletion($aFiles);
+
+            if (null !== $iFiles)
+                $iFiles += count($aFiles);
+        }
+
+        return true;
+    }
+
+    public function deleteMetaInfo ($mixedCmtId)
+    {
+        if (!$this->_sMetatagsObj)
+            return;
+
+        if (!is_array($mixedCmtId))
+            $mixedCmtId = array($mixedCmtId);
+
+        $oMetatags = BxDolMetatags::getObjectInstance($this->_sMetatagsObj);
+
+        foreach ($mixedCmtId as $iCmtId) {
+            $oMetatags->onDeleteContent($this->_oQuery->getUniqId($this->_aSystem['system_id'], $iCmtId));
+            $this->_oQuery->deleteCmtIds($this->_aSystem['system_id'], $iCmtId);
+        }
+    }
+
+    /**
+     * Permissions functions
+     */
+    public function checkAction ($sAction, $isPerformAction = false)
+    {
+        $iId = $this->_getAuthorId();
+        $a = checkActionModule($iId, $sAction, 'system', $isPerformAction);
+        return $a[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    public function checkActionErrorMsg ($sAction)
+    {
+        $iId = $this->_getAuthorId();
+        $a = checkActionModule($iId, $sAction, 'system');
+        return $a[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED ? $a[CHECK_ACTION_MESSAGE] : '';
+    }
+
+    public function isViewAllowed ($isPerformAction = false)
+    {
+        $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 isPostReplyAllowed ($isPerformAction = false)
+    {
+        $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 msgErrPostReplyAllowed ()
+    {
+        return $this->checkActionErrorMsg('comments post');
+    }
+
+    public function isEditAllowed ($aCmt, $isPerformAction = false)
+    {
+        if(isAdmin())
+            return true;
+
+        if($this->checkAction('comments edit all', $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 $aCmt['cmt_author_id'] == $this->_getAuthorId() && $this->checkAction ('comments edit own', $isPerformAction);
+    }
+
+    public function msgErrEditAllowed ()
+    {
+        return $this->checkActionErrorMsg ('comments edit own');
+    }
+
+    public function isRemoveAllowed ($aCmt, $isPerformAction = false)
+    {
+        if(isAdmin())
+            return true;
+
+        if($aCmt['cmt_author_id'] == $this->_getAuthorId() && $this->checkAction ('comments remove own', $isPerformAction))
+            return true;
+
+        return $this->checkAction ('comments remove all', $isPerformAction);
+    }
+
+    public function msgErrRemoveAllowed ()
+    {
+        return $this->checkActionErrorMsg('comments remove own');
+    }
+
+    /**
+     * Actions functions
+     */
+    public function actionGetFormPost ()
+    {
+        if (!$this->isEnabled())
+            return '';
+
+        $iCmtParentId= isset($_REQUEST['CmtParent']) ? bx_process_input($_REQUEST['CmtParent'], BX_DATA_INT) : 0;
+        $sCmtBrowse = isset($_REQUEST['CmtBrowse']) ? bx_process_input($_REQUEST['CmtBrowse'], BX_DATA_TEXT) : '';
+        $sCmtDisplay = isset($_REQUEST['CmtDisplay']) ? bx_process_input($_REQUEST['CmtDisplay'], BX_DATA_TEXT) : '';
+
+        return $this->getFormBoxPost(array('parent_id' => $iCmtParentId, 'type' => $sCmtBrowse), array('type' => $sCmtDisplay, 'dynamic_mode' => true));
+    }
+
+    public function actionGetFormEdit ()
+    {
+        if (!$this->isEnabled()){
+            echoJson(array());
+            return;
+        }
+
+        $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(array(
+            'parent_id' => $aCmt['cmt_parent_id'],
+            'vparent_id' => $aCmt['cmt_vparent_id'],
+            'content' => $this->getComment($aCmt, array('type' => $sCmtBrowse), array('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;
+        return $this->getComments($aBp, $aDp);
+    }
+
+    public function actionSubmitPostForm()
+    {
+        if(!$this->isEnabled() || !$this->isPostReplyAllowed()) {
+            echoJson(array());
+            return;
+        }
+
+        $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()) {
+            echoJson(array());
+            return;
+        };
+
+        $iObjId = $this->getId();
+        $iObjAthrId = $this->getObjectAuthorId($iObjId);
+
+        $iCmtId = 0;
+        if(bx_get('Cmt') !== false)
+            $iCmtId = bx_process_input(bx_get('Cmt'), BX_DATA_INT);
+
+        $aCmt = $this->_oQuery->getCommentSimple ($iObjId, $iCmtId);
+        if(!$aCmt) {
+            echoJson(array('msg' => _t('_No such comment')));
+            return;
+        }
+
+        if ($aCmt['cmt_replies'] > 0) {
+            echoJson(array('msg' => _t('_Can not delete comments with replies')));
+            return;
+        }
+
+        $iPerformerId = $this->_getAuthorId();
+        if(!$this->isRemoveAllowed($aCmt)) {
+            echoJson(array('msg' => $aCmt['cmt_author_id'] == $iPerformerId ? strip_tags($this->msgErrRemoveAllowed()) : _t('_Access denied')));
+            return;
+        }
+
+        if($this->_oQuery->removeComment($iObjId, $iCmtId, $aCmt['cmt_parent_id'])) {
+            $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(true);
+
+            $this->deleteMetaInfo ($iCmtId);
+
+            bx_alert($this->_sSystem, 'commentRemoved', $iObjId, $iPerformerId, array(
+                'object_author_id' => $iObjAthrId,
+
+                'comment_id' => $iCmtId, 
+                'comment_author_id' => $aCmt['cmt_author_id']
+            ));
+
+            bx_alert('comment', 'deleted', $iCmtId, $iPerformerId, array(
+                'object_system' => $this->_sSystem, 
+                'object_id' => $iObjId, 
+                'object_author_id' => $iObjAthrId,
+                
+                'comment_author_id' => $aCmt['cmt_author_id']
+            ));
+
+            if(!empty($aCmt['cmt_parent_id'])) {
+                $iCmtPrntId = (int)$aCmt['cmt_parent_id'];
+                $aCmtPrnt = $this->_oQuery->getCommentSimple($iObjId, $iCmtPrntId);
+                if(!empty($aCmtPrnt) && is_array($aCmtPrnt)) {
+                    bx_alert($this->_sSystem, 'replyRemoved', $iCmtPrntId, $iPerformerId, array(
+                        'object_id' => $iObjId, 
+                        'object_author_id' => $iObjAthrId,
+                        
+                        'parent_author_id' => $aCmtPrnt['cmt_author_id'],
+                        
+                        'comment_id' => $iCmtId, 
+                        'comment_author_id' => $aCmt['cmt_author_id'],
+                    ));
+
+                    bx_alert('reply', 'deleted', $iCmtId, $iPerformerId, array(
+                        'object_system' => $this->_sSystem, 
+                        'object_id' => $iObjId, 
+                        'object_author_id' => $iObjAthrId,
+
+                        'parent_id' => $iCmtPrntId,
+                        'parent_author_id' => $aCmtPrnt['cmt_author_id'],
+
+                        'comment_author_id' => $aCmt['cmt_author_id'], 
+                    ));
+                }
+            }
+
+            echoJson(array('id' => $iCmtId));
+            return;
+        }
+
+        echoJson(array('msg' => _t('_cmt_err_cannot_perform_action')));
+    }
+
+    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 onPostAfter($iCmtId)
+    {
+        $iObjId = (int)$this->getId();
+        $iObjAthrId = $this->getObjectAuthorId($iObjId);
+
+        $aCmt = $this->_oQuery->getCommentSimple($iObjId, $iCmtId);
+        if(empty($aCmt) || !is_array($aCmt))
+            return false;
+
+        $iPerformerId = (int)$aCmt['cmt_author_id'];
+        bx_alert($this->_sSystem, 'commentPost', $iObjId, $iPerformerId, array(
+			'object_author_id' => $iObjAthrId,
+
+        	'comment_id' => $iCmtId, 
+        	'comment_author_id' => $aCmt['cmt_author_id'], 
+        	'comment_text' => $aCmt['cmt_text']
+        ));
+
+        bx_alert('comment', 'added', $iCmtId, $iPerformerId, array(
+        	'object_system' => $this->_sSystem, 
+        	'object_id' => $iObjId, 
+        	'object_author_id' => $iObjAthrId,
+
+        	'comment_author_id' => $aCmt['cmt_author_id'], 
+        	'comment_text' => $aCmt['cmt_text']
+        ));
+
+        if(!empty($aCmt['cmt_parent_id'])) {
+            $iCmtPrntId = (int)$aCmt['cmt_parent_id'];
+            $aCmtPrnt = $this->_oQuery->getCommentSimple($iObjId, $iCmtPrntId);
+            if(!empty($aCmtPrnt) && is_array($aCmtPrnt)) {
+                bx_alert($this->_sSystem, 'replyPost', $iCmtPrntId, $iPerformerId, array(
+                    'object_id' => $iObjId, 
+                    'object_author_id' => $iObjAthrId,
+
+                	'parent_author_id' => $aCmtPrnt['cmt_author_id'],
+
+                	'comment_id' => $iCmtId,
+                	'comment_author_id' => $aCmt['cmt_author_id'], 
+                	'comment_text' => $aCmt['cmt_text']
+                ));
+
+                bx_alert('comment', 'replied', $iCmtId, $iPerformerId, array(
+                	'object_system' => $this->_sSystem, 
+                	'object_id' => $iObjId, 
+                	'object_author_id' => $iObjAthrId,
+ 
+                	'parent_id' => $iCmtPrntId,
+                	'parent_author_id' => $aCmtPrnt['cmt_author_id'],
+
+                	'comment_author_id' => $aCmt['cmt_author_id'],  
+                	'comment_text' => $aCmt['cmt_text']
+                ));
+            }
+        }
+
+        return true;
+    }
+
+    public function onEditAfter($iCmtId)
+    {
+        $iObjId = (int)$this->getId();
+    	$iObjAthrId = $this->getObjectAuthorId($iObjId);
+
+    	$aCmt = $this->_oQuery->getCommentSimple($iObjId, $iCmtId);
+        if(empty($aCmt) || !is_array($aCmt))
+            return false;
+
+        $iPerformerId = $this->_getAuthorId();
+        bx_alert($this->_sSystem, 'commentUpdated', $iObjId, $iPerformerId, array(
+            'object_author_id' => $iObjAthrId,
+
+            'comment_id' => $iCmtId, 
+            'comment_author_id' => $aCmt['cmt_author_id'], 
+            'comment_text' => $aCmt['cmt_text']        	
+        ));
+
+        bx_alert('comment', 'edited', $iCmtId, $iPerformerId, array(
+            'object_system' => $this->_sSystem, 
+            'object_id' => $iObjId, 
+            'object_author_id' => $iObjAthrId,
+
+            'comment_author_id' => $aCmt['cmt_author_id'],
+            'comment_text' => $aCmt['cmt_text']
+        ));
+
+        return true;
+    }
+
+    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()
+    {
+        $oForm = BxDolForm::getObjectInstance('sys_comment', 'sys_comment_post', $this->_oTemplate);
+        if(!$oForm)
+            return array();
+
+        $aSrchNamesExcept = array();
+        $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' => $aSrchCaptions['cmt_author_id'],
+            	'values' => '',
+        		'pass' => ''
+            )
+        );
+
+        foreach ($oForm->aInputs as $aInput)
+            if (in_array($aInput['type'], BxDolSearchExtended::$SEARCHABLE_TYPES) && !in_array($aInput['name'], $aSrchNamesExcept))
+                $aResult[$aInput['name']] = array(
+                	'type' => $aInput['type'], 
+                	'caption' => !empty($aInput['caption_src']) ? $aInput['caption_src'] : (!empty($aSrchCaptions[$aInput['name']]) ? $aSrchCaptions[$aInput['name']] : ''),
+                    '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)
+    {
+        if(empty($aParams) || !is_array($aParams))
+            return array();
+
+        return $this->_oQuery->getCommentsBy(array('type' => 'search_ids', 'search_params' => $aParams, 'start' => $iStart, 'per_page' => $iPerPage));
+    }
+
+    /**
+     * 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)
+    {
+        $oProfile = $this->_getAuthorObject($iAuthorId);
+
+        return array(
+            $oProfile->getDisplayName(),
+            $oProfile->getUrl(),
+            $oProfile->getThumb(),
+            $oProfile->getUnit(0, array('template' => 'unit_wo_info'))
+        );
+    }
+
+    protected function _getAuthorObject($iAuthorId = 0)
+    {
+        if(empty($iAuthorId))
+            return BxDolProfileUndefined::getInstance();
+
+        $oProfile = BxDolProfile::getInstance($iAuthorId);
+        if (!$oProfile)
+            $oProfile = BxDolProfileUndefined::getInstance();
+
+        return $oProfile;
+    }
+
+	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);
+
+        //--- 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);
+
+        $aDp['blink'] = isset($aDp['blink']) ? $aDp['blink'] : '';
+        if(bx_get('CmtBlink') !== false) 
+            $aDp['blink'] = bx_process_input($_REQUEST['CmtBlink'], BX_DATA_TEXT);
+
+        $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 _prepareTextForEdit ($s)
+    {
+        if ($this->isNl2br())
+            return htmlspecialchars_decode(str_replace('<br />', "", $s));
+
+        return $s;
+    }
+
+    protected function _prepareTextForSave ($s)
+    {
+        $iDataAction = $this->isNl2br() ? BX_DATA_TEXT_MULTILINE : BX_DATA_HTML;
+        return bx_process_input($s, $iDataAction);
+    }
+
+    protected function _prepareTextForOutput ($s, $iCmtId = 0)
+    {
+    	$iDataAction = $this->isNl2br() ? BX_DATA_TEXT_MULTILINE : BX_DATA_HTML;
+    	$s = bx_process_output($s, $iDataAction);
+    	$s = bx_linkify_html($s, 'class="' . BX_DOL_LINK_CLASS . '"');
+
+        if ($this->_sMetatagsObj && $iCmtId) {
+            $oMetatags = BxDolMetatags::getObjectInstance($this->_sMetatagsObj);
+            $s = $oMetatags->metaParse($this->_oQuery->getUniqId($this->_aSystem['system_id'], $iCmtId), $s);
+        }
+
+        return $s;
+    }
+
+    protected function _prepareParams(&$aBp, &$aDp)
+    {
+        $aBp['type'] = isset($aBp['type']) && !empty($aBp['type']) ? $aBp['type'] : $this->_sBrowseType;
+        $aBp['filter'] = isset($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['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'];
+
+        $aDp['type'] = isset($aDp['type']) && !empty($aDp['type']) ? $aDp['type'] : $this->_sDisplayType;
+        $aDp['blink'] = isset($aDp['blink']) && !empty($aDp['blink']) ? $aDp['blink'] : array();
+        if(!is_array($aDp['blink']))
+        	$aDp['blink'] = explode(',', $aDp['blink']);
+
+        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:
+                $aBp['per_view'] = $aBp['per_view'] != -1 ? $aBp['per_view'] : $this->getPerView($aBp['vparent_id']);
+                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;
+        }
+
+        $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) {
+            $aBp['start'] = $aBp['count'] - $aBp['per_view'];
+            if($aBp['start'] < 0) {
+                $aBp['per_view'] += $aBp['start'];
+                $aBp['start'] = 0;
+            }
+        }
+
+        $this->_setUserChoice($aDp['type'], $aBp['type'], $aBp['filter']);
+    }
+
+    protected function _triggerComment()
+    {
+        if(!$this->_aSystem['trigger_table'])
+            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']);
+    }
+}
+
+/** @} */

+ 519 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolCmtsQuery.php

@@ -0,0 +1,519 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * @see BxDolCmts
+ */
+class BxDolCmtsQuery extends BxDolDb
+{
+    protected $_oMain;
+
+    protected $_sTable;
+    protected $_sTriggerTable;
+    protected $_sTriggerFieldId;
+    protected $_sTriggerFieldAuthor;
+    protected $_sTriggerFieldTitle;
+    protected $_sTriggerFieldComments;
+
+    protected $_sTableFiles;
+    protected $_sTableFiles2Entries;
+
+    protected $_sTableIds;
+
+    public function __construct(&$oMain)
+    {
+        $this->_oMain = $oMain;
+
+        $aSystem = $this->_oMain->getSystemInfo();
+        $this->_sTable = $aSystem['table'];
+        $this->_sTriggerTable = $aSystem['trigger_table'];
+        $this->_sTriggerFieldId = $aSystem['trigger_field_id'];
+        $this->_sTriggerFieldAuthor = $aSystem['trigger_field_author'];
+        $this->_sTriggerFieldTitle = $aSystem['trigger_field_title'];
+        $this->_sTriggerFieldComments = $aSystem['trigger_field_comments'];
+
+        $this->_sTableFiles = $aSystem['table_images'];
+        $this->_sTableFiles2Entries = $aSystem['table_images2entries'];
+
+        $this->_sTableIds = $aSystem['table_ids'];
+
+        parent::__construct();
+    }
+
+    function getTableName ()
+    {
+        return $this->_sTable;
+    }
+
+    function setTableNameFiles($sTable)
+    {
+    	$this->_sTableFiles = $sTable;
+    }
+
+	function setTableNameFiles2Entries($sTable)
+    {
+    	$this->_sTableFiles2Entries = $sTable;
+    }
+
+    function getCommentsCountAll ($iId)
+    {
+        $sQuery = $this->prepare("SELECT `{$this->_sTriggerFieldComments}` FROM `{$this->_sTriggerTable}` WHERE `{$this->_sTriggerFieldId}` = ?", $iId);
+        return (int)$this->getOne($sQuery);
+    }
+
+    function getCommentsCount ($iId, $iCmtVParentId = -1, $iAuthorId = 0, $sFilter = '')
+    {
+    	$aBindings = array(
+    		'cmt_object_id' => $iId
+    	);
+
+        $sWhereClause = '';
+        if((int)$iCmtVParentId >= 0) {
+        	$aBindings['cmt_vparent_id'] = $iCmtVParentId;
+
+            $sWhereClause .= " AND `{$this->_sTable}`.`cmt_vparent_id` = :cmt_vparent_id";
+        }
+
+        $sJoinClause = '';
+        switch($sFilter) {
+        	case BX_CMT_FILTER_FRIENDS:
+        	case BX_CMT_FILTER_SUBSCRIPTIONS:
+	            $oConnection = BxDolConnection::getObjectInstance($this->_oMain->getConnectionObject($sFilter));
+	
+	            $aQueryParts = $oConnection->getConnectedContentAsSQLParts($this->_sTable, 'cmt_author_id', $iAuthorId);
+	            $sJoinClause .= ' ' . $aQueryParts['join'];
+	            break;
+
+        	case BX_CMT_FILTER_OTHERS:
+        		$aBindings['cmt_author_id'] = $iAuthorId;
+
+        		$sWhereClause .= " AND `{$this->_sTable}`.`cmt_author_id` <> :cmt_author_id";
+        		break;
+        }
+
+        $sQuery = "SELECT COUNT(*) FROM `{$this->_sTable}` $sJoinClause WHERE `{$this->_sTable}`.`cmt_object_id` = :cmt_object_id" . $sWhereClause;
+        return (int)$this->getOne($sQuery, $aBindings);
+    }
+
+    function getComments ($iId, $iCmtVParentId = 0, $iAuthorId = 0, $sFilter = '', $aOrder = array(), $iStart = 0, $iCount = -1)
+    {
+    	$aBindings = array(
+    		'cmt_object_id' => $iId
+    	);
+        $sFields = $sJoin = "";
+
+        $oVote = $this->_oMain->getVoteObject(0);
+        if($oVote !== false) {
+            $aSql = $oVote->getSqlParts($this->_sTable, 'cmt_id');
+
+            $sFields .= $aSql['fields'];
+            $sJoin .= $aSql['join'];
+        }
+
+        $sWhereParent = '';
+        if((int)$iCmtVParentId >= 0) {
+        	$aBindings['cmt_vparent_id'] = $iCmtVParentId;
+
+            $sWhereParent = " AND `{$this->_sTable}`.`cmt_vparent_id` = :cmt_vparent_id";
+        }
+
+        if(in_array($sFilter, array(BX_CMT_FILTER_FRIENDS, BX_CMT_FILTER_SUBSCRIPTIONS))) {
+            $oConnection = BxDolConnection::getObjectInstance($this->_oMain->getConnectionObject($sFilter));
+
+            $aQueryParts = $oConnection->getConnectedContentAsSQLParts($this->_sTable, 'cmt_author_id', $iAuthorId);
+            $sJoin .= ' ' . $aQueryParts['join'];
+        }
+
+        $sOder = " ORDER BY `{$this->_sTable}`.`cmt_time` ASC";
+        if(isset($aOrder['by']) && isset($aOrder['way'])) {
+            $aOrder['way'] = strtoupper(in_array($aOrder['way'], array(BX_CMT_ORDER_WAY_ASC, BX_CMT_ORDER_WAY_DESC)) ? $aOrder['way'] : BX_CMT_ORDER_WAY_ASC);
+
+            switch($aOrder['by']) {
+                case BX_CMT_ORDER_BY_DATE:
+                    $sOder = " ORDER BY `{$this->_sTable}`.`cmt_time` " . $aOrder['way'];
+                    break;
+
+                case BX_CMT_ORDER_BY_POPULAR:
+                    $sOder = " ORDER BY `{$this->_sTable}`.`cmt_rate` " . $aOrder['way'];
+                    break;
+            }
+        }
+
+       	$sLimit = $iCount != -1 ? $this->prepareAsString(" LIMIT ?, ?", (int)$iStart, (int)$iCount) : '';
+
+        $sQuery = "SELECT
+                `{$this->_sTable}`.`cmt_id`,
+                `{$this->_sTable}`.`cmt_parent_id`,
+                `{$this->_sTable}`.`cmt_vparent_id`,
+                `{$this->_sTable}`.`cmt_object_id`,
+                `{$this->_sTable}`.`cmt_author_id`,
+                `{$this->_sTable}`.`cmt_level`,
+                `{$this->_sTable}`.`cmt_text`,
+                `{$this->_sTable}`.`cmt_replies`,
+                `{$this->_sTable}`.`cmt_time`
+                $sFields
+            FROM `{$this->_sTable}`
+            $sJoin
+            WHERE `{$this->_sTable}`.`cmt_object_id` = :cmt_object_id" . $sWhereParent . $sOder . $sLimit;
+
+        return $this->getAll($sQuery, $aBindings);
+    }
+
+	function getCommentsBy($aParams = array())
+    {
+    	$aMethod = array('name' => 'getAll', 'params' => array(0 => 'query', 1 => array()));
+        $sSelectClause = $sJoinClause = $sWhereClause = $sOrderClause = $sLimitClause = "";
+
+        $sSelectClause = "
+        	`{$this->_sTable}`.`cmt_id`,
+            `{$this->_sTable}`.`cmt_parent_id`,
+            `{$this->_sTable}`.`cmt_vparent_id`,
+            `{$this->_sTable}`.`cmt_object_id`,
+            `{$this->_sTable}`.`cmt_author_id`,
+            `{$this->_sTable}`.`cmt_level`,
+            `{$this->_sTable}`.`cmt_text`,
+            `{$this->_sTable}`.`cmt_replies`,
+            `{$this->_sTable}`.`cmt_time`";
+
+        if(isset($aParams['object_id'])) {
+        	$aMethod['params'][1]['cmt_object_id'] = (int)$aParams['object_id'];
+
+        	$sWhereClause .= " AND `{$this->_sTable}`.`cmt_object_id` = :cmt_object_id";
+        }
+
+        switch($aParams['type']) {
+            case 'id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1]['id'] = (int)$aParams['id'];
+
+                $sWhereClause .= " AND `{$this->_sTable}`.`cmt_id` = :id";
+                break;
+
+            case 'latest':
+            	if(!empty($aParams['author'])) {
+            		$aMethod['params'][1]['cmt_author_id'] = (int)$aParams['author'];
+
+            		$sWhereClause .= " AND `{$this->_sTable}`.`cmt_author_id` " . (isset($aParams['others']) && (int)$aParams['others'] == 1 ? "<>" : "=") . " :cmt_author_id";
+            	}
+
+                $sOrderClause = "`{$this->_sTable}`.`cmt_time` DESC";
+                $sLimitClause = "";
+                if(isset($aParams['per_page']))
+                	$sLimitClause = $this->prepareAsString("?, ?", $aParams['start'], $aParams['per_page']);
+
+                break;
+
+            case 'object_id':
+        		$aMethod['params'][1]['cmt_object_id'] = (int)$aParams['object_id'];
+
+        		$sWhereClause .= " AND `{$this->_sTable}`.`cmt_object_id` = :cmt_object_id";
+
+                $sOrderClause = "`{$this->_sTable}`.`cmt_time` ASC";
+                $sLimitClause = "";
+                if(isset($aParams['per_page']))
+                	$sLimitClause = $this->prepareAsString("?, ?", $aParams['start'], $aParams['per_page']);
+
+                break;
+
+            case 'search_ids':
+                $aMethod['name'] = 'getColumn';
+
+                $sSelectClause = "`{$this->_sTable}`.`cmt_id`";
+
+                if (!empty($aParams['start']) && !empty($aParams['per_page']))
+                    $sLimitClause = $this->prepareAsString("?, ?", $aParams['start'], $aParams['per_page']);
+                elseif (!empty($aParams['per_page']))
+                    $sLimitClause = $this->prepareAsString("?", $aParams['per_page']);
+
+                $sWhereConditions = "1";
+                foreach($aParams['search_params'] as $sSearchParam => $aSearchParam) {
+                    $sSearchValue = "";
+                    switch ($aSearchParam['operator']) {
+                        case 'like':
+                            $sSearchValue = " LIKE " . $this->escape("%" . $aSearchParam['value'] . "%");
+                            break;
+
+                        case 'in':
+                            $sSearchValue = " IN (" . $this->implode_escape($aSearchParam['value']) . ")";
+                            break;
+
+                        case 'and':
+                            $iResult = 0;
+                            if (is_array($aSearchParam['value']))
+                                foreach ($aSearchParam['value'] as $iValue)
+                                    $iResult |= pow (2, $iValue - 1);
+                            else 
+                                $iResult = (int)$aSearchParam['value'];
+
+                            $sSearchValue = " & " . $iResult . "";
+                            break;
+
+                        default:
+                             $sSearchValue = " " . $aSearchParam['operator'] . " :" . $sSearchParam;
+                             $aMethod['params'][1][$sSearchParam] = $aSearchParam['value'];                             
+                    }
+
+                    $sWhereConditions .= " AND `{$this->_sTable}`.`" . $sSearchParam . "`" . $sSearchValue;
+                }
+
+                $sWhereClause .= " AND (" . $sWhereConditions . ")"; 
+
+                $sOrderClause .=  "`{$this->_sTable}`.`cmt_time` ASC";
+                break;
+
+            case 'all_ids':
+                $aMethod['name'] = 'getColumn';
+
+                $sSelectClause = "`{$this->_sTable}`.`cmt_id`";
+                $sOrderClause =  "`{$this->_sTable}`.`cmt_time` ASC";
+                break;
+        }
+
+        if(!empty($sOrderClause))
+            $sOrderClause = 'ORDER BY ' . $sOrderClause;
+
+        if(!empty($sLimitClause))
+            $sLimitClause = 'LIMIT ' . $sLimitClause;
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . " FROM `{$this->_sTable}` " . $sJoinClause . " WHERE 1 " . $sWhereClause . " " . $sOrderClause . " " . $sLimitClause;
+		return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+
+    function getComment ($iId, $iCmtId)
+    {
+        $sFields = $sJoin = "";
+
+        $oVote = $this->_oMain->getVoteObject($iCmtId);
+        if($oVote !== false) {
+            $aSql = $oVote->getSqlParts($this->_sTableIds, 'id');
+
+            $sFields .= $aSql['fields'];
+            $sJoin .= $aSql['join'];
+        }
+
+        $sQuery = $this->prepare("SELECT
+                `{$this->_sTable}`.`cmt_id`,
+                `{$this->_sTableIds}`.`id` AS `cmt_unique_id`,
+                `{$this->_sTable}`.`cmt_parent_id`,
+                `{$this->_sTable}`.`cmt_vparent_id`,
+                `{$this->_sTable}`.`cmt_object_id`,
+                `{$this->_sTable}`.`cmt_author_id`,
+                `{$this->_sTable}`.`cmt_level`,
+                `{$this->_sTable}`.`cmt_text`,
+                `{$this->_sTable}`.`cmt_replies`,
+                `{$this->_sTable}`.`cmt_time`
+                $sFields
+            FROM `{$this->_sTable}`
+            LEFT JOIN `{$this->_sTableIds}` ON (`{$this->_sTable}`.`cmt_id` = `{$this->_sTableIds}`.`cmt_id` AND `{$this->_sTableIds}`.`system_id` = ?)
+            $sJoin
+            WHERE `{$this->_sTable}`.`cmt_object_id` = ? AND `{$this->_sTable}`.`cmt_id` = ?
+            LIMIT 1", $this->_oMain->getSystemId(), $iId, $iCmtId);
+        return $this->getRow($sQuery);
+    }
+
+    function getCommentSimple ($iId, $iCmtId)
+    {
+        $sQuery = $this->prepare("SELECT * FROM {$this->_sTable} AS `c` WHERE `cmt_object_id` = ? AND `cmt_id` = ? LIMIT 1", $iId, $iCmtId);
+        return $this->getRow($sQuery);
+    }
+
+    function removeComment ($iId, $iCmtId, $iCmtParentId)
+    {
+        $sQuery = $this->prepare("DELETE FROM {$this->_sTable} WHERE `cmt_object_id` = ? AND `cmt_id` = ? LIMIT 1", $iId, $iCmtId);
+        if (!$this->query($sQuery))
+            return false;
+
+        if($iCmtParentId)
+            $this->updateRepliesCount($iCmtParentId, -1);
+
+        return true;
+    }
+
+    function saveImages($iSystemId, $iCmtId, $iImageId)
+    {
+        $sQuery = $this->prepare("INSERT IGNORE INTO `{$this->_sTableFiles2Entries}` SET `system_id`=?, `cmt_id`=?, `image_id`=?", $iSystemId, $iCmtId, $iImageId);
+        return (int)$this->query($sQuery) > 0;
+    }
+
+    function getFiles($iSystemId, $iCmtId, $iId = false)
+    {
+    	$aBindings = array(
+    		'system_id' => $iSystemId
+    	);
+
+        $sJoin = "";
+        $sWhere = " AND `tf2e`.`system_id` = :system_id ";
+
+        if($iCmtId !== false) {
+        	$aBindings['cmt_id'] = $iCmtId;
+
+            $sWhere .= " AND `tf2e`.`cmt_id` = :cmt_id ";
+        }
+
+        if($iId !== false) {
+        	$aBindings['cmt_object_id'] = $iId;
+
+            $sWhere .= " AND `te`.`cmt_object_id` = :cmt_object_id";
+            $sJoin .= " INNER JOIN `{$this->_sTable}` AS `te` ON (`tf2e`.`cmt_id` = `te`.`cmt_id`) ";
+        }
+
+        $sQuery = "SELECT 
+        		`tf2e`.*,
+        		`tf`.`file_name` AS `file_name`,
+        		`tf`.`mime_type` AS `mime_type`,
+        		`tf`.`size` AS `size` 
+        	FROM `{$this->_sTableFiles2Entries}` AS `tf2e` 
+        	LEFT JOIN `{$this->_sTableFiles}` AS `tf` ON (`tf2e`.`image_id` = `tf`.`id`) " . $sJoin . " 
+        	WHERE 1 " . $sWhere;
+
+        return $this->getAll($sQuery, $aBindings);
+    }
+
+    function deleteImages($iSystemId, $iCmtId)
+    {
+    	$aBindings = array(
+    		'system_id' => $iSystemId
+    	);
+
+        $sWhereAddon = "";
+        if ($iCmtId !== false) {
+        	$aBindings['cmt_id'] = $iCmtId;
+
+            $sWhereAddon = " AND `cmt_id` = :cmt_id ";
+        }
+
+        return $this->query("DELETE FROM `{$this->_sTableFiles2Entries}` WHERE `system_id` = :system_id" . $sWhereAddon, $aBindings);
+    }
+
+    function updateRepliesCount($iCmtId, $iCount)
+    {
+        $sQuery = $this->prepare("UPDATE `{$this->_sTable}` SET `cmt_replies`=`cmt_replies`+? WHERE `cmt_id`=? LIMIT 1", $iCount, $iCmtId);
+        return $this->query($sQuery);
+    }
+
+    function deleteAuthorComments ($iAuthorId, &$aFiles = null, &$aCmtIds = null)
+    {
+        $aSystem = $this->_oMain->getSystemInfo();
+
+        $isDelOccured = 0;
+        $sQuery = $this->prepare("SELECT `cmt_id`, `cmt_parent_id` FROM {$this->_sTable} WHERE `cmt_author_id` = ? AND `cmt_replies` = 0", $iAuthorId);
+        $a = $this->getAll ($sQuery);
+        for ( reset($a) ; list (, $r) = each ($a) ; ) {
+            $sQuery = $this->prepare("DELETE FROM {$this->_sTable} WHERE `cmt_id` = ?", $r['cmt_id']);
+            $this->query ($sQuery);
+
+            $sQuery = $this->prepare("UPDATE {$this->_sTable} SET `cmt_replies` = `cmt_replies` - 1 WHERE `cmt_id` = ?", $r['cmt_parent_id']);
+            $this->query ($sQuery);
+
+            $aFilesMore = $this->convertImagesArray($this->getFiles($aSystem['system_id'], $r['cmt_id']));
+            $this->deleteImages($aSystem['system_id'], $r['cmt_id']);
+            if ($aFilesMore && null !== $aFiles)
+                $aFiles = array_merge($aFiles, $aFilesMore);
+
+            if (null !== $aCmtIds)
+                $aCmtIds[] = $r['cmt_id'];
+
+            $isDelOccured = 1;
+        }
+        $sQuery = $this->prepare("UPDATE {$this->_sTable} SET `cmt_author_id` = 0 WHERE `cmt_author_id` = ? AND `cmt_replies` != 0", $iAuthorId);
+        $this->query ($sQuery);
+        if ($isDelOccured)
+            $this->query ("OPTIMIZE TABLE {$this->_sTable}");
+    }
+
+    function deleteObjectComments ($iObjectId, &$aFilesReturn = null, &$aCmtIds = null)
+    {
+        $aSystem = $this->_oMain->getSystemInfo();
+        $aFiles = $this->convertImagesArray($this->getFiles($aSystem['system_id'], false, $iObjectId));
+
+        if ($aFiles) {
+            $sQuery = $this->prepare("DELETE FROM {$this->_sTableFiles2Entries} WHERE `system_id` = ? AND `image_id` IN(" . $this->implode_escape($aFiles) . ")", $aSystem['system_id']);
+            $this->query($sQuery);
+        }
+
+        if (null !== $aCmtIds) {
+            $sQuery = $this->prepare("SELECT `cmt_id` FROM {$this->_sTable} WHERE `cmt_object_id` = ?", $iObjectId);
+            $aCmtIds = $this->getColumn ($sQuery);
+        }
+
+        $sQuery = $this->prepare("DELETE FROM {$this->_sTable} WHERE `cmt_object_id` = ?", $iObjectId);
+        $this->query ($sQuery);
+        $this->query ("OPTIMIZE TABLE {$this->_sTable}");
+
+        if (null !== $aFilesReturn)
+            $aFilesReturn = $aFiles;
+    }
+
+    function deleteAll ($iSystemId, &$aFiles = null, &$aCmtIds = null)
+    {
+        // get files
+        if (null !== $aFiles)
+            $aFiles = $this->convertImagesArray($this->getFiles($iSystemId, false));
+
+        // delete files
+        $this->deleteImages($iSystemId, false);
+
+        if (null !== $aCmtIds)
+            $aCmtIds = $this->getColumn ("SELECT `cmt_id` FROM {$this->_sTable}");
+
+        // delete comments
+        $sQuery = $this->prepare("TRUNCATE TABLE {$this->_sTable}");
+        $this->query ($sQuery);
+    }
+
+    function deleteCmtIds ($iSystemId, $iCmtId)
+    {
+        $sQuery = $this->prepare("DELETE FROM {$this->_sTableIds} WHERE `system_id` = ? AND `cmt_id` = ?", $iSystemId, $iCmtId);
+        return $this->query ($sQuery);
+    }
+
+	function getObjectAuthorId($iId)
+    {
+        $sQuery = $this->prepare("SELECT `{$this->_sTriggerFieldAuthor}` FROM `{$this->_sTriggerTable}` WHERE `{$this->_sTriggerFieldId}` = ? LIMIT 1", $iId);
+        return $this->getOne($sQuery);
+    }
+
+    function getObjectTitle($iId)
+    {
+        $sQuery = $this->prepare("SELECT `{$this->_sTriggerFieldTitle}` FROM `{$this->_sTriggerTable}` WHERE `{$this->_sTriggerFieldId}` = ? LIMIT 1", $iId);
+        return $this->getOne($sQuery);
+    }
+
+    function updateTriggerTable($iId, $iCount)
+    {
+        $sQuery = $this->prepare("UPDATE `{$this->_sTriggerTable}` SET `{$this->_sTriggerFieldComments}` = ? WHERE `{$this->_sTriggerFieldId}` = ? LIMIT 1", $iCount, $iId);
+        return $this->query($sQuery);
+    }
+
+    function getUniqId($iSystemId, $iCmtId)
+    {
+        $sQuery = $this->prepare("SELECT `id` FROM `{$this->_sTableIds}` WHERE `system_id` = ? AND `cmt_id` = ?", $iSystemId, $iCmtId);
+        if ($iUniqId = $this->getOne($sQuery))
+            return $iUniqId;
+            
+        $sQuery = $this->prepare("INSERT INTO `{$this->_sTableIds}` SET `system_id` = ?, `cmt_id` = ?", $iSystemId, $iCmtId);
+        if (!$this->query($sQuery))
+            return false;
+
+        return $this->lastId();
+    }
+
+    protected function convertImagesArray($a)
+    {
+        if (!$a || !is_array($a))
+            return array();
+
+        $aFiles = array ();
+        foreach ($a as $aFile)
+            $aFiles[] = $aFile['image_id'];
+        return $aFiles;
+    }
+}
+
+/** @} */

+ 752 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolConnection.php

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

+ 303 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolContentInfo.php

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

+ 83 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolContentInfoCmts.php

@@ -0,0 +1,83 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolContentInfoCmts extends BxDolContentInfo
+{
+    protected $_oCmts;
+
+    protected function __construct($sSystem)
+    {
+        parent::__construct($sSystem);
+
+        $this->_oCmts = BxDolCmts::getObjectInstance($this->_aSystem['alert_unit'], 0, false);
+    }
+
+    public function getContentAuthor ($iContentId)
+    {
+        return $this->_oCmts->serviceGetAuthor($iContentId);
+    }
+
+    public function getContentDateAdded ($iContentId)
+    {
+        return $this->_oCmts->serviceGetDateAdded($iContentId);
+    }
+
+    public function getContentDateChanged ($iContentId)
+    {
+        return $this->_oCmts->serviceGetDateChanged($iContentId);
+    }
+
+    public function getContentTitle ($iContentId)
+    {
+        return $this->_oCmts->serviceGetTitle($iContentId);
+    }
+
+    public function getContentThumb ($iContentId)
+    {
+        return $this->_oCmts->serviceGetThumb($iContentId);
+    }
+
+    public function getContentLink ($iContentId)
+    {
+        return $this->_oCmts->serviceGetLink($iContentId);
+    }
+
+    public function getContentText ($iContentId)
+    {
+        return $this->_oCmts->serviceGetText($iContentId);
+    }
+
+    public function getContentInfo ($iContentId, $bSearchableFieldsOnly = true)
+    {
+        return $this->_oCmts->serviceGetInfo($iContentId);
+    }
+
+    public function getContentSearchResultUnit ($iContentId, $sUnitTemplate = '')
+    {
+        $this->_oCmts->addCssJs();
+        return '<ul class="cmts">' . $this->_oCmts->serviceGetSearchResultUnit($iContentId, $sUnitTemplate) . '</ul>';
+    }
+
+    public function getAll ($aParams = array())
+    {
+        return $this->_oCmts->serviceGetAll($aParams);
+    }
+    
+    public function getSearchableFieldsExtended ()
+    {
+        return $this->_oCmts->serviceGetSearchableFieldsExtended();
+    }
+
+    public function getSearchResultExtended ($aParams, $iStart = 0, $iPerPage = 0)
+    {
+        return $this->_oCmts->serviceGetSearchResultExtended($aParams, $iStart, $iPerPage);
+    }
+}
+
+/** @} */

+ 18 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolCronQueueEmail.php

@@ -0,0 +1,18 @@
+<?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 BxDolCronQueueEmail extends BxDolCron
+{
+    public function processing()
+    {
+    	BxDolQueueEmail::getInstance()->send((int)getParam('sys_eq_send_per_start'));
+    }
+}
+
+/** @} */

+ 18 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolCronQueuePush.php

@@ -0,0 +1,18 @@
+<?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 BxDolCronQueuePush extends BxDolCron
+{
+    public function processing()
+    {
+    	BxDolQueuePush::getInstance()->send((int)getParam('sys_push_queue_send_per_start'));
+    }
+}
+
+/** @} */

+ 1185 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolDb.php

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

+ 184 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolEmailTemplates.php

@@ -0,0 +1,184 @@
+<?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('BxDolPermalinks');
+
+/**
+ * Email templates are used to send preformated messages.
+ *
+ * Email templates are multilingual.
+ *
+ * User is sent email in language which is defined in their account, if account language is not set - then default site language is used.
+ *
+ * Email templates use template system, so it is possible to customize header/footer via Studio -> Polyglot -> Emails header/footer
+ *
+ * 1-click unsubscribe link is added automatically to every email (except a few system emails, which is not supposed to unsubscribe from, like forgot password email).
+ * Please note: make sure that {unsubscribe} marker is not removed, or unsubscribe link will not be added.
+ *
+ *
+ * @section example Example of usage
+ *
+ * Send an email using email templates:
+ *
+ * @code
+ *
+ *     // define custom template variables
+ *     $aPlus = array();
+ *     $aPlus['email'] = 'ktoto@example.com';
+ *     $aPlus['conf_code'] = '123456';
+ *     $aPlus['conf_link'] = BX_DOL_URL_ROOT . 'page.php?i=confirm-email&code=123456';
+ *     $aPlus['conf_form_link'] = BX_DOL_URL_ROOT . 'page.php?i=confirm-email';
+ *
+ *     $aTemplate = BxDolEmailTemplates::getInstance()->parseTemplate('t_Confirmation', $aPlus); // get class instance and parse template
+ *
+ *     if ($aTemplate && sendMail('ktoto@example.com', $aTemplate['Subject'], $aTemplate['Body'])) // send email if template exists
+ *         echo 'email was successfully sent';
+ *     else
+ *         echo 'email send failed';
+ *
+ * @endcode
+ *
+ */
+class BxDolEmailTemplates extends BxDolFactory implements iBxDolSingleton
+{
+    protected $_oEmailTemplatesQuery;
+
+    protected $iDefaultLangId;
+    protected $iFallbackLangId;
+    protected $aDefaultKeys;
+
+    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();
+
+        $oLang = BxDolLanguages::getInstance();
+        $this->iDefaultLangId = $oLang->getCurrentLangId();
+        $this->iFallbackLangId = $oLang->getLangId('en');
+
+        $sAboutUs = _t('_sys_et_txt_about_us');
+        $this->aDefaultKeys = array(
+        	'site_url' => BX_DOL_URL_ROOT,
+        	'site_name' => getParam('site_title'),
+            'about_us' => '<a href="' . BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=about') . '" title="' . bx_html_attribute($sAboutUs) . '">' . $sAboutUs . '</a>',
+        );
+
+        $this->_oEmailTemplatesQuery = BxDolEmailTemplatesQuery::getInstance();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolEmailTemplates();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    /**
+     * Function will return array of needed template ;
+     *
+     * @param  string  $sTemplateName - name of necessary template.
+     * @param  integer $iAccountId    - account ID of registered member.
+     * @return array   with template 'Subject' and 'Body'.
+     */
+    function getTemplate($sTemplateName, $iAccountId = 0 )
+    {
+        $iUseLang = $this->iDefaultLangId;
+        if ($iAccountId) {
+            $oAccount = BxDolAccount::getInstance($iAccountId);
+            if ($oAccount) {
+                $aAccountInfo = $oAccount->getInfo();
+                $iUseLang = $aAccountInfo['lang_id'] ? $aAccountInfo['lang_id'] : $this->iDefaultLangId;
+            }
+        }
+
+        return $this->_oEmailTemplatesQuery->getTemplate ($sTemplateName, $iUseLang, $this->iDefaultLangId, $this->iFallbackLangId);
+    }
+
+    /**
+     * Function will return array of needed template with neccessary markers replaced ;
+     *
+     * @param  string  $sTemplateName - name of necessary template.
+     * @param  string  $aTemplatekeys - key ane value pairs to replace in subject and body.
+     * @param  integer $iAccountId    - account ID of registered member.
+     * @param  integer $iProfileId    - profile ID of registered member.
+     * @return array   with template 'Subject' and 'Body'.
+     */
+    function parseTemplate($sTemplateName, $aTemplateKeys, $iAccountId = 0, $iProfileId = 0)
+    {
+        $aTemplate = $this->getTemplate($sTemplateName, $iAccountId);
+        if (!$aTemplate)
+            return false;
+        return array(
+           'Subject' => $this->parseContent($aTemplate['Subject'], $aTemplateKeys, $iAccountId, $iProfileId),
+           'Body' => $this->parseContent($aTemplate['Body'], $aTemplateKeys, $iAccountId, $iProfileId)
+        );
+    }
+
+    function parseContent($sContent, $aKeys, $iAccountId = 0, $iProfileId = 0)
+    {
+        $aResultKeys = $this->aDefaultKeys;
+
+        $oProfile = null;
+        if ($iProfileId) {
+            $oProfile = BxDolProfile::getInstance($iProfileId);
+            if ($oProfile && ($aProfileInfo = $oProfile->getInfo())) {
+                $aResultKeys = array_merge($aResultKeys, array(
+                    'profile_name' => $oProfile->getDisplayName(),
+                    'profile_url' => $oProfile->getUrl(),
+                    'profile_thumb' => $oProfile->getThumb(),
+                    'profile_icon' => $oProfile->getIcon(),
+
+                    'profile_id' => $aProfileInfo['id'],
+                    'profile_status' => $aProfileInfo['status'],
+                    'profile_type' => $aProfileInfo['type'],
+                    'profile_content_id' => $aProfileInfo['content_id'],
+                ));
+            }
+        }
+        
+        if ($iAccountId || $oProfile) {
+            $oAccount = BxDolAccount::getInstance($iAccountId ? $iAccountId : $oProfile->getAccountId());
+            if ($oAccount && ($aAccountInfo = $oAccount->getInfo())) {
+                $aResultKeys = array_merge($aResultKeys, array(
+                    'account_id' => $aAccountInfo['id'],
+                    'account_name' => $oAccount->getDisplayName(),
+                    'account_email' => $aAccountInfo['email'],
+                ));
+            }
+        }
+
+        if (is_array($aKeys))
+            $aResultKeys = array_merge($aResultKeys, $aKeys);
+
+        $aKeyWrapper = array('{', '}');
+        $sContent = BxDolTemplate::getInstance()->parseHtmlByContent($sContent, array(
+            'email_header' => getParam('site_email_html_template_header'),
+            'email_footer' => getParam('site_email_html_template_footer'),
+        ), $aKeyWrapper);
+
+        return BxDolTemplate::getInstance()->parseHtmlByContent($sContent, $aResultKeys, $aKeyWrapper);
+    }
+}
+
+/** @} */

+ 1701 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolForm.php

@@ -0,0 +1,1701 @@
+<?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_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
+
+/**
+ * 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.
+ *      - 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);
+    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, 'hostdomain' => 1, 'hostdomainchat' => 1);
+
+    protected $_aMarkers = array ();
+
+    protected $oTemplate;
+
+    protected $_isValid = true;
+    protected $_sCheckerHelper;
+    protected $_aSpecificValues;
+
+    public $aFormAttrs; ///< form html element attributes
+    public $aInputs; ///< form inputs
+    public $aParams; ///< additional form parameters
+    public $id; ///< Form element id
+
+    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_POST;
+
+        // set default action
+        if (!isset($this->aFormAttrs['action']))
+            $this->aFormAttrs['action'] = '';
+
+        $this->_sCheckerHelper = isset($this->aParams['checker_helper']) ? $this->aParams['checker_helper'] : '';
+
+        BxDolForm::genCsrfToken();
+    }
+
+    /**
+     * 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 BxDolFormChecker($this->_sCheckerHelper);
+        $oChecker->setFormMethod($this->aFormAttrs['method'], $aSpecificValues);
+        $oChecker->setFormParams($this->aParams);
+
+        // 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);
+
+
+        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;
+
+        }
+    }
+
+    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)
+    {
+        $oChecker = new BxDolFormChecker($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))
+            return $oDb->lastId();
+        return false;
+    }
+
+    function update ($val, $aValsToAdd = array(), &$aTrackTextFieldsChanges = null)
+    {
+        $oChecker = new BxDolFormChecker($this->_sCheckerHelper);
+        $oChecker->setFormMethod($this->aFormAttrs['method'], $this->_aSpecificValues);
+        $sSql = $oChecker->dbUpdate($val, $this->aParams['db'], $this->aInputs, $aValsToAdd, $aTrackTextFieldsChanges);
+        if (!$sSql)
+            return false;
+        return BxDolDb::getInstance()->query($sSql);
+    }
+
+    function delete ($val)
+    {
+        $oChecker = new BxDolFormChecker($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 BxDolFormChecker($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 getId ()
+    {
+        return $this->id;
+    }
+
+    function setId ($sId)
+    {
+        $this->id = $sId;
+        $this->aFormAttrs['id'] = $sId;
+    }
+
+    function setName($sName)
+    {
+        $this->aFormAttrs['name'] = $sName;
+    }
+
+    function setValid ($isValid)
+    {
+        $this->_isValid = $isValid;
+    }
+
+    function isValid ()
+    {
+        return $this->_isValid;
+    }
+
+    function isSubmittedAndValid ()
+    {
+        return ($this->isSubmitted() && $this->isValid());
+    }
+
+    function getSpecificValues()
+    {
+        return $this->_aSpecificValues;
+    }
+
+    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)
+    {
+        if (getParam('sys_security_form_token_enable') != 'on')
+            return false;
+
+        $oSession = BxDolSession::getInstance();
+
+        $iCsrfTokenLifetime = (int)getParam('sys_security_form_token_lifetime');
+        if($oSession->getValue('csrf_token') === false || ($iCsrfTokenLifetime != 0 && time() - (int)$oSession->getValue('csrf_token_time') > $iCsrfTokenLifetime)) {
+            $sToken = genRndPwd(20, true);
+            $oSession->setValue('csrf_token', $sToken);
+            $oSession->setValue('csrf_token_time', time());
+        } else
+            $sToken = $oSession->getValue('csrf_token');
+
+        if($bReturn)
+            return $sToken;
+    }
+    public static function getCsrfToken()
+    {
+        $oSession = BxDolSession::getInstance();
+        return $oSession->getValue('csrf_token');
+    }
+    public static function getCsrfTokenTime()
+    {
+        $oSession = BxDolSession::getInstance();
+        return $oSession->getValue('csrf_token_time');
+    }
+
+    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 BxDolFormNested))
+                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 BxDolFormNested($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']))
+                    $aSpecificValues = array ($this->aParams['db']['submit_name'] => 1);
+                foreach ($oFormNested->aInputs as $r) {
+                    $sName = str_replace('[]', '', $r['name']);
+                    $aValue = $this->getSubmittedValue($sName, $this->aFormAttrs['method']);
+                    $aSpecificValues[$sName] = $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);
+    }
+}
+
+class BxDolFormChecker
+{
+    protected $_oChecker;
+    protected $_sFormMethod;
+    protected $_aFormParams;
+    protected $_bFormCsrfChecking;
+    protected $_aSpecificValues;
+
+    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 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']);
+            $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) && 
+                !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);
+                }
+            }
+
+            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;
+            }
+
+            // special check for location field
+            if ('location' == $aInputs[$k]['type'] && $val && !BxDolForm::getSubmittedValue($a['name'] . '_country', $this->_sFormMethod, $this->_aSpecificValues)) {
+                $aInputs[$k]['value'] = '';
+                $val = '';
+            }
+
+            $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), $a['checker']['params']) : array ($val));
+            else
+                $bool = true;
+
+            if (is_string($bool)) {
+                ++$iErrors;
+                $aInputs[$k]['error'] = $bool;
+            } elseif (!$bool) {
+                ++$iErrors;
+                $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 && ($mixedCsrfTokenSys = BxDolForm::getCsrfToken()) !== false) {
+            $mixedCsrfTokenUsr = BxDolForm::getSubmittedValue('csrf_token', $this->_sFormMethod, $this->_aSpecificValues);
+            unset($aInputs['csrf_token']);
+
+            if($mixedCsrfTokenUsr === false || $mixedCsrfTokenSys != $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 ($a['type'] != 'textarea')
+                    continue;
+
+                $a['name'] = str_replace('[]', '', $a['name']);
+                $val = BxDolForm::getSubmittedValue($a['name'], $this->_sFormMethod, $this->_aSpecificValues);
+                if (!$val)
+                    continue;
+
+                if (!$oChecker->checkIsSpam($val))
+                    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 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+]$#');
+    }
+    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 checkEmail($s)
+    {
+        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)
+    {
+        $bSpam = false;
+        bx_alert('system', 'check_spam', 0, getLoggedId(), array('is_spam' => &$bSpam, 'content' => $val, 'where' => 'form'));
+        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);
+    }
+    static public function displayDateTs ($i)
+    {
+        return bx_process_output ($i, BX_DATA_DATE_TS);
+    }
+    static public function displayDateUtc ($i)
+    {
+        return bx_process_output ($i, BX_DATA_DATE_TS_UTC);
+    }    
+    static public function displayDateTime ($i)
+    {
+        return bx_process_output ($i, BX_DATA_DATETIME);
+    }
+    static public function displayDateTimeTs ($i)
+    {
+        return bx_process_output ($i, BX_DATA_DATETIME_TS);
+    }
+    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;
+        $a = array();
+        while ($bit < BX_DOL_INT_MAX && $bit > 0) {
+            if ($bit & $i)
+                $a[] = (int)log($bit, 2) + 1;
+            $bit <<= 1;
+        }
+        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);
+    }
+}
+
+/** @} */

+ 222 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolFormQuery.php

@@ -0,0 +1,222 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+bx_import('BxDolForm');
+
+/**
+ * Database queries for forms.
+ * @see BxDolForm
+ */
+class BxDolFormQuery extends BxDolDb
+{
+    protected $_aObject;
+
+    protected static $TYPES_SET = array (
+        'select_multiple' => 1,
+        'checkbox_set' => 1,
+    );
+    protected static $TYPES_TRANSLATABLE = array (
+        'submit' => 1,
+        'reset' => 1,
+        'button' => 1,
+    );
+
+    public function __construct($aObject)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+    }
+
+    static public function getFormArray ($sObject, $sDisplayName)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_form` WHERE `active` = 1 AND `object` = ?", $sObject);
+        $aObject = $oDb->getRow($sQuery);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_form_displays` WHERE `object` = ? AND `display_name` = ?", $sObject, $sDisplayName);
+        $aDisplay = $oDb->getRow($sQuery);
+        if (!$aDisplay || !is_array($aDisplay))
+            return false;
+
+        $aForm = array ();
+
+        // form attrs
+        if (!empty($aObject['form_attrs']))
+            $aAddFormAttrs = unserialize($aObject['form_attrs']);
+
+        $aDefaultsFormAttrs = array(
+            'action' => $aObject['action'],
+            'name' => $aObject['object'],
+            'id' => $aObject['object'],
+        );
+
+        $aForm['form_attrs'] = array_merge($aDefaultsFormAttrs, !empty($aAddFormAttrs) && is_array($aAddFormAttrs) ? $aAddFormAttrs : array());
+
+        // form action
+        if (!empty($aForm['form_attrs']['action']) && 0 !== strncasecmp($aForm['form_attrs']['action'], 'http://', 7) && 0 !== strncasecmp($aForm['form_attrs']['action'], 'https://', 8))
+            $aForm['form_attrs']['action'] = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink($aForm['form_attrs']['action']);
+
+        // params
+        if (!empty($aObject['params']))
+            $aAddFormParams = unserialize($aObject['params']);
+
+        $mixedSubminName = '';
+        if (!empty($aObject['submit_name'])) {
+            $mixedSubminName = @unserialize($aObject['submit_name']);
+            if($mixedSubminName === false)
+                $mixedSubminName = $aObject['submit_name'];
+        }
+
+        $aDefaultsFormParams = array(
+            'db' => array(
+                'submit_name' => $mixedSubminName,
+                'table' => $aObject['table'],
+                'key' => $aObject['key'],
+                'uri' => $aObject['uri'],
+                'uri_title' => $aObject['uri_title'],
+            ),
+            'view_mode' => $aDisplay['view_mode'],
+            'display' => $sDisplayName,
+        );
+
+        $aForm['params'] = array_merge_recursive($aDefaultsFormParams, !empty($aAddFormParams) && is_array($aAddFormParams) ? $aAddFormParams : array());
+
+        // form inputs
+        $sQuery = $oDb->prepare("SELECT `i`.*, `d`.`visible_for_levels` FROM `sys_form_inputs` AS `i` INNER JOIN `sys_form_display_inputs` AS `d` ON (`d`.`input_name` = `i`.`name`) WHERE `d`.`active` = 1 AND `d`.`display_name` = ? AND `i`.`object` = ? ORDER BY `d`.`order` ASC", $aDisplay['display_name'], $sObject);
+        $aInputs = $oDb->getAllWithKey($sQuery, 'name');
+        $aForm['inputs'] = array();
+        $aInputSets = array();
+        foreach ($aInputs as $a) {
+
+            // main attributes
+            $aInput = array (
+                'type' => $a['type'],
+                'name' => $a['name'],
+            	'caption_src' => $a['caption'],
+                'caption' => _t($a['caption']),
+            	'info_src' => $a['info'] ? $a['info'] : '',
+                'info' => $a['info'] ? _t($a['info']) : '',
+                'required' => $a['required'] ? true : false,
+            	'unique' => $a['unique'] ? true : false,
+                'collapsed' => $a['collapsed'] ? true : false,
+                'html' => $a['html'],
+                'attrs' => $a['attrs'] ? unserialize($a['attrs']) : false,
+                'tr_attrs' => $a['attrs_tr'] ? unserialize($a['attrs_tr']) : false,
+                'attrs_wrapper' => $a['attrs_wrapper'] ? unserialize($a['attrs_wrapper']) : false,
+                'visible_for_levels' => $a['visible_for_levels'],
+            );
+
+            // if type is input set stop to process other attribnutes
+            if ('input_set' == $a['type']) {
+                $aForm['inputs'][$a['name']] = $aInput;
+                $aInputSets[] = $a['name'];
+                continue;
+            }
+
+            // default value
+            if (!empty($a['value']))
+                $aInput['value'] = isset(self::$TYPES_TRANSLATABLE[$aInput['type']]) ? _t($a['value']) : $a['value'];
+
+            if (!empty($a['values'])) {
+                $aInput['values_src'] = $a['values'];
+
+                if (0 === strncmp(BX_DATA_LISTS_KEY_PREFIX, $a['values'], 2)) {
+                    $aInput['values_list_name'] = trim($a['values'], BX_DATA_LISTS_KEY_PREFIX . ' ');
+                    $aInput['values'] = self::getDataItems(trim($a['values'], BX_DATA_LISTS_KEY_PREFIX . ' '), isset(self::$TYPES_SET[$aInput['type']]));
+                } else {
+                    $aInput['values'] = unserialize($a['values']);
+                }
+            }
+
+            $aInput['checked'] = $a['checked'] ? true : false;
+
+            // checker function options
+            if (!empty($a['checker_func'])) {
+                $aChecker = array ('func' => $a['checker_func']);
+                if ($a['checker_error'])
+                    $aChecker['error'] = _t($a['checker_error']);
+                if ($a['checker_params'])
+                    $aChecker['params'] = unserialize($a['checker_params']);
+                $aInput['checker'] = $aChecker;
+            }
+
+            // db processing options
+            if (!empty($a['db_pass'])) {
+                $aDb = array ('pass' => $a['db_pass']);
+                if ($a['db_params'])
+                    $a['params'] = unserialize($a['db_params']);
+                $aInput['db'] = $aDb;
+            }
+
+            $aForm['inputs'][$a['name']] = $aInput;
+        }
+
+        foreach ($aInputSets as $sName) {
+            $a = explode(',', $aInputs[$sName]['values']);
+            foreach ($a as $sInputName) {
+                if (!isset($aForm['inputs'][$sInputName]))
+                    continue;
+                $aForm['inputs'][$sName][] = $aForm['inputs'][$sInputName];
+                unset($aForm['inputs'][$sInputName]);
+            }
+        }
+
+        $aForm['override_class_name'] = $aObject['override_class_name'];
+        $aForm['override_class_file'] = $aObject['override_class_file'];
+
+        return $aForm;
+    }
+
+    static public function getDataItems($sKey, $isUseForSet = false, $sUseValues = BX_DATA_VALUES_DEFAULT)
+    {
+        if ($sUseValues != BX_DATA_VALUES_DEFAULT && $sUseValues != BX_DATA_VALUES_ADDITIONAL)
+            $sUseValues = BX_DATA_VALUES_DEFAULT;
+
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT `Value`, `LKey`, `LKey2` FROM `sys_form_pre_values` WHERE `Key` = ? ORDER BY `Order` ASC", $sKey);
+        $a = $oDb->getAll($sQuery);
+
+        $iMaxValue = 0;
+        if ($isUseForSet)
+            $iMaxValue = log(BX_DOL_INT_MAX, 2) + 1;
+
+        $aRet = array();
+        foreach ($a as $r) {
+            if ($isUseForSet && (!is_numeric($r['Value']) || $r['Value'] < 1 || $r['Value'] > $iMaxValue))
+                continue;
+            $aRet[$r['Value']] = BX_DATA_VALUES_ADDITIONAL == $sUseValues && !empty($r[BX_DATA_VALUES_ADDITIONAL]) ? _t($r[BX_DATA_VALUES_ADDITIONAL]) : _t($r[BX_DATA_VALUES_DEFAULT]);
+        }
+
+        return $aRet;
+    }
+
+    static public function fieldCheckUnique($sTable, $sField, $sValue)
+    {
+        return uriCheckUniq($sValue, $sTable, $sField);
+    } 
+
+    static public function fieldGetValue($sTable, $sField, $sFieldKey, $sFieldKeyValue)
+    {
+        return BxDolDb::getInstance()->getOne("SELECT `" . $sField . "` FROM `" . $sTable . "` WHERE `" . $sFieldKey . "`=:field_key LIMIT 1", array(
+            'field_key' => $sFieldKeyValue
+        ));
+    }
+
+    public function getFormInputs()
+    {
+        $sQuery = $this->prepare("SELECT * FROM `sys_form_inputs` WHERE `object` = ? ORDER BY `order` ASC", $this->_aObject['object']);
+        return $this->getAll($sQuery);
+    }
+
+}
+
+/** @} */

+ 406 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolLanguagesQuery.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
+ * @{
+ */
+
+class BxDolLanguagesQuery extends BxDolDb implements iBxDolSingleton
+{
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolLanguagesQuery();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    function getLanguageId($sName, $bFromCache = true)
+    {
+        return (int)$this->getLanguageField($sName, 'ID', $bFromCache);
+    }
+
+    function getLanguageName($iId, $bFromCache = true)
+    {
+        return $this->getLanguageFieldById($iId, 'Name', $bFromCache);
+    }
+
+    function getLanguageFlag($sName, $bFromCache = true)
+    {
+        return $this->getLanguageField($sName, 'Flag', $bFromCache);
+    }
+
+    function getLanguageDirection($sName, $bFromCache = true)
+    {
+        return $this->getLanguageField($sName, 'Direction', $bFromCache);
+    }
+    
+    protected function getLanguageField($sName, $sField, $bFromCache = true)
+    {
+        $sSql = $this->prepare("SELECT `" . $sField . "` FROM `sys_localization_languages` WHERE `Name`=? AND `Enabled`='1' LIMIT 1", $sName);
+
+        if($bFromCache)
+            return $this->fromCache('checkLangExists_' . $sName . '_' . $sField, 'getOne', $sSql);
+        else
+            return $this->getOne($sSql);
+    }
+
+    protected function getLanguageFieldById($iId, $sField, $bFromCache = true)
+    {
+        $sSql = $this->prepare("SELECT `" . $sField . "` FROM `sys_localization_languages` WHERE `ID`=? AND `Enabled`='1' LIMIT 1", $iId);
+
+        if($bFromCache)
+            return $this->fromCache('checkLangExists_' . $iId . '_' . $sField, 'getOne', $sSql);
+        else
+            return $this->getOne($sSql);
+    }
+
+    function getLanguages($bIdAsKey = false, $bActiveOnly = false)
+    {
+        return $this->getLanguagesWithKey($bIdAsKey ? 'ID' : 'Name', $bActiveOnly);
+    }
+
+    function getLanguagesWithKey($sKey = 'Name', $bActiveOnly = false)
+    {
+        $sSql = "SELECT * FROM `sys_localization_languages` WHERE 1 " . ($bActiveOnly ? " AND `Enabled`='1'" : "") . " ORDER BY `Title` ASC";
+        return $this->getPairs($sSql, $sKey, 'Title');
+    }
+
+    function getLanguagesBy($aParams, &$aItems, $bReturnCount = true)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sSelectClause = $sJoinClause = $sWhereClause = $sOrderClause = $sLimitClause = "";
+
+        if(!isset($aParams['order']) || empty($aParams['order']))
+           $sOrderClause = " `tl`.`ID` ASC ";
+
+        switch($aParams['type']) {
+            case 'by_id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                	'id' => $aParams['value']
+                );
+
+                $sWhereClause .= " AND `tl`.`ID`=:id";
+                break;
+
+            case 'by_name':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                	'name' => $aParams['value']
+                );
+
+                $sWhereClause .= " AND `tl`.`Name`=:name";
+                break;
+
+            case 'default':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                	'name' => getParam('lang_default')
+                );
+
+                $sWhereClause .= " AND `tl`.`Name`=:name";
+                break;
+
+            case 'active':
+                $sWhereClause = " AND `tl`.`Enabled`='1'";
+                break;
+
+            case 'all':
+                $sOrderClause = " `tl`.`Name` ASC ";
+                break;
+
+            case 'all_by_id':
+            	$aMethod['params'][1] = array(
+                	'id' => $aParams['value']
+                );
+
+                $sWhereClause .= " AND `tl`.`ID`=:id";
+                $sOrderClause = " `tl`.`Name` ASC ";
+                break;
+
+			case 'all_by_name':
+				$aMethod['params'][1] = array(
+                	'name' => $aParams['value']
+                );
+
+                $sWhereClause .= " AND `tl`.`Name`=:name";
+                $sOrderClause = " `tl`.`Name` ASC ";
+                break;
+
+            case 'all_key_id':
+                $aMethod['name'] = 'getAllWithKey';
+                $aMethod['params'][1] = 'id';
+
+                if(isset($aParams['language']) && (int)$aParams['language'] != 0) {
+                	$aMethod['params'][2] = array(
+	                	'id' => $aParams['language']
+	                );
+
+                    $sWhereClause .= " AND `tl`.`ID`=:id";
+                }
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT SQL_CALC_FOUND_ROWS
+                `tl`.`ID` AS `id`,
+                `tl`.`Name` AS `name`,
+                `tl`.`Flag` AS `flag`,
+                `tl`.`Title` AS `title`,
+                `tl`.`Enabled` AS `enabled`" . $sSelectClause . "
+            FROM `sys_localization_languages` AS `tl`" . $sJoinClause . "
+            WHERE 1" . $sWhereClause . "
+            ORDER BY" . $sOrderClause . $sLimitClause;
+        $aItems = call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+
+        if(!$bReturnCount)
+            return count($aItems) > 0;
+
+        return (int)$this->getOne("SELECT FOUND_ROWS()");
+    }
+
+    function getKeys()
+    {
+        $sSql = "SELECT `ID`, `IDCategory`, `Key` FROM `sys_localization_keys`";
+        return $this->getAllWithKey($sSql, 'ID');
+    }
+
+    function getKeysBy($aParams, &$aItems)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sSelectClause = $sJoinClause = $sWhereClause = $sGroupClause = $sOrderClause = $sLimitClause = "";
+
+        $sSelectClause = "`tk`.`ID` AS `id`, `tk`.`IDCategory` AS `category_id`, `tk`.`Key` AS `key`";
+
+        if(!isset($aParams['order']) || empty($aParams['order']))
+           $sOrderClause = " `tk`.`Key` ASC ";
+
+        switch($aParams['type']) {
+            case 'by_ids':
+                $sSelectClause .= ", `tc`.`Name` AS `category_name`, `ts`.`IDLanguage` AS `language_id`, `tl`.`Name` AS `language_name`, `tl`.`Title` AS `language_title`, `ts`.`String` AS `string`";
+                $sJoinClause = " LEFT JOIN `sys_localization_categories` AS `tc` ON `tk`.`IDCategory`=`tc`.`ID` LEFT JOIN `sys_localization_strings` AS `ts` ON `tk`.`ID`=`ts`.`IDKey` LEFT JOIN `sys_localization_languages` AS `tl` ON `ts`.`IDLanguage`=`tl`.`ID` ";
+                $sWhereClause .= " AND `tk`.`ID` IN ('" . implode("','", $aParams['value']) . "')";
+
+                if(isset($aParams['language']) && (int)$aParams['language'] != 0) {
+                	$aMethod['params'][1] = array(
+	                	'id' => $aParams['language']
+	                );
+
+                    $sWhereClause .= " AND `tl`.`ID`=:id";
+                }
+                break;
+
+            case 'by_name':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                	'key' => $aParams['value']
+                );
+
+                $sWhereClause = " AND `tk`.`Key`=:key ";
+                break;
+
+			case 'by_language_name_key_key':
+            	$aMethod['name'] = 'getAllWithKey';
+            	$aMethod['params'][1] = 'key';
+            	$aMethod['params'][2] = array(
+                	'name' => $aParams['value']
+                );
+
+                $sSelectClause .= ", `ts`.`String` AS `string` ";
+                $sJoinClause = " LEFT JOIN `sys_localization_strings` AS `ts` ON `tk`.`ID`=`ts`.`IDKey` LEFT JOIN `sys_localization_languages` AS `tl` ON `ts`.`IDLanguage`=`tl`.`ID` ";
+                $sWhereClause = " AND `tl`.`Name`=:name ";
+                break;
+
+            case 'by_language_id_key_key':
+            	$aMethod['name'] = 'getAllWithKey';
+            	$aMethod['params'][1] = 'key';
+            	$aMethod['params'][2] = array(
+                	'language_id' => (int)$aParams['value']
+                );
+
+                $sSelectClause .= ", `ts`.`String` AS `string` ";
+                $sJoinClause = " LEFT JOIN `sys_localization_strings` AS `ts` ON `tk`.`ID`=`ts`.`IDKey` ";
+                $sWhereClause = " AND `ts`.`IDLanguage`=:language_id ";
+                break;
+
+            case 'search':
+                $aMethod['name'] = 'getColumn';
+                $aMethod['params'][1] = array();
+
+                $sSelectClause = "DISTINCT `tk`.`ID` AS `id`";
+                $sJoinClause = " LEFT JOIN `sys_localization_categories` AS `tc` ON `tk`.`IDCategory`=`tc`.`ID` LEFT JOIN `sys_localization_strings` AS `ts` ON `tk`.`ID`=`ts`.`IDKey` LEFT JOIN `sys_localization_languages` AS `tl` ON `ts`.`IDLanguage`=`tl`.`ID` ";
+
+                if(isset($aParams['category']) && (int)$aParams['category'] != 0) {
+                	$aMethod['params'][1]['category_id'] = $aParams['category'];
+
+                    $sWhereClause .= " AND `tc`.`ID`=:category_id";
+                }
+
+                if(isset($aParams['language']) && (int)$aParams['language'] != 0) {
+                	$aMethod['params'][1]['language_id'] = $aParams['language'];
+
+                    $sWhereClause .= " AND `tl`.`ID`=:language_id";
+                }
+
+                if(isset($aParams['keyword']) && $aParams['keyword'] != '' && $aParams['keyword'] != _t('_adm_pgt_txt_keyword')) {
+                	$aMethod['params'][1]['key'] = '%' . $aParams['keyword'] . '%';
+                	$aMethod['params'][1]['string'] = '%' . $aParams['keyword'] . '%';
+
+                    $sWhereClause .= " AND (`tk`.`Key` LIKE :key OR `ts`.`String` LIKE :string)";
+                }
+
+                $iStart = 0;
+                if(isset($aParams['start']) && (int)$aParams['start'] != 0)
+                    $iStart = $aParams['start'];
+
+                $iLength = 20;
+                if(isset($aParams['length']) && (int)$aParams['length'] != 0)
+                    $iLength = $aParams['length'];
+
+				$aMethod['params'][1]['start'] = $iStart;
+				$aMethod['params'][1]['length'] = $iLength;
+                $sLimitClause .= " LIMIT :start, :length";
+                break;
+
+            case 'counter_by_category':
+                $aMethod['name'] = 'getPairs';
+                $aMethod['params'][1] = 'category_id';
+                $aMethod['params'][2] = 'counter';
+                $sSelectClause .= ", COUNT(*) AS `counter`";
+                $sGroupClause = "GROUP BY `tk`.`IDCategory`";
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . "
+            FROM `sys_localization_keys` AS `tk`" . $sJoinClause . "
+            WHERE 1" . $sWhereClause . " " . $sGroupClause . "
+            ORDER BY" . $sOrderClause . $sLimitClause;
+        $aItems = call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+
+        return $aItems !== false;
+    }
+
+    function getCategories()
+    {
+        $sSql = "SELECT `ID`, `Name` FROM `sys_localization_categories` ORDER BY `Name`";
+        return $this->getPairs($sSql, 'ID', 'Name');
+    }
+
+    function getCategoriesBy($aParams, &$aItems, $bReturnCount = true)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sSelectClause = $sJoinClause = $sWhereClause = $sOrderClause = $sLimitClause = "";
+
+        $sSelectClause = "`tc`.`ID` AS `id`, `tc`.`Name` AS `name`";
+
+        if(!isset($aParams['order']) || empty($aParams['order']))
+           $sOrderClause = " `tc`.`ID` ASC ";
+
+        switch($aParams['type']) {
+            case 'id_by_name':
+                $aMethod['name'] = 'getOne';
+                $aMethod['params'][1] = array(
+                	'name' => $aParams['value']
+                );
+
+                $sSelectClause = "`tc`.`ID` AS `id`";
+                $sWhereClause = " AND `tc`.`Name`=:name";
+                break;
+            case 'by_name':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                	'name' => $aParams['value']
+                );
+
+                $sWhereClause = " AND `tc`.`Name`=:name";
+                break;
+
+            case 'all':
+                $sOrderClause = " `tc`.`Name` ASC ";
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT " . ($bReturnCount ? "SQL_CALC_FOUND_ROWS" : "") . "
+                " . $sSelectClause . "
+            FROM `sys_localization_categories` AS `tc`" . $sJoinClause . "
+            WHERE 1" . $sWhereClause . "
+            ORDER BY" . $sOrderClause . $sLimitClause;
+        $aItems = call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+
+        if(!$bReturnCount)
+            return !empty($aItems);
+
+        return (int)$this->getOne("SELECT FOUND_ROWS()");
+    }
+
+    function getStringsBy($aParams, &$aItems, $bReturnCount = true)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sSelectClause = $sJoinClause = $sWhereClause = $sOrderClause = $sLimitClause = "";
+
+        if(!isset($aParams['order']) || empty($aParams['order']))
+           $sOrderClause = "`ts`.`IDKey` ASC";
+
+        switch($aParams['type']) {
+            case 'by_key_language_id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                	'key' => $aParams['key'],
+                	'language_id' => $aParams['language_id']
+                );
+
+                $sJoinClause = "LEFT JOIN `sys_localization_keys` AS `tk` ON `ts`.`IDKey`=`tk`.`ID`";
+                $sWhereClause .= "AND `tk`.`Key`=:key AND `ts`.`IDLanguage`=:language_id";
+                break;
+
+            case 'all_by_key_key_language_id':
+                $aMethod['name'] = 'getAllWithKey';
+                $aMethod['params'][1] = 'language_id';
+                $aMethod['params'][2] = array(
+                	'key' => $aParams['value']
+                );
+
+                $sJoinClause = "LEFT JOIN `sys_localization_keys` AS `tk` ON `ts`.`IDKey`=`tk`.`ID`";
+                $sWhereClause .= "AND `tk`.`Key`=:key";
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT SQL_CALC_FOUND_ROWS
+                `ts`.`IDKey` AS `key_id`,
+                `ts`.`IDLanguage` AS `language_id`,
+                `ts`.`String` AS `string` " . $sSelectClause . "
+            FROM `sys_localization_strings` AS `ts` " . $sJoinClause . "
+            WHERE 1 " . $sWhereClause . "
+            ORDER BY " . $sOrderClause . " " . $sLimitClause;
+        $aItems = call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+
+        if(!$bReturnCount)
+            return count($aItems) > 0;
+
+        return (int)$this->getOne("SELECT FOUND_ROWS()");
+    }
+}
+
+/** @} */

+ 892 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolMetatags.php

@@ -0,0 +1,892 @@
+<?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_METATAGS_KEYWORDS_MAX', 9);
+define('BX_METATAGS_MENTIONS_MAX', 9);
+
+// TODO: integration with notifications, when smbd is \@mentioned
+
+/**
+ * Meta-tags for different content. It can handle \#keywords, \@mentions, locations and meta image for the content.
+ *
+ * Keywords are parsed after content add and edit and added to the database, 
+ * later when content is displayed \#keywords are highlighted as links to the search page with all content with this keyword, 
+ * also keywords are displayed as meta info in page header.
+ * 
+ * Location info upon content adding/editing is displayed as custom field, it detects current user location and attach location as hidden form fields.
+ * When content is displayed location can be shown as clickable links to search page where results are other items from same location,
+ * also location is displayed as meta info in page header as latutude and longitude.
+ * 
+ * Content image can be added as meta info using this class as well.
+ *
+ *
+ *
+ * @section metatags_create Add new meta-tags object.
+ *
+ * To add new meta-tags object insert new record into `sys_objects_metatags` table:
+ * - object: name of the meta-tags object, in the format: vendor prefix, underscore, module prefix, underscore, internal identifier or nothing
+ * - table_keywords: table name to store keywords, leave empty to disable keywords
+ * - table_locations: table name to store locations, leave empty to disable locations
+ * - table_mentions: table name to store mentions, leave empty to disable mentions
+ * - override_class_name: class name to override  
+ * - override_class_file: class file path to override
+ *
+ * To simplify operations with metatags in modules which are derived from some 'base' modules, specify it in module config class in 'CNF' array: 
+ * - FIELDS_WITH_KEYWORDS: array or comma separated list of field names with keywords support, or 'auto' to automatically use all 'textarea' fields
+ * - OBJECT_METATAGS: metatags object name
+ *
+ *
+ *
+ * @section metatags_locations Location
+ *
+ * Upon content 'add' and 'edit' form submit, call BxDolMetatags::locationsAddFromForm
+ * @code
+ *   $oMetatags = BxDolMetatags::getObjectInstance('object_name');
+ *   $oMetatags->locationsAddFromForm($iContentId);
+ * @endcode
+ *
+ * Upon 'edit' form display call BxDolMetatags::locationGet to fill-in location form input, for example:
+ * @code
+ *   $oMetatags = BxDolMetatags::getObjectInstance('object_name');
+ *   $aSpecificValues = $oMetatags->locationGet($iContentId);
+ *   $oForm->initChecker($aContentInfo, $aSpecificValues);
+ * @endcode
+ *
+ * To display location call BxDolMetatags::locationsString:
+ * @code
+ *   $oMetatags = BxDolMetatags::getObjectInstance('object_name');
+ *   echo $oMetatags->locationsString($iContentId)
+ * @endcode
+ *
+ * Upon page display call BxDolMetatags::addPageMetaInfo to add location (and all other, including content image) meta information to the page header:
+ * @code
+ *   $o = BxDolMetatags::getObjectInstance('object_name');
+ *   $o->addPageMetaInfo($iContentId, array('id' => $iFileId, 'object' => 'storage_object_name'));
+ * @endcode
+ * 
+ * Form object must have location field, usual name for this field is 'location' (this name is used as $sPrefix in some functions), type is 'custom', 
+ * also form object must be derived from BxBaseModGeneralFormEntry class, which has custom field declaration.
+ *
+ *
+ *
+ * @section metatags_keywords \#keyword
+ *
+ * Upon content 'add' and 'edit' form submit, call BxDolMetatags::locationsAddFromForm
+ * @code
+ *   $oMetatags = BxDolMetatags::getObjectInstance('object_name');
+ *   $oMetatags->keywordsAdd($iContentId, $aContentInfo['text_field_with_hash_tags']);
+ * @endcode
+ *
+ * Before displaying the text with hashtags call BxDolMetatags::keywordsParse to highlight keywords:
+ * @code
+ *   $oMetatags = BxDolMetatags::getObjectInstance('object_name');
+ *   $sText = $oMetatags->keywordsParse($iContentId, $sText);
+ * @endcode
+ *
+ * Upon page display call BxDolMetatags::addPageMetaInfo to add keywords (and all other, including content image) meta information to the page header.
+ *
+ * To be able to search by tags metatgs object must be specified in *SearchResult class as 'object_metatags' in 'aCurrent' array.
+ *
+ *
+ *
+ * @section metatags_mention \@mention
+ * TODO:
+ *
+ *
+ *
+ * @section metatags_delete_content Content deletion
+ * 
+ * When content is deleted associated meta data can be deleted by calling BxDolMetatags::onDeleteContent:
+ * @code
+ *   $oMetatags = BxDolMetatags::getObjectInstance('object_name');
+ *   $oMetatags->onDeleteContent($iContentId);
+ * @endcode
+ *
+ */
+class BxDolMetatags extends BxDolFactory implements iBxDolFactoryObject
+{
+    protected $_sObject;
+    protected $_aObject;
+    protected $_oQuery;
+    protected $_aMetas = array ();
+
+    /**
+     * Constructor
+     * @param $aObject array of metags object options
+     */
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+
+        $this->_sObject = $aObject['object'];
+        $this->_aObject = $aObject;
+        
+        $a = array ('keywords', 'locations', 'mentions', 'pictures');
+        foreach ($a as $sMeta) {
+            if (empty($this->_aObject['table_' . $sMeta]))
+                continue;
+            $this->_aMetas[] = $sMeta;
+        }
+
+        $this->_oQuery = new BxDolMetatagsQuery($this->_aObject);
+    }
+
+    /**
+     * Get metatags object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    static public function getObjectInstance($sObject)
+    {
+        if (isset($GLOBALS['bxDolClasses']['BxDolMetatags!'.$sObject])) 
+            return $GLOBALS['bxDolClasses']['BxDolMetatags!'.$sObject];
+
+        $aObject = BxDolMetatagsQuery::getMetatagsObject($sObject);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        $sClass = 'BxTemplMetatags';
+        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);
+
+        return ($GLOBALS['bxDolClasses']['BxDolMetatags!'.$sObject] = $o);
+    }
+
+    /**
+     * Add all available meta tags to the head section 
+     * @return number of successfully added metas
+     */
+    public function addPageMetaInfo($iId, $mixedImage = false)
+    {
+        $i = 0;
+        foreach ($this->_aMetas as $sMeta) {
+            $sFunc = $sMeta . 'AddMeta';
+            $i += $this->$sFunc($iId);
+        }
+
+        if ($mixedImage && is_array($mixedImage)) {
+            
+            if (!empty($mixedImage['object']))
+                $o = BxDolStorage::getObjectInstance($mixedImage['object']);
+            elseif (!empty($mixedImage['transcoder']))
+                $o = BxDolTranscoder::getObjectInstance($mixedImage['transcoder']);
+
+            $mixedImage = $o ? $o->getFileUrlById($mixedImage['id']) : false;
+        }
+
+        if ($mixedImage) 
+            BxDolTemplate::getInstance()->addPageMetaImage($mixedImage);
+
+        return $i;
+    }
+
+    /**
+     * Perform @see keywordsParse @see mentionsParse and maybe other
+     * @param $iId content id
+     * @param $s string
+     * @return modified string
+     */    
+    public function metaParse($iId, $s)
+    {
+        foreach ($this->_aMetas as $sMeta) {
+            $sFunc = $sMeta . 'Parse';
+            if (method_exists($this, $sFunc))
+                $s = $this->$sFunc($iId, $s);
+        }
+        return $s;
+    }
+
+    /**
+     * Perform @see keywordsAddAuto @see mentionsAddAuto and maybe other
+     * @param $iId content id
+     * @param $aContentInfo content info array
+     * @param $CNF module config array
+     * @param $sFormDisplay form display object
+     * @return number of founded keywords, mentions, etc
+     */
+    public function metaAddAuto($iId, $aContentInfo, $CNF, $sFormDisplay) 
+    {
+        $i = 0;
+        foreach ($this->_aMetas as $sMeta) {
+            $sFunc = $sMeta . 'AddAuto';
+            if (method_exists($this, $sFunc))
+                $i += $this->$sFunc($iId, $aContentInfo, $CNF, $sFormDisplay);
+        }
+        return $i;
+    }
+
+    /**
+     * Perform @see keywordsAdd @see mentionsAdd and maybe other
+     * @param $iId content id
+     * @param $s string
+     * @return number of added keywords, mentions, etc
+     */
+    public function metaAdd($iId, $s)
+    {
+        $i = 0;
+        foreach ($this->_aMetas as $sMeta) {
+            if ('locations' == $sMeta)
+                continue;
+            $sFunc = $sMeta . 'Add';
+            if (method_exists($this, $sFunc))
+                $i += $this->$sFunc($iId, $s);
+        }
+        return $i;
+    }
+    
+    /**
+     * Checks if keywords enabled for current metatags object
+     */
+    public function keywordsIsEnabled() 
+    {
+        return empty($this->_aObject['table_keywords']) ? false : true;
+    }
+
+    /**
+     * This function is specifically for formatting "Photo Camera" string, when "Photo Camera" is used as image hashtag
+     */
+    static public function keywordsCameraModel($aExif) 
+    {
+        if (!isset($aExif['Make']))
+            return '';
+
+        $sMake = trim($aExif['Make']);
+        if ($sMake && isset($aExif['Model'])) {
+            $sModel = trim($aExif['Model']);
+            if (0 === mb_strpos($sModel, $sMake))
+                $sModel = mb_substr($sModel, mb_strlen($sMake));
+        }
+
+        return $sMake . (empty($sModel) ? '' : ' ' . trim($sModel));
+    }
+
+    /**
+     * Add \#keywords from the string
+     * @param $iId content id
+     * @param $s string with \#keywords
+     * @return number of found keywords
+     */
+    public function keywordsAdd($iId, $s) 
+    {
+        return $this->_metaAdd($iId, ' ' . strip_tags($s), '/[\s]\#([\pL\pN_]+)/u', 'keywordsDelete', 'keywordsAdd', BX_METATAGS_KEYWORDS_MAX, 'keyword');
+    }
+
+    /**
+     * Add keyword from the whole string
+     * @param $iId content id
+     * @param $s keyword 
+     * @return number of added keywords, should be 1
+     */
+    public function keywordsAddOne($iId, $s, $bDeletePreviousKeywords = true)
+    {
+        if ($bDeletePreviousKeywords)
+            $this->_oQuery->keywordsDelete($iId);
+
+        return $this->_oQuery->keywordsAdd($iId, array($s));
+    }
+
+    /**
+     * Add \#keywords from the content fields
+     * @param $iId content id
+     * @param $aContentInfo content info array
+     * @param $CNF module config array
+     * @param $sFormDisplay form display object
+     * @return number of found keywords
+     */
+    public function keywordsAddAuto($iId, $aContentInfo, $CNF, $sFormDisplay) 
+    {
+        $aFields = $this->metaFields($aContentInfo, $CNF, $sFormDisplay); 
+        $sTextWithKeywords = '';
+        foreach ($aFields as $sField)
+            $sTextWithKeywords .= "\n" . $aContentInfo[$sField];
+
+        return $this->keywordsAdd($iId, $sTextWithKeywords);
+    }
+
+    /**
+     * Get field names which are subject to parse keywords
+     */
+    public function metaFields($aContentInfo, $CNF, $sFormDisplay, $bHtmlOnly = false)
+    {
+        $aFields = array();
+        if (empty($CNF['FIELDS_WITH_KEYWORDS'])) {
+            return array();
+        }
+        elseif (is_string($CNF['FIELDS_WITH_KEYWORDS']) && 'auto' == $CNF['FIELDS_WITH_KEYWORDS']) {
+            if (!($oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $sFormDisplay)))
+                return array();
+
+            foreach ($oForm->aInputs as $k => $a) {
+                if ('textarea' == $a['type'] && (!$bHtmlOnly || ($bHtmlOnly && $a['html'])))
+                    $aFields[] = $a['name'];
+            }
+        } 
+        elseif (is_array($CNF['FIELDS_WITH_KEYWORDS'])) {
+            $aFields = $CNF['FIELDS_WITH_KEYWORDS'];
+        } 
+        elseif (is_string($CNF['FIELDS_WITH_KEYWORDS'])) {
+            $aFields = explode(',', $CNF['FIELDS_WITH_KEYWORDS']);
+        }
+
+        return $aFields;
+    }
+
+    /**
+     * Add links to the \#keywords in the string
+     * @param $iId content id
+     * @param $s string with \#keywords
+     * @return modified string where all \#keywords are transformed to links with rel="tag" attribute
+     */
+    public function keywordsParse($iId, $s) 
+    {   
+        $a = $this->keywordsGet($iId);
+        if (empty($a))
+            return $s;
+
+        foreach ($a as $sKeyword)
+            $s = str_ireplace('#' . $sKeyword, '<a rel="tag" href="' . BX_DOL_URL_ROOT . 'searchKeyword.php?type=keyword&keyword=' . rawurlencode($sKeyword) . '"><s>#</s><b>' . $sKeyword . '</b></a>', $s);
+
+        return $s;
+    }
+
+    /**
+     * Add link to the provided keyword
+     * @param $iId content id
+     * @param $s keyword 
+     * @return modified string where provided keyword is transformed to link with rel="tag" attribute
+     */
+    public function keywordsParseOne($iId, $s) 
+    {   
+        $a = $this->keywordsGet($iId);
+        if (empty($a))
+            return $s;
+    
+        foreach ($a as $sKeyword)
+            if (0 === strcasecmp(mb_strtolower($s), mb_strtolower($sKeyword)))
+                $s = '<a rel="tag" href="' . BX_DOL_URL_ROOT . 'searchKeyword.php?type=keyword&keyword=' . rawurlencode($sKeyword) . '">' . $sKeyword . '</a>';
+
+        return $s;
+    }
+
+    /**
+     * Add keywords meta info to the head section
+     * @param $iId content id
+     */
+    protected function keywordsAddMeta($iId)
+    {
+        BxDolTemplate::getInstance()->addPageKeywords($this->keywordsGet($iId));
+    }
+
+    /**
+     * Get list of keywords associated with the content
+     * @return array of keywords
+     */
+    public function keywordsGet($iId)
+    {
+        return $this->_oQuery->keywordsGet($iId);
+    }
+
+    /**
+     * Set condition for search results object for meta keyword
+     * @param $oSearchResult search results object
+     * @param $sKeyword keyword
+     */
+    public function keywordsSetSearchCondition($oSearchResult, $sKeyword, $iCmtsSystemId = 0)
+    {
+        if (!$this->keywordsIsEnabled())
+            return;
+
+        if ('sys_cmts' == $oSearchResult->aCurrent['object_metatags']) {
+            $this->keywordsSetSearchConditionCmts($oSearchResult, $sKeyword, $iCmtsSystemId);
+            return;
+        }
+
+        $oSearchResult->aCurrent['restriction']['meta_keyword'] = array(
+            'value' => $sKeyword,
+            'field' => 'keyword',
+            'operator' => '=',
+            'table' => $this->_aObject['table_keywords'],
+        );
+
+        $oSearchResult->aCurrent['join']['meta_keyword'] = array(
+            'type' => 'INNER',
+            'table' => $this->_aObject['table_keywords'],
+            'mainField' => $oSearchResult->aCurrent['ident'],
+            'mainTable' => !empty($oSearchResult->aCurrent['tableSearch']) ? $oSearchResult->aCurrent['tableSearch'] : $oSearchResult->aCurrent['table'],
+            'onField' => 'object_id',
+            'joinFields' => array(),
+        );
+    }
+
+    public function keywordsSetSearchConditionCmts($oSearchResult, $sKeyword, $iCmtsSystemId = 0)
+    {
+        if (!$this->keywordsIsEnabled())
+            return;
+
+        if ($iCmtsSystemId) {
+            $oSearchResult->aCurrent['restriction']['meta_keyword_cmts_system'] = array(
+                'value' => $iCmtsSystemId,
+                'field' => 'system_id',
+                'operator' => '=',
+                'table' => 'sys_cmts_ids',
+            );
+        }
+        
+        $oSearchResult->aCurrent['restriction']['meta_keyword'] = array(
+            'value' => $sKeyword,
+            'field' => 'keyword',
+            'operator' => '=',
+            'table' => $this->_aObject['table_keywords'],
+        );
+
+        $oSearchResult->aCurrent['join']['meta_keyword_cmts'] = array(
+            'type' => 'INNER',
+            'table' => 'sys_cmts_ids',
+            'mainField' => $oSearchResult->aCurrent['ident'],
+            'mainTable' => !empty($oSearchResult->aCurrent['tableSearch']) ? $oSearchResult->aCurrent['tableSearch'] : $oSearchResult->aCurrent['table'],
+            'onField' => 'cmt_id',
+            'joinFields' => array(),
+        );
+        
+        $oSearchResult->aCurrent['join']['meta_keyword'] = array(
+            'type' => 'INNER',
+            'table' => $this->_aObject['table_keywords'],
+            'mainField' => 'id',
+            'mainTable' => 'sys_cmts_ids',
+            'onField' => 'object_id',
+            'joinFields' => array(),
+        );
+    }
+    
+	/**
+     * Get part of SQL query for meta keyword
+     * @param $sContentTable content table or alias
+     * @param $sContentField content table field or field alias
+     * @param $sKeyword keyword
+     */
+    public function keywordsGetAsSQLPart($sContentTable, $sContentField, $sKeyword)
+    {
+        if (empty($this->_aObject['table_keywords']))
+            return array();
+
+        return call_user_func_array(array($this->_oQuery, 'keywordsGetSQLParts'), func_get_args());
+    }
+
+    public function keywordsPopularList($iLimit)
+    {
+        return $this->_oQuery->keywordsPopularList($iLimit);
+    }
+
+
+    /**
+     * Checks if locations enabled for current metatags object
+     */
+    public function locationsIsEnabled() 
+    {
+        return empty($this->_aObject['table_locations']) ? false : true;
+    }
+
+    /**
+     * Add location for the content
+     * @param $iId content id
+     * @param $sLatitude latitude
+     * @param $sLongitude longitude
+     * @param $sCountryCode optional 2 letters country code (ISO 3166-1)
+     * @param $sState optional state/province/territory name
+     * @param $sCity optional city name
+     * @param $sZip optional ZIP/postcode
+     * @param $sStreet optional street name
+     * @param $sStreetNumber optional street number
+     * @return true if location was added, or false otherwise
+     */
+    public function locationsAdd($iId, $sLatitude, $sLongitude, $sCountryCode, $sState, $sCity, $sZip = '', $sStreet = '', $sStreetNumber = '') 
+    {
+        return $this->_oQuery->locationsAdd($iId, $sLatitude, $sLongitude, $sCountryCode, $sState, $sCity, $sZip, $sStreet, $sStreetNumber);
+    }
+
+	/**
+     * Retrieve location for the content from POST data
+     * @param $sPrefix field prefix for POST data, or empty -  if no prefix
+     * @param $oForm form to use to get POST data, or null - then new form instance will be created
+     */
+    public static function locationsRetrieveFromForm($sPrefix = '', $oForm = null)
+    {
+        if ($sPrefix)
+            $sPrefix .= '_';
+
+        if (!$oForm)
+            $oForm = new BxDolForm(array(), false);
+
+        return array(
+            $oForm->getCleanValue($sPrefix.'lat'), 
+            $oForm->getCleanValue($sPrefix.'lng'), 
+            $oForm->getCleanValue($sPrefix.'country'), 
+            $oForm->getCleanValue($sPrefix.'state'), 
+            $oForm->getCleanValue($sPrefix.'city'), 
+            $oForm->getCleanValue($sPrefix.'zip'), 
+            $oForm->getCleanValue($sPrefix.'street'), 
+            $oForm->getCleanValue($sPrefix.'street_number')
+        );
+    }
+
+    /**
+     * Parse google's formatted address components into array with the followin indexes:
+     * lat, lng, country, state, city, zip, street, street_number
+     */ 
+    public static function locationsParseAddressComponents($aAdress, $sPrefix = '')
+    {
+        if (!isset($aAdress['address_components']))
+            return false;
+        if (!isset($aAdress['geometry']['location']))
+            return false;
+
+        $aRet = array(
+            $sPrefix . 'lat' => $aAdress['geometry']['location']['lat'],
+            $sPrefix . 'lng' => $aAdress['geometry']['location']['lng'],
+        );
+
+        $aMap = array(
+            $sPrefix . 'city' => 'locality',
+            $sPrefix . 'state' => 'administrative_area_level_1',
+            $sPrefix . 'country' => 'country',
+            $sPrefix . 'zip' => 'postal_code',
+            $sPrefix . 'street' => 'route',
+            $sPrefix . 'street_number' => 'street_number'        
+        );
+
+        $aAdressComponents = $aAdress['address_components'];
+        
+        foreach ($aAdressComponents as $r) {
+            if (!isset($r['types']))
+                continue;
+            foreach ($aMap as $sKey => $sName) {
+                
+                if ('locality' == $sName && !in_array($sName, $r['types']))
+                    $sName = 'postal_town';
+                
+                if ('administrative_area_level_1' == $sName && !in_array($sName, $r['types']))
+                    $sName = 'administrative_area_level_2';
+
+                if (in_array($sName, $r['types'])) {
+                    $sIndex = 'route' == $sName || 'locality' == $sName ? 'long_name' : 'short_name';
+                    $aRet[$sKey] = $r[$sIndex];
+                }
+
+                if (in_array('locality', $r['types']))
+                    $aRet[$sPrefix . 'city'] = $r['short_name'];
+                else if (in_array('country', $r['types']))
+                    $aRet[$sPrefix . 'country'] = $r['short_name'];
+            }
+        }
+        
+        return $aRet;
+    }
+    
+    /**
+     * Add location for the content from POST data
+     * @param $iId content id
+     * @param $sPrefix field prefix for POST data, or empty -  if no prefix
+     * @param $oForm form to use to get POST data, or null - then new form instance will be created
+     */
+    public function locationsAddFromForm($iId, $sPrefix = '', $oForm = null)
+    {
+        if (!$this->locationsIsEnabled())
+            return;
+
+        call_user_func_array(array($this, 'locationsAdd'), array_merge(array($iId), self::locationsRetrieveFromForm($sPrefix, $oForm)));
+    }
+
+    /**
+     * Get locations string with links
+     * @param $iId content id
+     * @return string with links to country and city
+     */
+    public function locationsString($iId, $bHTML = true, $aParams = array())
+    {
+        bx_import('BxDolForm');
+        $aCountries = BxDolFormQuery::getDataItems('Country');
+        $aLocation = $this->locationGet($iId);
+        if(!$aLocation || !$aLocation['country'] || !isset($aCountries[$aLocation['country']]))
+            return '';
+
+        $s = '';
+        
+        $sCountryUrl = '<a href="' . BX_DOL_URL_ROOT . 'searchKeyword.php?type=location_country&keyword=' . $aLocation['country'] . '">' . $aCountries[$aLocation['country']] . '</a>';
+            
+        if(empty($aLocation['state']) || empty($aLocation['city']) || (isset($aParams['country_only']) && $aParams['country_only']))
+            $s = _t('_sys_location_country', $sCountryUrl);
+
+        if (!$s) {
+            $sCityUrl = '<a href="' . BX_DOL_URL_ROOT . 'searchKeyword.php?type=location_country_city&keyword=' . $aLocation['country'] . '&state=' . rawurlencode($aLocation['state']) . '&city=' . rawurlencode($aLocation['city']) . '">' . $aLocation['city'] . '</a>';
+            $sStateUrl = '<a href="' . BX_DOL_URL_ROOT . 'searchKeyword.php?type=location_country_state&keyword=' . $aLocation['country'] . '&state=' . rawurlencode($aLocation['state']) . '">' . $aLocation['state'] . '</a>';
+
+            if (empty($aLocation['street']) || empty($aLocation['street_number']) || (isset($aParams['country_city_only']) && $aParams['country_city_only']))
+        	    $s = _t('_sys_location_country_city', $sCountryUrl, $sStateUrl, $sCityUrl);
+            elseif (!isset($aParams['country_city_only']) || !$aParams['country_city_only'])
+                $s = _t('_sys_location_country_city_street', $sCountryUrl, $sStateUrl, $sCityUrl, $aLocation['street'], $aLocation['street_number']);
+        }
+
+        return $bHTML ? $s : trim(strip_tags($s));
+    }
+
+    /**
+     * Add keywords meta info to the head section
+     * @param $iId content id
+     */
+    protected function locationsAddMeta($iId) 
+    {
+        $aLocation = $this->locationGet($iId);
+        if (!empty($aLocation['lat']) && !empty($aLocation['lng']) && !empty($aLocation['country']))
+            BxDolTemplate::getInstance()->addPageMetaLocation($aLocation['lat'], $aLocation['lng'], $aLocation['country']);
+    }
+
+    /**
+     * Set condition for search results object for meta locations
+     * @param $oSearchResult search results object
+     * @param $sCountry country and other location info
+     */
+    public function locationsSetSearchCondition($oSearchResult, $sCountry, $sState = false, $sCity = false, $sZip = false)
+    {
+        if (empty($this->_aObject['table_locations'])) {
+            $oSearchResult->aCurrent['restriction']['meta_location'] = array(
+                'operator' => 'nothing',
+                'field' => 'nofield',
+                'value' => 'novalue',
+            );
+            return;
+        }
+
+        $a = array('country' => 'sCountry', 'state' => 'sState', 'city' => 'sCity', 'zip' => 'sZip');
+        foreach ($a as $sIndex => $sVar) {
+            if (!$$sVar)
+                continue;
+
+            $oSearchResult->aCurrent['restriction']['meta_location_' . $sIndex] = array(
+                'value' => $$sVar,
+                'field' => $sIndex,
+                'operator' => '=',
+                'table' => $this->_aObject['table_locations'],
+            );
+        }
+
+        $oSearchResult->aCurrent['join']['meta_location'] = array(
+            'type' => 'INNER',
+            'table' => $this->_aObject['table_locations'],
+            'mainField' => $oSearchResult->aCurrent['ident'],
+            'mainTable' => !empty($oSearchResult->aCurrent['tableSearch']) ? $oSearchResult->aCurrent['tableSearch'] : $oSearchResult->aCurrent['table'],
+            'onField' => 'object_id',
+            'joinFields' => array(),
+        );
+    }
+
+    /**
+     * Get part of SQL query for meta locations
+     * @param $sContentTable content table or alias
+     * @param $sContentField content table field or field alias
+     * @param $sCountry country and other location info
+     */
+    public function locationsGetAsSQLPart($sContentTable, $sContentField, $sCountry = false, $sState = false, $sCity = false, $sZip = false, $aBounds = array())
+    {
+        if (empty($this->_aObject['table_locations']))
+            return array();
+
+        return call_user_func_array(array($this->_oQuery, 'locationsGetSQLParts'), func_get_args());
+    }
+
+    /**
+     * Get location
+     * @param $iId content id
+     * @param $sPrefix field prefix for returning data array
+     * @return location array with the following keys (when no prefix specified): object_id, lat, lng, country, state, city, zip, street, street_number
+     */
+    public function locationGet($iId, $sPrefix = '')
+    {
+        if (!$this->locationsIsEnabled())
+            return;
+
+        $a = $this->_oQuery->locationGet($iId);
+        if (!$sPrefix)
+            return $a;
+
+        $aRet = array();
+        foreach ($a as $sKey => $sVal)
+            $aRet[$sPrefix . '_' . $sKey] = $sVal;
+        return $aRet;
+    }
+
+    /**
+     * Checks if mentions enabled for current metatags object
+     */
+    public function mentionsIsEnabled() 
+    {
+        return empty($this->_aObject['table_mentions']) ? false : true;
+    }
+
+    /**
+     * Add \@mentions from the string (most probably \@mentions will be some sort of links already, so parsing may have to look for smth like <a data-mention="bx_persons|123">mention name</a> instead of \@mention, since there is no usernames for profiles modules and name could contain spaces and othr characters)
+     * @param $iId content id
+     * @param $s string with \@mentions
+     * @return number of found mentions 
+     */
+    public function mentionsAdd($iId, $s) 
+    {
+        return $this->_metaAdd($iId, $s, '/data\-profile\-id="(\d+)"/u', 'mentionsDelete', 'mentionsAdd', BX_METATAGS_MENTIONS_MAX, 'mention');
+    }
+
+    /**
+     * Add \@mentions from the content of form fields
+     * @param $iId content id
+     * @param $aContentInfo content info array
+     * @param $CNF module config array
+     * @param $sFormDisplay form display object
+     * @return number of found keywords
+     */
+    public function mentionsAddAuto($iId, $aContentInfo, $CNF, $sFormDisplay) 
+    {
+        $aFields = $this->metaFields($aContentInfo, $CNF, $sFormDisplay, true); 
+        $sTextWithKeywords = '';
+        foreach ($aFields as $sField)
+            $sTextWithKeywords .= "\n" . $aContentInfo[$sField];
+
+        return $this->mentionsAdd($iId, $sTextWithKeywords);
+    }
+    
+    /**
+     * Add links to the \@mentions in the string (actual tranformation may have to be performed with ready links like <a data-mention="bx_persons|123">mention name</a>)
+     * @param $iId content id
+     * @param $s string with \@mentions
+     * @return modified string where all \@mentions are transformed to links
+     */
+    public function mentionsParse($iId, $s) 
+    {
+        return $s;
+    }
+
+    /**
+     * No mentions meta info in the head section
+     */
+    protected function mentionsAddMeta($iId) 
+    {
+        return 0;
+    }
+
+    /**
+     * Set condition for search results object for mentions
+     * @param $oSearchResult search results object
+     * @param $sMention smbd
+     */
+    public function mentionsSetSearchCondition($oSearchResult, $iProfileId, $iCmtsSystemId = 0)
+    {
+        if (!$this->mentionsIsEnabled())
+            return;
+
+        if ('sys_cmts' == $oSearchResult->aCurrent['object_metatags']) {
+            $this->mentionsSetSearchConditionCmts($oSearchResult, $iProfileId, $iCmtsSystemId);
+            return;
+        }
+
+        $oSearchResult->aCurrent['restriction']['meta_mentions'] = array(
+            'value' => $iProfileId,
+            'field' => 'profile_id',
+            'operator' => '=',
+            'table' => $this->_aObject['table_mentions'],
+        );
+
+        $oSearchResult->aCurrent['join']['meta_mentions'] = array(
+            'type' => 'INNER',
+            'table' => $this->_aObject['table_mentions'],
+            'mainField' => $oSearchResult->aCurrent['ident'],
+            'mainTable' => !empty($oSearchResult->aCurrent['tableSearch']) ? $oSearchResult->aCurrent['tableSearch'] : $oSearchResult->aCurrent['table'],
+            'onField' => 'object_id',
+            'joinFields' => array(),
+        );
+    }
+
+    public function mentionsSetSearchConditionCmts($oSearchResult, $iProfileId, $iCmtsSystemId = 0)
+    {
+        if (!$this->keywordsIsEnabled())
+            return;
+
+        if ($iCmtsSystemId) {
+            $oSearchResult->aCurrent['restriction']['meta_mentions_cmts_system'] = array(
+                'value' => $iCmtsSystemId,
+                'field' => 'system_id',
+                'operator' => '=',
+                'table' => 'sys_cmts_ids',
+            );
+        }
+        
+        $oSearchResult->aCurrent['restriction']['meta_mentions'] = array(
+            'value' => $iProfileId,
+            'field' => 'profile_id',
+            'operator' => '=',
+            'table' => $this->_aObject['table_mentions'],
+        );
+
+        $oSearchResult->aCurrent['join']['meta_mentions_cmts'] = array(
+            'type' => 'INNER',
+            'table' => 'sys_cmts_ids',
+            'mainField' => $oSearchResult->aCurrent['ident'],
+            'mainTable' => !empty($oSearchResult->aCurrent['tableSearch']) ? $oSearchResult->aCurrent['tableSearch'] : $oSearchResult->aCurrent['table'],
+            'onField' => 'cmt_id',
+            'joinFields' => array(),
+        );
+        
+        $oSearchResult->aCurrent['join']['meta_mentions'] = array(
+            'type' => 'INNER',
+            'table' => $this->_aObject['table_mentions'],
+            'mainField' => 'id',
+            'mainTable' => 'sys_cmts_ids',
+            'onField' => 'object_id',
+            'joinFields' => array(),
+        );
+    }
+    
+    /**
+     * Delete all data associated with the content
+     * @param $iId content id
+     */
+    public function onDeleteContent($iId) 
+    {
+        $i = 0;
+        foreach ($this->_aMetas as $sMeta) {
+            $sFunc = $sMeta . 'Delete';
+            $i += $this->_oQuery->$sFunc($iId);
+        }
+        return $i;
+    }
+
+    protected function _metaAdd($iId, $s, $sPreg, $sFuncDelete, $sFuncAdd, $iMaxItems, $sAlertName)
+    {
+        $a = array();
+        if (!preg_match_all($sPreg, $s, $a)) {
+            $this->_oQuery->$sFuncDelete($iId);
+            return 0;
+        }
+
+        $aMetas = array_unique($a[1]);
+        $aMetas = array_slice($aMetas, 0, $iMaxItems);
+
+        if ($iRet = $this->_oQuery->$sFuncAdd($iId, $aMetas)) {
+            foreach ($aMetas as $sMeta) {
+                $iObjectId = 'mention' == $sAlertName ? $sMeta : $iId;
+                bx_alert($this->_sObject, $sAlertName . '_added', $iObjectId, bx_get_logged_profile_id(), array('meta' => $sMeta, 'content_id' => $iId));
+                bx_alert('meta_' . $sAlertName, 'added', $iObjectId, bx_get_logged_profile_id(), array('meta' => $sMeta, 'content_id' => $iId, 'object' => $this->_sObject));
+            }
+        }
+
+        return $iRet;
+    }
+}
+
+/** @} */

+ 145 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolMetatagsQuery.php

@@ -0,0 +1,145 @@
+<?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 metatags objects.
+ * @see BxDolMetatag
+ */
+class BxDolMetatagsQuery extends BxDolDb
+{
+    protected $_aObject;
+
+    public function __construct($aObject)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+    }
+
+    static public function getMetatagsObject ($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_metatags` WHERE `object` = ?", $sObject);
+        $aObject = $oDb->getRow($sQuery);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        return $aObject;
+    }
+
+    public function mentionsAdd($mixedContentId, $aMentions, $bDeletePrevious = true)
+    {
+        return $this->metaAdd($mixedContentId, $aMentions, $bDeletePrevious, 'mentionsDelete', $this->_aObject['table_mentions'], 'profile_id');
+    }
+
+    public function mentionsDelete($mixedContentId)
+    {
+        return $this->metaDelete($this->_aObject['table_mentions'], $mixedContentId);
+    }
+    
+    public function keywordsAdd($mixedContentId, $aKeywords, $bDeletePreviousKeywords = true)
+    {
+        return $this->metaAdd($mixedContentId, $aKeywords, $bDeletePreviousKeywords, 'keywordsDelete', $this->_aObject['table_keywords'], 'keyword');
+    }
+
+    public function keywordsDelete($mixedContentId)
+    {
+        return $this->metaDelete($this->_aObject['table_keywords'], $mixedContentId);
+    }
+
+    public function keywordsGet($mixedContentId)
+    {
+        $sQuery = $this->prepare("SELECT `keyword` FROM `{$this->_aObject['table_keywords']}` WHERE `object_id` = ?", $mixedContentId);
+        return $this->getColumn($sQuery);
+    }
+
+    public function keywordsGetSQLParts($sContentTable, $sContentField, $mixedKeyword)
+    {
+        if(!is_array($mixedKeyword))
+            $mixedKeyword = array($mixedKeyword);
+
+        return array(
+            'where' => !empty($mixedKeyword) ? ' AND `tt`.`keyword` IN (' . $this->implode_escape($mixedKeyword) . ')' : '',
+            'join' => 'INNER JOIN `' . $this->_aObject['table_keywords'] . '` AS `tt` ON `' . $sContentTable . '`.`' . $sContentField . '`=`tt`.`object_id`'
+        );
+    }
+
+    public function keywordsPopularList($iLimit)
+    {
+        $sQuery = $this->prepare("SELECT `keyword`, COUNT(*) as `count` FROM `{$this->_aObject['table_keywords']}` GROUP BY `keyword` ORDER BY `count` DESC LIMIT ?", $iLimit);
+        return $this->getPairs($sQuery, 'keyword', 'count');
+    }    
+
+
+
+    public function locationsAdd($mixedContentId, $fLat, $fLng, $sCountryCode, $sState, $sCity, $sZip, $sStreet, $sStreetNumber)
+    {
+        $this->locationsDelete($mixedContentId);
+        if (!$fLat && !$fLng)
+            return true;
+
+        $sQuery = $this->prepare("INSERT INTO `{$this->_aObject['table_locations']}` SET `object_id` = ?, `lat` = ?, `lng` = ?, `country` = ?, `state` = ?, `city` = ?, `zip` = ?, `street` = ?, `street_number` = ?", $mixedContentId, $fLat, $fLng, $sCountryCode, $sState, $sCity, $sZip, $sStreet, $sStreetNumber);
+        return $this->query($sQuery);
+    }
+    
+    public function locationsDelete($mixedContentId)
+    {
+        return $this->metaDelete($this->_aObject['table_locations'], $mixedContentId);
+    }
+
+    public function locationGet($mixedContentId)
+    {
+        $sQuery = $this->prepare("SELECT * FROM `{$this->_aObject['table_locations']}` WHERE `object_id` = ?", $mixedContentId);
+        return $this->getRow($sQuery);
+    }
+
+    public function locationsGetSQLParts($sContentTable, $sContentField, $sCountry = false, $sState = false, $sCity = false, $sZip = false, $aBounds = array())
+    {
+        $aFields = array('country' => 'sCountry', 'state' => 'sState', 'city' => 'sCity', 'zip' => 'sZip', 'bounds' => 'aBounds');
+
+        $aWhere = array();
+        $sWhereBounds = '';
+        foreach ($aFields as $sIndex => $sVar) {
+            if (!$$sVar)
+                continue;
+
+            if ('bounds' == $sIndex) {
+                $sWhereBounds = $this->prepareAsString("AND `tl`.`lat` != 0 AND `tl`.`lng` != 0 AND `tl`.`lat` > ? AND `tl`.`lat` < ? AND `tl`.`lng` > ? AND `tl`.`lng` < ?", ${$sVar}['min_lat'], ${$sVar}['max_lat'], ${$sVar}['min_lng'], ${$sVar}['max_lng']);
+            } 
+            else {
+                $aWhere['tl`.`' . $sIndex] = $$sVar;
+            }
+        }
+
+        return array(
+            'where' => $sWhereBounds . (!empty($aWhere) ? ' AND ' . $this->arrayToSQL($aWhere, ' AND ') : ''),
+            'join' => 'INNER JOIN `' . $this->_aObject['table_locations'] . '` AS `tl` ON `' . $sContentTable . '`.`' . $sContentField . '`=`tl`.`object_id`'
+        );
+    }
+
+    protected function metaDelete($sTable, $mixedContentId)
+    {
+        $sQuery = $this->prepare("DELETE FROM `{$sTable}` WHERE `object_id` = ?", $mixedContentId);
+        return $this->query($sQuery);
+    }
+
+    protected function metaAdd($mixedContentId, $aMetas, $bDeletePrevious, $sFuncDelete, $sTable, $sField)
+    {
+        if ($bDeletePrevious)
+            $this->$sFuncDelete($mixedContentId);
+
+        $i = 0;
+        foreach ($aMetas as $sMeta) {
+            $sQuery = $this->prepare("INSERT INTO `{$sTable}` SET `object_id` = ?, `{$sField}` = ?", $mixedContentId, $sMeta);
+            $i += ($this->query($sQuery) ? 1 : 0);
+        }
+        return $i;
+    }
+}
+
+/** @} */

+ 516 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolProfile.php

@@ -0,0 +1,516 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+define('BX_PROFILE_ACTION_AUTO', 0); ///< automatic action without any checking
+define('BX_PROFILE_ACTION_MANUAL', 1); ///< manual action performed by human
+define('BX_PROFILE_ACTION_ROBOT', 2); ///< action peformed by some robot based on some conditions
+define('BX_PROFILE_ACTION_EXTERNAL', 2); ///< action peformed by external service, like join using OAuth 
+
+class BxDolProfile extends BxDolFactory implements iBxDolProfile
+{
+    protected $_iProfileID;
+    protected $_aProfile;
+    protected $_oQuery;
+
+    /**
+     * Constructor
+     */
+    protected function __construct ($iProfileId)
+    {
+        $iProfileId = (int)$iProfileId;
+        $sClass = get_class($this) . '_' . $iProfileId;
+        if (isset($GLOBALS['bxDolClasses'][$sClass]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+
+        $this->_iProfileID = $iProfileId; // since constructor is protected $iProfileId is always valid
+        $this->_oQuery = BxDolProfileQuery::getInstance();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        $sClass = get_class($this) . '_' . $this->_iProfileID;
+        if (isset($GLOBALS['bxDolClasses'][$sClass]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of Account Profile by account id
+     */
+    public static function getInstanceAccountProfile($iAccountId = false, $bClearCache = false)
+    {
+        if (!$iAccountId)
+            $iAccountId = getLoggedId();
+        $oQuery = BxDolProfileQuery::getInstance();
+        $aProfile = $oQuery->getProfileByContentTypeAccount($iAccountId, 'system', $iAccountId);
+        if (!$aProfile)
+            return false;
+        return self::getInstance($aProfile['id'], $bClearCache);
+    }
+
+    /**
+     * Get singleton instance of Profile by account id, content id and type
+     */
+    public static function getInstanceByContentTypeAccount($iContent, $sType, $iAccountId = false)
+    {
+        if (!$iAccountId)
+            $iAccountId = getLoggedId();
+        $oQuery = BxDolProfileQuery::getInstance();
+        $aProfile = $oQuery->getProfileByContentTypeAccount($iContent, $sType, $iAccountId);
+        if (!$aProfile)
+            return false;
+        return self::getInstance($aProfile['id']);
+    }
+
+    /**
+     * Get singleton instance of Profile by content id and type
+     */
+    public static function getInstanceByContentAndType($iContent, $sType, $bClearCache = false)
+    {
+        $oQuery = BxDolProfileQuery::getInstance();
+        $aProfile = $oQuery->getProfileByContentAndType($iContent, $sType, $bClearCache);
+        if (!$aProfile)
+            return false;
+        return self::getInstance($aProfile['id']);
+    }
+
+    /**
+     * Get singleton instance of Profile by Account id (currently active profile is returned)
+     */
+    public static function getInstanceByAccount($iAccountId = false, $bClearCache = false)
+    {
+        $oQuery = BxDolProfileQuery::getInstance();
+        $mixedProfileId = $oQuery->getCurrentProfileByAccount($iAccountId, $bClearCache);
+
+        return self::getInstance($mixedProfileId);
+    }
+
+    /**
+     * Get singleton instance of Profile by profile id
+     */
+    public static function getInstance($mixedProfileId = false, $bClearCache = false)
+    {
+        if (!$mixedProfileId) {
+            $oQuery = BxDolProfileQuery::getInstance();
+            $mixedProfileId = $oQuery->getCurrentProfileByAccount(getLoggedId(), $bClearCache);
+        }
+
+        $iProfileId = self::getID($mixedProfileId);
+        if (!$iProfileId)
+            return false;
+
+        $sClass = __CLASS__ . '_' . $iProfileId;
+        if (!isset($GLOBALS['bxDolClasses'][$sClass]))
+            $GLOBALS['bxDolClasses'][$sClass] = new BxDolProfile($iProfileId);
+
+        return $GLOBALS['bxDolClasses'][$sClass];
+    }
+
+    /**
+     * Get profile id
+     */
+    public function id()
+    {
+        $aProfile = $this->getInfo($this->_iProfileID);
+        return isset($aProfile['id']) ? $aProfile['id'] : false;
+    }
+
+    /**
+     * Get account id associated with the profile
+     */
+    public function getAccountId()
+    {
+        $aInfo = $this->getInfo();
+        return $aInfo['account_id'];
+    }
+
+    /**
+     * Get account object associated with the profile
+     */
+    public function getAccountObject()
+    {
+        return BxDolAccount::getInstance($this->getAccountId());
+    }
+
+    /**
+     * Get content id associated with the profile
+     */
+    public function getContentId()
+    {
+        $aInfo = $this->getInfo();
+        return $aInfo['content_id'];
+    }
+
+    /**
+     * Check if profile status is active
+     */
+    public function isActive($iProfileId = false)
+    {
+        return BX_PROFILE_STATUS_ACTIVE == $this->getStatus($iProfileId);
+    }
+
+	/**
+     * Is profile online
+     */
+	public function isOnline($iProfileId = false)
+    {
+        $iProfileId = (int)$iProfileId ? $iProfileId : $this->_iProfileID;
+        return $this->_oQuery->isOnline($iProfileId);
+    }
+
+    /**
+     * Get profile status
+     */
+    public function getStatus($iProfileId = false)
+    {
+        $aInfo = $this->_oQuery->getInfoById((int)$iProfileId ? $iProfileId : $this->_iProfileID);
+        return $aInfo['status'];
+    }
+
+    /**
+     * Get profile module name
+     */
+    public function getModule($iProfileId = false)
+    {
+        $aInfo = $this->_oQuery->getInfoById((int)$iProfileId ? $iProfileId : $this->_iProfileID);
+        return $aInfo['type'];
+    }
+
+    /**
+     * Get profile info
+     */
+    public function getInfo($iProfileId = 0)
+    {
+        if ($iProfileId && $iProfileId != $this->_iProfileID)
+            return $this->_oQuery->getInfoById((int)$iProfileId ? $iProfileId : $this->_iProfileID);
+
+        if ($this->_aProfile)
+            return $this->_aProfile;
+        
+        $this->_aProfile = $this->_oQuery->getInfoById((int)$iProfileId ? $iProfileId : $this->_iProfileID);
+        return $this->_aProfile;
+    }
+
+    /**
+     * Validate profile id.
+     * @param $s - profile id
+     * @return profile id or false if profile was not found
+     */
+    static public function getID($s)
+    {
+        $iId = BxDolProfileQuery::getInstance()->getIdById((int)$s);
+        return $iId ? $iId : false;
+    }
+
+    /**
+     * Get name to display in thumbnail
+     */
+    public function getDisplayName($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_name', array($aInfo['content_id']));
+    }
+
+    /**
+     * Get profile url
+     */
+    public function getUrl($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_url', array($aInfo['content_id']));
+    }
+
+    /**
+     * Get profile unit
+     */
+    public function getUnit($iProfileId = 0, $aParams = array())
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_unit', array($aInfo['content_id'], $aParams));
+    }
+
+	/**
+     * Check whether a profile has real image uploaded by user.
+     */
+    public function hasImage($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'has_image', array($aInfo['content_id']));
+    }
+
+    /**
+     * Get picture url
+     */
+    public function getPicture($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_picture', array($aInfo['content_id']));
+    }
+
+    /**
+     * Get avatar url
+     */
+    public function getAvatar($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_avatar', array($aInfo['content_id']));
+    }
+
+    /**
+     * Get thumbnail url
+     */
+    public function getThumb($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_thumb', array($aInfo['content_id']));
+    }
+
+    /**
+     * Get icon url
+     */
+    public function getIcon($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_icon', array($aInfo['content_id']));
+    }
+
+    /**
+     * Get module icon
+     */
+    public function getIconModule($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'module_icon');
+    }
+
+    /**
+     * get profile edit page url
+     */
+    public function getEditUrl($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_edit_url', array($aInfo['content_id']));
+    }
+
+    /**
+     * @see iBxDolProfile::checkAllowedProfileView
+     */
+    public function checkAllowedProfileView($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'check_allowed_profile_view', array($aInfo['content_id']));
+    }
+
+    /**
+     * @see iBxDolProfile::checkAllowedPostInProfile
+     */
+    public function checkAllowedPostInProfile($iProfileId = 0)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'check_allowed_post_in_profile', array($aInfo['content_id']));
+    }
+    
+    /**
+     * Delete profile.
+     * @param $ID - optional profile id to delete
+     * @param $bDeleteWithContent - delete profile with all its contents
+     * @param $bForceDelete - force deletetion is case of account profile deletion
+     * @return false on error, or true on success
+     */
+    function delete($ID = false, $bDeleteWithContent = false, $bForceDelete = false)
+    {
+        $ID = (int)$ID;
+        if (!$ID)
+            $ID = $this->_iProfileID;
+
+        $aProfileInfo = $this->_oQuery->getInfoById($ID);
+        if (!$aProfileInfo)
+            return false;
+
+        // delete system profiles (accounts) is not allowed, instead - delete whole account
+        if (!$bForceDelete && 'system' == $aProfileInfo['type'])
+            return false;
+
+        // delete actual profile
+        if ($sErrorMsg = BxDolService::call($aProfileInfo['type'] , 'delete_entity_service', array($aProfileInfo['content_id'], $bDeleteWithContent)))
+            return false;
+
+        // switch profile context if deleted profile is active profile context
+        $oAccount = BxDolAccount::getInstance ($aProfileInfo['account_id']);
+        $aAccountInfo = $oAccount->getInfo();
+        if (!$bForceDelete && $ID == $aAccountInfo['profile_id'])
+            $oAccount->updateProfileContextAuto($ID);
+
+        // create system event before deletion
+        $isStopDeletion = false;
+        bx_alert('profile', 'before_delete', $ID, 0, array('delete_with_content' => $bDeleteWithContent, 'stop_deletion' => &$isStopDeletion));
+        if ($isStopDeletion)
+            return false;
+
+        // delete associated content
+        if($bDeleteWithContent) {
+	        BxDolCmts::onAuthorDelete($ID);
+
+	        BxDolReport::onAuthorDelete($ID);
+
+	        BxDolVote::onAuthorDelete($ID);
+
+	        BxDolFavorite::onAuthorDelete($ID);
+
+	        BxDolView::onAuthorDelete($ID);
+        }
+
+        // delete connections
+        $oConn = BxDolConnection::getObjectInstance('sys_profiles_friends');
+        $oConn->onDeleteInitiatorAndContent($ID);
+
+        $oConn = BxDolConnection::getObjectInstance('sys_profiles_subscriptions');
+        $oConn->onDeleteInitiatorAndContent($ID);
+
+        // delete profile's acl levels
+        BxDolAcl::getInstance()->onProfileDelete($ID);
+
+        // delete profile
+        if (!$this->_oQuery->delete($ID))
+            return false;
+
+        // create system event
+        bx_alert('profile', 'delete', $ID, 0, array('delete_with_content' => $bDeleteWithContent));
+
+        // unset class instance to prevent creating the instance again
+        $this->_iProfileID = 0;
+        $sClass = get_class($this) . '_' . $ID;
+        unset($GLOBALS['bxDolClasses'][$sClass]);
+
+        return true;
+    }
+
+    /**
+     * Insert account and content id association. Also if currect profile id is not defined - it updates current profile id in account.
+     * @param $iAccountId account id
+     * @param $iContentId content id
+     * @param $sStatus profile status
+     * @param $sType profile content type
+     * @return inserted profile's id
+     */
+    static public function add ($iAction, $iAccountId, $iContentId, $sStatus, $sType = 'system')
+    {
+        $oQuery = BxDolProfileQuery::getInstance();
+        if (!($iProfileId = $oQuery->insertProfile ($iAccountId, $iContentId, $sStatus, $sType)))
+            return false;
+        bx_alert('profile', 'add', $iProfileId, 0, array('module' => $sType, 'content' => $iContentId, 'account' => $iAccountId, 'status' => $sStatus, 'action' => $iAction, 'profile_id' => &$iProfileId));
+        return $iProfileId;
+    }
+
+    /**
+     * Change profile status to 'Active'
+     */
+    public function activate($iAction, $iProfileId = 0, $bSendEmailNotification = true)
+    {
+        $sStatus = $this->getStatus($iProfileId);
+        return $this->changeStatus(BX_PROFILE_STATUS_ACTIVE, BX_PROFILE_STATUS_PENDING == $sStatus ? 'approve' : 'activate', $iAction, $iProfileId, $bSendEmailNotification);
+    }
+
+    /**
+     * Change profile status from 'Pending' to the next level - 'Active'
+     */
+    public function approve($iAction, $iProfileId = 0, $bSendEmailNotification = true)
+    {
+        return $this->changeStatus(BX_PROFILE_STATUS_ACTIVE, 'approve', $iAction, $iProfileId, $bSendEmailNotification);
+    }
+
+    /**
+     * Change profile status to 'Pending'
+     */
+    public function disapprove($iAction, $iProfileId = 0, $bSendEmailNotification = true)
+    {
+        return $this->changeStatus(BX_PROFILE_STATUS_PENDING, 'disapprove', $iAction, $iProfileId, $bSendEmailNotification);
+    }
+
+    /**
+     * Change profile status to 'Suspended'
+     */
+    public function suspend($iAction, $iProfileId = 0, $bSendEmailNotification = true)
+    {
+        return $this->changeStatus(BX_PROFILE_STATUS_SUSPENDED, 'suspend', $iAction, $iProfileId, $bSendEmailNotification);
+    }
+
+    protected function changeStatus($sStatus, $sAlertActionName, $iAction, $iProfileId = 0, $bSendEmailNotification = true)
+    {
+        if (!$iProfileId)
+            $iProfileId = $this->_iProfileID;
+
+        // get account and profile objects
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        $oAccount = $oProfile->getAccountObject();
+        if (!$oProfile || !$oAccount)
+            return false;
+
+        // change status
+        if (!$this->_oQuery->changeStatus($iProfileId, $sStatus))
+            return false;
+
+        $this->_aProfile = array();
+
+        // alert about status changing
+        bx_alert('profile', $sAlertActionName, $iProfileId, false, array('action' => $iAction));
+
+        // send email to member about status change
+        if ($bSendEmailNotification)
+            sendMailTemplate('t_ChangeStatus' . ucfirst($sStatus), $oAccount->id(), $iProfileId, array('status' => $sStatus), BX_EMAIL_SYSTEM);
+
+        return true;
+    }
+
+    /**
+     * Display informer message if it is possible to switch to this profile
+     */
+    public function checkSwitchToProfile($oTemplate = null, $iViewerAccountId = false, $iViewerProfileId = false)
+    {
+        if (false === $iViewerAccountId)
+            $iViewerAccountId = getLoggedId();
+        if (false === $iViewerProfileId)
+            $iViewerProfileId = bx_get_logged_profile_id();
+
+        if (!$iViewerAccountId || !$iViewerProfileId)
+            return;
+
+        $iSwitchToAccountId = $this->getAccountId();
+        $iSwitchToProfileId = $this->id();
+        $bCanSwitch = $iSwitchToAccountId == $iViewerAccountId;
+        bx_alert('account', 'check_switch_context', $iSwitchToAccountId, $iViewerProfileId, array('switch_to_profile' => $iSwitchToProfileId, 'viewer_account' => $iViewerAccountId, 'override_result' => &$bCanSwitch));
+
+        if (!$bCanSwitch ||  $iViewerProfileId == $iSwitchToProfileId)
+            return;
+
+        $oInformer = BxDolInformer::getInstance($oTemplate);
+        if ($oInformer)
+            $oInformer->add('sys-switch-profile-context', _t('_sys_txt_account_profile_context_change_suggestion', BxDolPermalinks::getInstance()->permalink('page.php?i=account-profile-switcher', array('switch_to_profile' => $this->id(), 'redirect_back' => 1))), BX_INFORMER_INFO);
+    }
+
+	/**
+     * Add permament messages.
+     */
+    public function addInformerPermanentMessages ($oInformer)
+    {
+    	$aInfo = $this->getInfo();
+    	$aProfiles = $this->_oQuery->getProfilesByAccount($aInfo['account_id']);
+
+        if ($aInfo['type'] == 'system' && count($aProfiles) == 1) {
+            $sUrl = BxDolPermalinks::getInstance()->permalink('page.php?i=account-profile-switcher');
+            $oInformer->add('sys-account-profile-system', _t('_sys_txt_account_profile_system', $sUrl), BX_INFORMER_ALERT);
+        }
+    }
+}
+
+/** @} */

+ 84 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolPush.php

@@ -0,0 +1,84 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolPush extends BxDolDb implements iBxDolSingleton
+{
+    protected $_sAppId;
+    protected $_sRestApi;
+
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+
+        $this->_sAppId = getParam('sys_push_app_id');
+        $this->_sRestApi = getParam('sys_push_rest_api');
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolPush();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+    
+    public function send($iProfileId, $aMessage, $bAddToQueue = false)
+    {
+        if(empty($this->_sAppId) || empty($this->_sRestApi))
+            return false;
+
+        if($bAddToQueue && BxDolQueuePush::getInstance()->add($iProfileId, $aMessage))
+            return true;
+
+		$aFields = array(
+			'app_id' => $this->_sAppId,
+			'filters' => array(
+		        array('field' => 'tag', 'key' => 'user', 'relation' => '=', 'value' => $iProfileId)
+            ),
+			'contents' => !empty($aMessage['contents']) && is_array($aMessage['contents']) ? $aMessage['contents'] : array(),
+			'headings' => !empty($aMessage['headings']) && is_array($aMessage['headings']) ? $aMessage['headings'] : array(),
+			'url' => !empty($aMessage['url']) ? $aMessage['url'] : '',
+			'chrome_web_icon' => !empty($aMessage['icon']) ? $aMessage['icon'] : ''
+		);
+
+		$oChannel = curl_init();
+		curl_setopt($oChannel, CURLOPT_URL, "https://onesignal.com/api/v1/notifications");
+		curl_setopt($oChannel, CURLOPT_HTTPHEADER, array(
+			'Content-Type: application/json; charset=utf-8',
+			'Authorization: Basic ' . $this->_sRestApi
+		));
+		curl_setopt($oChannel, CURLOPT_RETURNTRANSFER, true);
+		curl_setopt($oChannel, CURLOPT_HEADER, false);
+		curl_setopt($oChannel, CURLOPT_POST, true);
+		curl_setopt($oChannel, CURLOPT_POSTFIELDS, json_encode($aFields));
+		curl_setopt($oChannel, CURLOPT_SSL_VERIFYPEER, false);
+
+		$sResult = curl_exec($oChannel);
+		curl_close($oChannel);
+
+		return $sResult;
+    }
+}
+
+/** @} */

+ 53 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolQueue.php

@@ -0,0 +1,53 @@
+<?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 BxDolQueue extends BxDolFactory
+{
+    protected $_oQuery;
+
+    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);
+    }
+
+	/**
+	 * Send some number of email form mail queue
+	 *
+	 * @param int $iLimit - number of emails to send
+	 * @return real number of sent emails
+	 */
+    public function send($iLimit = 1)
+    {
+        $aSent = array();
+
+    	$aMails = $this->_oQuery->getItems(array('type' => 'to_send', 'start' => 0, 'per_page' => $iLimit));
+    	foreach($aMails as $iId => $aMail)
+    	    if(call_user_func_array(array($this, '_send'), array_slice($aMail, 1)))
+    	        $aSent[] = $iId;
+
+        if(!empty($aSent) && is_array($aSent))
+            $this->_oQuery->deleteItem($aSent);
+
+    	return count($aSent);
+    }
+}
+
+/** @} */

+ 58 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolQueueEmail.php

@@ -0,0 +1,58 @@
+<?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 BxDolQueueEmail extends BxDolQueue implements iBxDolSingleton
+{
+    protected function __construct()
+    {
+        parent::__construct();
+
+        $this->_oQuery = new BxDolQueueEmailQuery();
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolQueueEmail();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    /**
+     * Add message to mail queue
+     *
+     * @param string $sEmail - recipient email
+     * @param string $sSubject - message subject
+     * @param string $sBody - message body
+     * @return true on success, false on error
+     */
+    public function add($sEmail, $sSubject, $sBody, $sHeaders = '', $sParams = '')
+    {
+        return (int)$this->_oQuery->insertItem(array(
+        	'email' => $sEmail,
+            'subject' => $sSubject,
+            'body' => $sBody,
+            'headers' => $sHeaders,
+        	'params' => $sParams
+        )) > 0;
+    }
+
+    /**
+     * Internal method which performs sending using predefined list of params.
+     */
+    protected function _send($sEmail, $sSubject, $sBody, $sHeaders = '', $sParams = '')
+    {
+        return mail($sEmail, $sSubject, $sBody, $sHeaders, $sParams);
+    }
+}
+
+/** @} */

+ 24 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolQueueEmailQuery.php

@@ -0,0 +1,24 @@
+<?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 BxDolQueueEmail object.
+ * @see BxDolQueueEmail
+ */
+class BxDolQueueEmailQuery extends BxDolQueueQuery
+{
+    public function __construct()
+    {
+        parent::__construct();
+
+        $this->_sTable = 'sys_queue_email';
+    }
+}
+
+/** @} */

+ 54 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolQueuePush.php

@@ -0,0 +1,54 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolQueuePush extends BxDolQueue implements iBxDolSingleton
+{
+    protected function __construct()
+    {
+        parent::__construct();
+
+        $this->_oQuery = new BxDolQueuePushQuery();
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolQueuePush();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    /**
+     * Add message to push queue
+     *
+     * @param integer $iProfileId - recipient profile ID
+     * @param array $aMessage - array with Push message 
+     * @return true on success, false on error
+     */
+    public function add($iProfileId, $aMessage)
+    {
+        return (int)$this->_oQuery->insertItem(array(
+        	'profile_id' => $iProfileId,
+            'message' => serialize($aMessage)
+        )) > 0;
+    }
+
+    /**
+     * Internal method which performs sending using predefined list of params.
+     */
+    protected function _send($iProfileId, $sMessage)
+    {
+        return BxDolPush::getInstance()->send($iProfileId, unserialize($sMessage));
+    }
+}
+
+/** @} */

+ 24 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolQueuePushQuery.php

@@ -0,0 +1,24 @@
+<?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 BxDolQueuePush object.
+ * @see BxDolQueuePush
+ */
+class BxDolQueuePushQuery extends BxDolQueueQuery
+{
+    public function __construct()
+    {
+        parent::__construct();
+
+        $this->_sTable = 'sys_queue_push';
+    }
+}
+
+/** @} */

+ 82 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolQueueQuery.php

@@ -0,0 +1,82 @@
+<?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 BxDolQueue object.
+ * @see BxDolQueue
+ */
+class BxDolQueueQuery extends BxDolDb
+{
+    protected $_sTable;
+
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    public function getItems($aParams = array())
+    {
+        if(empty($this->_sTable))
+            return array();
+
+    	$aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+
+    	$sSelectClause = '*';
+    	$sWhereClause = '';
+    	$sLimitClause = isset($aParams['start']) && !empty($aParams['per_page']) ? "LIMIT " . $aParams['start'] . ", " . $aParams['per_page'] : "";
+
+    	if(!empty($aParams['type']))
+            switch($aParams['type']) {
+                case 'to_send':
+                    $aMethod['name'] = 'getAllWithKey';
+                    $aMethod['params'][1] = 0;
+                    $aMethod['params'][2] = array();
+                    $aMethod['params'][3] = PDO::FETCH_NUM;
+                    break;
+            }
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . " FROM `" . $this->_sTable . "` WHERE 1 " . $sWhereClause . $sLimitClause;
+        return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+
+    public function insertItem($aValues)
+    {
+        if(empty($this->_sTable))
+            return false;
+
+        if(empty($aValues) || !is_array($aValues))
+            return false;
+
+        return $this->query("INSERT INTO `" . $this->_sTable . "` SET " . $this->arrayToSQL($aValues));
+    }
+
+	public function updateItem($aValues, $aWhere)
+    {
+        if(empty($this->_sTable))
+            return false;
+
+        if(empty($aValues) || !is_array($aValues) || empty($aWhere) || !is_array($aWhere))
+            return false;
+
+        return (int)$this->query("UPDATE `" . $this->_sTable . "` SET " . $this->arrayToSQL($aValues) . " WHERE " . $this->arrayToSQL($aWhere, ' AND ')) > 0;
+    }
+
+    public function deleteItem($mixedId)
+    {
+        if(empty($this->_sTable))
+            return false;
+
+    	if(!is_array($mixedId))
+    		$mixedId = array($mixedId);
+
+        return (int)$this->query("DELETE FROM `" . $this->_sTable . "` WHERE `id` IN (" . $this->implode_escape($mixedId) . ")") > 0;
+    }
+}
+
+/** @} */

+ 996 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolSearch.php

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

+ 2677 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolTemplate.php

@@ -0,0 +1,2677 @@
+<?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_CODE_KEY', 'skin');
+define('BX_DOL_TEMPLATE_DEFAULT_CODE', 'protean');
+define('BX_DOL_TEMPLATE_FOLDER_ROOT', 'template');
+
+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_PAGE_DEFAULT', 0); ///< default, regular page
+define('BX_PAGE_CLEAR', 2); ///< clear page, without any headers and footers
+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
+
+
+/**
+ * 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 $_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)
+    );
+
+    /**
+     * Main fields
+     */
+    protected $_sName;
+    protected $_sPrefix;
+    protected $_sRootPath;
+    protected $_sRootUrl;
+    protected $_sSubPath;
+    protected $_sInjectionsTable;
+    protected $_sInjectionsCache;
+    protected $_sCode;
+    protected $_sCodeKey;
+    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 $_bImagesInline;
+    protected $_iImagesMaxSize;
+
+    protected $_bCssCache;
+    protected $_bCssMinify;
+    protected $_bCssArchive;
+    protected $_sCssCachePrefix;
+
+    protected $_bJsCache;
+    protected $_bJsMinify;
+    protected $_bJsArchive;
+    protected $_sJsCachePrefix;
+
+    /**
+     * Less related fields
+     */
+    protected $_bLessEnable;
+    protected $_sLessCachePrefix;
+
+    protected $aPage;
+    protected $aPageContent;
+
+    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;
+        list(
+            $this->_sCode, 
+            $this->_sName, 
+            $this->_sSubPath
+        ) = self::retrieveCode($this->_sCodeKey, $this->_sRootPath);
+
+        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');
+
+        $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->_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->_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->_sCssCachePrefix = $this->_sCacheFilePrefix . 'css_';
+
+        $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->_bLessEnable = true;
+        $this->_sLessCachePrefix = $this->_sCacheFilePrefix . 'less_';
+
+        $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 $sRootPath path to root directory.
+     */
+    public static function retrieveCode($sCodeKey = BX_DOL_TEMPLATE_CODE_KEY, $sRootPath = BX_DIRECTORY_PATH_ROOT)
+    {
+        $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($sRootPath . '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;
+
+            $aUrl = parse_url(BX_DOL_URL_ROOT);
+            $sPath = isset($aUrl['path']) && !empty($aUrl['path']) ? $aUrl['path'] : '/';
+
+            setcookie($sCodeKey, $sCode, time() + 60*60*24*365, $sPath);
+
+            return $aResult;
+        };
+
+        $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;
+
+        return $aResult;
+    }
+
+    public static function getColorCode($mixedName = false, $fOpacity = false)
+    {
+        $aClasses = array_keys(self::$_aColors);
+
+        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 = self::$_aColors[$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::$_aColors);
+
+        if(empty($sName) || !in_array($sName, $aClasses))
+            $sName = $aClasses[rand(0, count($aClasses) - 1)];
+
+        $sPrefix = '';
+        switch ($sType) {
+            case BX_DOL_COLOR_FT:
+                $sPrefix = 'col-';
+                break;
+
+            case BX_DOL_COLOR_BG:
+                $sPrefix = 'bg-col-';
+                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,
+            'header' => '',
+            'header_text' => '',
+            'keywords' => array(),
+            'location' => array(),
+            'description'  => '',
+            'robots' => '',
+            'css_name' => array(),
+            'css_compiled' => array(),
+            'css_system' => array(),
+            'js_name' => array(),
+            'js_compiled' => array(),
+            'js_system' => array(),
+            'js_options' => array(),
+            'js_translations' => array(),
+            'js_images' => array(),
+            'injections' => array()
+        );
+
+        //--- Load default CSS ---//
+        $this->addCssSystem(array(
+            'common.css',
+            'default.less',
+            'general.css',
+            'icons.css',
+            'colors.css',
+            'forms.css',
+            'media-desktop.css',
+            'media-tablet.css',
+            'media-phone.css',
+            'media-print.css',
+            BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'marka/|marka.min.css',
+            BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'tinymce-mention/css/|autocomplete.css',
+        ));
+
+        //--- Load default JS ---//
+        $this->addJsSystem(array(
+            'jquery/jquery.min.js',
+            'jquery/jquery-migrate.min.js',
+            'jquery-ui/jquery.ui.position.min.js',
+            'spin.min.js',
+            'jquery.easing.js',
+            'jquery.cookie.min.js',
+            'moment-with-locales.min.js',
+            'functions.js',
+            'jquery.webForms.js',
+            'jquery.dolPopup.js',
+        	'jquery.dolConverLinks.js',
+            'marka/marka.min.js',
+            'headroom.min.js',
+        ));
+
+        //--- Load translations in JS ---//
+        bx_import('BxDolLanguages');
+        $this->addJsTranslation(array(
+            '_Are_you_sure',
+            '_error occured',
+            '_sys_loading',
+            '_copyright'
+        ));
+
+        //--- Load Embed.ly if Key and Pattern are available ---//
+        $bEmbedly = !empty(getParam('sys_embedly_api_key'));
+        if($bEmbedly) {
+            $this->addJsSystem(array(
+            	'jquery.embedly.min.js',
+            ));
+
+            $this->addJsOption(array(
+            	'sys_embedly_api_key',
+            	'sys_embedly_api_pattern',
+            ));
+        }
+
+        //--- Load Iframely if Key is available and Embed.ly is disabled ---//
+        $sIframely = getParam('sys_iframely_api_key');
+        $bIframely = !$bEmbedly && !empty($sIframely);
+        if($bIframely) {
+            $this->addJsSystem(array(
+                bx_proto() . '://cdn.iframe.ly/embed.js?key=' . md5($sIframely)
+            ));
+
+            $this->addJsOption(array(
+            	'sys_iframely_api_key'
+            ));
+        }
+
+        //--- 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;
+
+        bx_import('BxTemplConfig'); // TODO: for some reason autoloader isn't working here....
+        $this->_oTemplateConfig = BxTemplConfig::getInstance();
+
+        bx_import('BxTemplFunctions');
+        $this->_oTemplateFunctions = BxTemplFunctions::getInstance($this);
+    }
+
+    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 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 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 code.
+     *
+     * @return string template's code.
+     */
+    function getCode()
+    {
+        return $this->_sCode;
+    }
+
+    /**
+     * 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]);
+    }
+
+    /**
+     * 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]);
+    }
+    /**
+     * 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
+        );
+    }
+    /**
+     * 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)
+    {
+        if(is_string($mixedKey))
+            $mixedKey = array($mixedKey);
+
+        foreach($mixedKey as $sKey)
+            $this->aPage['js_translations'][$sKey] = _t($sKey, '{0}', '{1}');
+    }
+    /**
+     * 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 = '';
+
+        if (!empty($this->aPage['keywords']) && is_array($this->aPage['keywords']))
+            $sRet .= '<meta name="keywords" content="' . bx_html_attribute(implode(',', $this->aPage['keywords'])) . '" />';
+
+        $bDescription = !empty($this->aPage['description']) && is_string($this->aPage['description']);
+        if ($bDescription)
+            $sRet .= '<meta name="description" content="' . bx_html_attribute($this->aPage['description']) . '" />';
+
+        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']) . '" />';
+
+        if (!empty($this->aPage['image'])) {
+            $sRet .= '<meta property="og:image" content="' . $this->aPage['image'] . '" />';
+
+            $sRet .= '
+            	<meta name="twitter:card" content="summary_large_image" />
+				<meta name="twitter:title" content="' . (isset($this->aPage['header']) ? bx_html_attribute(strip_tags($this->aPage['header'])) : '') . '" />
+				<meta name="twitter:description" content="' . ($bDescription ? bx_html_attribute($this->aPage['description']) : '') . '" />
+				<meta name="twitter:image" content="' . $this->aPage['image'] . '" />';
+        }
+
+        $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'] . '" />';
+
+        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 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);
+    }
+
+    /**
+     * 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 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);
+        return $oMenu ? $oMenu->getCode () : '';
+    }
+
+    /**
+     * 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, $sRet, 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))
+            $sContent = $this->parseHtmlByName('default.html', $aVariables, $this->_sKeyWrapperHtml, BX_DOL_TEMPLATE_CHECK_IN_BOTH);
+
+        //--- 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'), $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 '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 'main_logo':
+                $sRet = BxTemplFunctions::getInstance()->getMainLogo();
+                break;
+            case 'informer':
+                $oInformer = BxDolInformer::getInstance($this);
+                $sRet = $oInformer ? $oInformer->display() : '';
+                break;
+            case 'cover':
+            	$oCover = BxDolCover::getInstance($this);
+            	if(!$oCover)
+            		break;
+
+            	$bCover = true;
+            	$oPage = BxDolPage::getObjectInstanceByURI();
+            	if($oPage !== 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 '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;
+
+            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 <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())
+    {
+        if(!empty($aAttrs['class'])) {
+            $sName .= ' ' . $aAttrs['class'];
+            unset($aAttrs['class']);
+        }
+
+        $sAttrs = '';
+        foreach($aAttrs as $sKey => $sValue)
+            $sAttrs .= ' ' . $sKey . '="' . bx_html_attribute($sValue) . '"';
+
+        return '<i class="sys-icon ' . $sName . '"' . $sAttrs . '></i>';
+    }
+
+    function getCacheFilePrefix($sType)
+    {
+    	$sResult = '';
+    	switch($sType) {
+    		case 'template':
+				$sResult = $this->_sCacheFilePrefix;
+				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;
+
+        $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);
+    }
+
+    /**
+     * 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 = str_replace(array("\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\" type=\"text/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\" type=\"text/javascript\">\n<!--\n" . $sCode . "\n-->\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)
+    {
+        return $this->_processFiles('css', 'add', $mixedFiles, $bDynamic);
+    }
+
+    /**
+     * 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)
+    {
+    	require_once(BX_DIRECTORY_PATH_PLUGINS . 'lessphp/Less.php');
+
+        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;
+
+            require_once(BX_DIRECTORY_PATH_PLUGINS . 'lessphp/Cache.php');
+        	$aFiles = array($mixed['path'] => $mixed['url']);
+        	$aOptions = array('cache_dir' => $this->_sCachePublicFolderPath);
+        	$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 ---//
+        $bLess = $this->_bLessEnable && method_exists($this, $sMethodLess);
+
+        $sResult = "";
+        $aIncluded = array();
+        foreach($aFiles as $aFile) {
+        	if($bLess)
+				$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)
+    {
+        $sUpcaseType = ucfirst($sType);
+
+        $sMethodWrap = '_wrapInTag' . $sUpcaseType;
+        $sMethodLess = '_less' . $sUpcaseType;
+
+        $sResult = "";
+        foreach($aFiles as $aFile) {
+            if($this->_bLessEnable && method_exists($this, $sMethodLess))
+                $aFile = $this->$sMethodLess($aFile);
+
+            $sResult .= $this->$sMethodWrap($aFile['url']);
+        }
+
+        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);
+
+        $sUpcaseType = ucfirst($sType);
+        $sMethodLocate = '_getAbsoluteLocation' . $sUpcaseType;
+        $sMethodWrap = '_wrapInTag' . $sUpcaseType;
+        $sResult = '';
+        foreach($mixedFiles as $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 {
+                $sUrl = $this->$sMethodLocate('url', $sFile);
+                $sPath = $this->$sMethodLocate('path', $sFile);
+            }
+
+            if(empty($sPath) || empty($sUrl))
+                continue;
+
+            $sArrayKey = $sType . ($bSystem ? '_system' : '_compiled');
+            switch($sAction) {
+                case 'add':
+                    if($bDynamic)
+                        $sResult .= $this->$sMethodWrap($sUrl);
+                    else {
+                        $bFound = false;
+                        $aSearchIn = $bSystem ? $this->aPage[$sArrayKey] : array_merge($this->aPage[$sType . '_system'], $this->aPage[$sArrayKey]);
+                        foreach($aSearchIn as $iKey => $aValue)
+                            if($aValue['url'] == $sUrl && $aValue['path'] == $sPath) {
+                                $bFound = true;
+                                break;
+                            }
+
+                        if(!$bFound)
+                            $this->aPage[$sArrayKey][] = array('url' => $sUrl, 'path' => $sPath);
+                    }
+                    break;
+                case 'delete':
+                    if(!$bDynamic)
+                        foreach($this->aPage[$sArrayKey]  as $iKey => $aValue)
+                            if($aValue['url'] == $sUrl) {
+                                unset($this->aPage[$sArrayKey][$iKey]);
+                                break;
+                            }
+                    break;
+            }
+        }
+
+        return $bDynamic ? $sResult : true;
+    }
+
+    /**
+     * 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";
+            $aValues[] = str_replace('$', '\\$', $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)
+                            $sValue .= $this->parseHtmlByContent($aMatches[1], $aValue, $mixedKeyWrapperHtml);
+                    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('$', '\\$', $sValue);
+        }
+
+        try {
+        	$oTemplate = &$this;
+
+        	$aCallbackPatterns = array(
+        		"'<bx_include_auto:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_BOTH,
+        		"'<bx_include_base:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_BASE,
+        		"'<bx_include_tmpl:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_TMPL
+        	);
+	        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_injection:([^\s]+) />'s" => 'get_injection',
+            	"'<bx_menu:([^\s]+) \/>'s" => 'get_menu',
+			));
+        }
+		catch(Exception $oException) {
+        	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 ---//
+        $sContent = preg_replace_callback("'" . $aKeyWrappers['left'] . "([a-zA-Z0-9_-]+)" . $aKeyWrappers['right'] . "'", function($aMatches) use($oTemplate, $mixedKeyWrapperHtml) {
+        	return $oTemplate->parseSystemKey($aMatches[1], $mixedKeyWrapperHtml);
+        }, $sContent);
+
+        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_base:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_BASE,
+        		"'<bx_include_tmpl:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_TMPL
+        	);
+	        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.');
+
+		        	return $mixedResult;
+		        }, $sContent);
+
+			$sContent = $this->_parseContentKeys($sContent);
+        }
+        catch(Exception $oException) {
+        	return false;
+        }
+
+        $aKeys = array_merge($aKeys, array(
+            "'<bx_injection:([^\s]+) />'s",
+            "'<bx_menu:([^\s]+) \/>'s",
+            "'<bx_url_root />'",
+            "'<bx_url_studio />'"
+        ));
+        $aValues = array_merge($aValues, array(
+            "<?php echo \$this->processInjection(\$this->aPage['name_index'], '\\1'); ?>",
+            "<?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_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",
+		));
+
+		foreach($aCallbackPatterns as $sPattern => $sAction)
+			$sContent = preg_replace_callback($sPattern, function($aMatches) use($oTemplate, $sAction) {
+		    	$sResult = '';
+
+	        	switch($sAction) {
+	        		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;
+	        	}
+
+	        	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]);
+            }
+        }
+
+        $sResult = '';
+        $aLocations = array_reverse($this->_aLocations, true);
+        foreach($aLocations as $sKey => $aLocation) {
+            if(($sCheckIn == BX_DOL_TEMPLATE_CHECK_IN_BOTH || $sCheckIn == BX_DOL_TEMPLATE_CHECK_IN_TMPL) && 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;
+            else if(($sCheckIn == BX_DOL_TEMPLATE_CHECK_IN_BOTH || $sCheckIn == BX_DOL_TEMPLATE_CHECK_IN_BASE) && extFileExists($aLocation['path'] . BX_DOL_TEMPLATE_FOLDER_ROOT . DIRECTORY_SEPARATOR . $sFolder . $sName))
+                $sResult = $aLocation[$sType] . BX_DOL_TEMPLATE_FOLDER_ROOT . $sDivider . $sFolder . $sName;
+            else
+                continue;
+            break;
+        }
+
+        /**
+         * 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 "data:image/" . strtolower($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 . $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 type="text/javascript" language="javascript">var 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 type="text/javascript" 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 type="text/javascript" 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;
+    }
+
+    /**
+     *
+     * Functions to display pages with errors, messages and so on.
+     *
+     */
+    function displayAccessDenied ($sMsg = '')
+    {
+        bx_import('BxDolLanguages');
+        header("HTTP/1.0 403 Forbidden");
+        $this->displayMsg($sMsg ? $sMsg : _t('_Access denied'));
+    }
+    function displayNoData ($sMsg = '')
+    {
+        bx_import('BxDolLanguages');
+        header("HTTP/1.0 204 No Content");
+        $this->displayMsg($sMsg ? $sMsg : _t('_Empty'));
+    }
+    function displayErrorOccured ($sMsg = '')
+    {
+        bx_import('BxDolLanguages');
+        header("HTTP/1.0 500 Internal Server Error");
+        $this->displayMsg($sMsg ? $sMsg : _t('_error occured'));
+    }
+    function displayPageNotFound ($sMsg = '')
+    {
+        bx_import('BxDolLanguages');
+        header("HTTP/1.0 404 Not Found");
+        $this->displayMsg($sMsg ? $sMsg : _t('_sys_request_page_not_found_cpt'));
+    }
+    function displayMsg ($s, $bTranslate = false)
+    {
+        $sTitle = $bTranslate ? _t($s) : $s;
+        $oTemplate = BxDolTemplate::getInstance();
+        $oTemplate->setPageNameIndex (BX_PAGE_DEFAULT);
+        $oTemplate->setPageHeader (_t('_Error'));
+        $oTemplate->setPageContent ('page_main_code', DesignBoxContent(_t('_Error'), MsgBox($sTitle), BX_DB_PADDING_DEF));
+        $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;
+
+        $oZ = new BxDolAlerts('system', 'design_before_output', 0, 0, array('page' => &$this->aPage, 'page_content' => &$this->aPageContent));
+        $oZ->alert();
+
+        header( 'Content-type: text/html; charset=utf-8' );
+        header( 'X-Frame-Options: sameorigin' );
+        echo $oTemplate->parsePageByName('page_' . $oTemplate->getPageNameIndex() . '.html', $oTemplate->getPageContent());
+    }
+}
+
+/** @} */

+ 229 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/classes/BxDolXmlParser.php

@@ -0,0 +1,229 @@
+<?php defined('BX_DOL') or defined('BX_DOL_INSTALL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolXmlParser extends BxDolFactory implements iBxDolSingleton
+{
+    protected $rParser;
+
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolXmlParser();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    /**
+     * Get the value of specified attribute for specified tag.
+     */
+    function getAttribute($sXmlContent, $sXmlTag, $sXmlAttribute)
+    {
+        $aValues = $aIndexes = array();
+        $rParser = xml_parser_create("UTF-8");
+        xml_parse_into_struct($rParser, $sXmlContent, $aValues, $aIndexes);
+        xml_parser_free($rParser);
+
+        $sTag = strtoupper($sXmlTag);
+        if(!isset($aIndexes[$sTag]))
+        	return false;
+
+        $iFieldIndex = $aIndexes[$sTag][0];
+        $sAttribute = strtoupper($sXmlAttribute);
+		if(!isset($aValues[$iFieldIndex]['attributes'][$sAttribute]))
+			return false;
+
+        return $aValues[$iFieldIndex]['attributes'][$sAttribute];
+    }
+
+    /**
+     * Get an array of attributes for specified tag or an array of tags with the same name.
+     */
+    function getAttributes($sXmlContent, $sXmlTagName, $sXmlTagIndex = -1)
+    {
+        $aValues = $aIndexes = array();
+        $rParser = xml_parser_create("UTF-8");
+        xml_parse_into_struct($rParser, $sXmlContent, $aValues, $aIndexes);
+        xml_parser_free($rParser);
+
+        /**
+         * gets two-dimensional array of attributes.
+         * tags-attributes
+         */
+        if($sXmlTagIndex == -1) {
+            $aResult = array();
+            $aTagIndexes = $aIndexes[strtoupper($sXmlTagName)];
+            if(count($aTagIndexes) <= 0) return NULL;
+            foreach($aTagIndexes as $iTagIndex)
+                $aResult[] = $aValues[$iTagIndex]['attributes'];
+            return $aResult;
+        } else {
+            $iTagIndex = $aIndexes[strtoupper($sXmlTagName)][$sXmlTagIndex];
+            return $aValues[$iTagIndex]['attributes'];
+        }
+    }
+
+    /**
+     * Get an array of tags or one tag if its index is specified.
+     */
+    function getTags($sXmlContent, $sXmlTagName, $iXmlTagIndex = -1)
+    {
+        $aValues = $aIndexes = array();
+        $rParser = xml_parser_create("UTF-8");
+        xml_parse_into_struct($rParser, $sXmlContent, $aValues, $aIndexes);
+        xml_parser_free($rParser);
+
+        //--- Get an array of tags ---//
+        if($iXmlTagIndex == -1) {
+            $aResult = array();
+            $aTagIndexes = $aIndexes[strtoupper($sXmlTagName)];
+            if(count($aTagIndexes) <= 0) return NULL;
+            foreach($aTagIndexes as $iTagIndex)
+                $aResult[] = $aValues[$iTagIndex];
+            return $aResult;
+        } else {
+            $iTagIndex = $aIndexes[strtoupper($sXmlTagName)][$iXmlTagIndex];
+            return $aValues[$iTagIndex];
+        }
+    }
+
+    /**
+     * Gets the values of the given tag with some attribute. 'name' is used as default attribute.
+     * 
+     * Usage:
+     * Input:
+     * 		<string name="string1"><![CDATA[value1]]></string>
+     * 		<string name="string2"><![CDATA[value2]]></string>
+     * 		<string name="string3"><![CDATA[value3]]></string>
+     * Output:
+     * 		array(
+     * 			'string1' => 'value1',
+     * 			'string2' => 'value2',
+     * 			'string3' => 'value3'
+     * 		)
+     */
+    function getValues($sXmlContent, $sXmlTagName, $sXmlAttrName = 'name')
+    {
+        $sXmlAttrName = strtoupper($sXmlAttrName);
+
+        $aValues = $aIndexes = array();
+        $rParser = xml_parser_create("UTF-8");
+        xml_parse_into_struct($rParser, $sXmlContent, $aValues, $aIndexes);
+        xml_parser_free($rParser);
+
+        $sTag = strtoupper($sXmlTagName);
+        $aTagIndexes = isset($aIndexes[$sTag]) ? $aIndexes[$sTag] : array();
+
+        $aReturnValues = array();
+        foreach($aTagIndexes as $iTagIndex) {
+            if(!isset($aValues[$iTagIndex]['attributes'][$sXmlAttrName]))
+                continue;
+
+            $aReturnValues[$aValues[$iTagIndex]['attributes'][$sXmlAttrName]] = isset($aValues[$iTagIndex]['value']) ? $aValues[$iTagIndex]['value'] : NULL;
+        }
+        return $aReturnValues;
+    }
+
+    /**
+     * Sets the values of tag where attribute "key" equals to specified.
+     */
+    function setValues($sXmlContent, $sXmlTagName, $aKeyValues)
+    {
+        $aValues = $aIndexes = array();
+        $rParser = xml_parser_create("UTF-8");
+        xml_parse_into_struct($rParser, $sXmlContent, $aValues, $aIndexes);
+        xml_parser_free($rParser);
+
+        $aTagIndexes = $aIndexes[strtoupper($sXmlTagName)];
+        if(count($aTagIndexes) == 0) return $this->getContent();
+        foreach($aTagIndexes as $iTagIndex)
+            foreach($aKeyValues as $sKey => $sValue)
+                if($aValues[$iTagIndex]['attributes']['KEY'] == $sKey) {
+                    $aValues[$iTagIndex]['value'] = $sValue;
+                    break;
+                }
+        return $this->getContent($aValues);
+    }
+
+    /**
+     * Adds given values to XML content.
+     */
+    function addValues($sXmlContent, $sXmlTagName, $aKeyValues)
+    {
+        $aValues = $aIndexes = array();
+        $rParser = xml_parser_create("UTF-8");
+        xml_parse_into_struct($rParser, $sXmlContent, $aValues, $aIndexes);
+        xml_parser_free($rParser);
+
+        $aTagIndexes = $aIndexes[strtoupper($sXmlTagName)];
+        $iLastTagIndex = $aTagIndexes[count($aTagIndexes) - 1];
+        $iAddsCount = count($aKeyValues);
+        $iLevel = $aValues[$iLastTagIndex]["level"];
+
+        for($i=count($aValues)-1; $i>$iLastTagIndex; $i--)
+            $aValues[$i+$iAddsCount] = $aValues[$i];
+
+        $i = $iLastTagIndex;
+        foreach($aKeyValues as $sKey => $sValue) {
+            $i++;
+            $aValues[$i] = Array("tag" => $sXmlTagName, "type" => "complete", "level" => $iLevel, "attributes" => Array("KEY" => $sKey), "value" => $sValue);
+        }
+        return $this->getContent($aValues);
+    }
+
+    /**
+     * get content in XML format from given values array
+     */
+    function getContent($aValues = array())
+    {
+        $sContent = "";
+        foreach($aValues as $aValue) {
+            $sTagName = strtolower($aValue['tag']);
+            switch($aValue['type']) {
+                case "open":
+                    $sContent .= "<" . $sTagName . ">";
+                    break;
+
+                case "complete":
+                    $sContent .= "<" . $sTagName;
+                    if(isset($aValue['attributes']))
+                        foreach($aValue['attributes'] as $sAttrKey => $sAttrValue)
+                            $sContent .= " " . strtolower($sAttrKey) . "=\"" . $sAttrValue . "\"";
+                    $sContent .= isset($aValue['value']) ? "><![CDATA[" . $aValue['value'] . "]]></" . $sTagName . ">" : " />";
+                    break;
+
+                case "close":
+                    $sContent .= "</" . $sTagName . ">";
+                    break;
+            }
+        }
+        return $sContent;
+    }
+}
+
+/** @} */

+ 460 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/js/classes/BxDolGrid.js

@@ -0,0 +1,460 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function BxDolGrid (sObject, oOptions) {
+    this._iSearchTimeoutId = false;
+    this._sObject = sObject;
+    this._sIdWrapper = 'bx-grid-wrap-' + sObject;
+    this._sIdContainer = 'bx-grid-cont-' + sObject;
+    this._sIdTable = 'bx-grid-table-' + sObject;
+    this._oOptions = oOptions;
+    this._sFilter = undefined == oOptions.filter ? '' : oOptions.filter;
+    this._sOrderField = undefined == oOptions.order_field ? '' : oOptions.order_field;
+    this._sOrderDir = undefined == oOptions.order_dir ? 'ASC' : oOptions.order_dir;
+    this._sSearchInputText = undefined == oOptions.text_search_input ? '' : oOptions.text_search_input;
+    this._oPopupOptions = $.extend({
+    	onBeforeShow: function(oElement) {
+    		jQuery(oElement).bxTime();
+    	}
+    }, oOptions.popup_options);
+    this._oQueryAppend = oOptions.query_append;
+    this._oConfirmMessages = oOptions.confirm_messages;
+
+    if (this._sFilter.length)
+        this._sSearchInputText = this._sFilter;
+
+    $('#bx-grid-search-' + this._sObject).val(this._sSearchInputText);
+
+    this._onDataReloaded();
+
+    if (this._sOrderField.length)
+        this.setSorting (this._sOrderField, this._sOrderDir, true);
+}
+
+BxDolGrid.prototype.resetSorting = function (sField, sDir) {
+    this._sOrderField = '';
+    this._sOrderDir = 'ASC';
+    this.reload(0);
+};
+
+BxDolGrid.prototype.setSorting = function (sField, sDir, isDisableReload) {
+    
+    if (typeof(sField) == "undefined" || 0 == sField.length)
+        return;
+
+    if (typeof(sDir) != "undefined")
+        this._sOrderDir = 'DESC' == sDir ? 'DESC' : 'ASC'; // explicit order
+    else
+        this._sOrderDir = 'ASC'; // of sorting new field, always order in asceding order    
+    
+    this._sOrderField = sField;
+    
+    var sHtmlIndi = 'ASC' == this._sOrderDir ? ' &darr;' : ' &uarr;';
+    jQuery('#' + this._sIdTable + ' *[bx_grid_sort_head] .bx-grid-sort-indi').hide();
+    jQuery('#' + this._sIdTable + ' *[bx_grid_sort_head=' + sField + '] .bx-grid-sort-indi').html(sHtmlIndi).show();    
+    
+    if (typeof(isDisableReload) == "undefined" || !isDisableReload)
+        this.reload(0);
+};
+
+BxDolGrid.prototype.setFilter = function (sFilter, isReload) {
+
+    if (this._sFilter == sFilter)
+        return;
+
+    this._sFilter = sFilter;
+
+    if (isReload) {
+        if (sFilter.length > 0)
+            this.reload(0);
+        else
+            this.reload();
+    }
+};
+
+BxDolGrid.prototype.orderable = function () {
+    var $this = this;
+    jQuery('#' + this._sIdTable + ' *[bx_grid_sort_head] .bx-grid-sort-handle').bind('click', function () {                
+        var sField = $(this).parents('*[bx_grid_sort_head]').attr('bx_grid_sort_head');
+        if (sField) {
+            $this.setSorting(sField, sField == $this._sOrderField ? ('ASC' == $this._sOrderDir ? 'DESC' : 'ASC') : undefined);
+        }
+    });
+    
+    if (this._sOrderField)
+        this.setSorting (this._sOrderField, this._sOrderDir, true);
+};
+
+BxDolGrid.prototype.sortable = function () {
+    var $this = this;
+    jQuery('#' + this._sIdTable + ' tbody .bx-grid-drag-handle').show();
+    jQuery('#' + this._sIdTable + ' tbody').sortable({
+        handle: '.bx-grid-drag-handle',
+        placeholder: 'bx-grid-drag-placeholder',
+        items:'tr', 
+        forcePlaceholderSize: true, 
+
+        start: function(oEvent, oUi) {
+
+            jQuery('#' + $this._sIdTable + ' tbody tr').removeClass('bx-def-color-bg-hl bx-grid-table-row-trans'); // remove rows highlighting and transitions  
+
+            oUi.placeholder.html('<td colspan="' + $this._oOptions.columns + '">&rarr;</td>'); // add placeholder with arrow
+        
+            oUi.item.addClass('bx-grid-gragging-row bx-def-color-bg-active'); // apply classes for dragged row            
+        },
+
+        stop: function(oEvent, oUi) {
+            oUi.item.removeClass('bx-grid-gragging-row'); // remove classes from dragged row
+            
+            jQuery('#' + $this._sIdTable + ' tbody tr:odd').addClass('bx-def-color-bg-hl'); // highlight odd rows            
+
+            $this.blink(oUi.item.attr('id')); // make dropped row to blink, so we clearly see where dropped row is places
+
+            // searialize current rows order and send result to the server for saving
+            var s = jQuery('#' + $this._sIdTable + ' tbody').sortable('serialize'); 
+            $this.action('reorder', {}, s, true);
+        }
+    });
+};
+
+BxDolGrid.prototype.blink = function (sId) {
+    var e = jQuery('#' + sId);
+    e.removeClass('bx-grid-table-row-trans');
+    e.addClass('bx-def-color-bg-active');    
+    setTimeout('glGrids.' + this._sObject + '._blinkCallback("' + sId + '")', 200);
+};
+
+BxDolGrid.prototype.enable = function (sId, isEnable) {
+    var e = jQuery('#' + sId);
+    var eActions = e.find('.bx-grid-cell-single-actions-wrapper *[bx_grid_action_single]').not('*[bx_grid_permanent_state]');
+
+    if (isEnable) {        
+        e.removeClass('bx-grid-table-row-disabled bx-def-font-grayed');
+        eActions.removeClass('bx-btn-disabled');
+        if (eActions.length)
+            this._bindActionsSingle(e);
+    } else {
+        eActions.addClass('bx-btn-disabled');
+        e.addClass('bx-grid-table-row-disabled bx-def-font-grayed');        
+        if (eActions.length)
+            this._unbindActionsSingle(e);
+    }
+};
+
+BxDolGrid.prototype._blinkCallback = function (sId) {
+    var e = jQuery('#' + sId);
+    e.addClass('bx-grid-table-row-trans');
+    e.removeClass('bx-def-color-bg-active');
+};
+
+BxDolGrid.prototype.actionWithId = function (sId, sAction, oData, sData, isDisableLoading, isConfirm) {
+    var sDataAdd = 'ids[]=' + sId;
+    if (typeof(sData) == "undefined" || !sData.length) {
+        sData = sDataAdd;
+    } else {
+        if ('&' != sData[sData.length-1])
+            sData += '&';
+        sData += sDataAdd;
+    } 
+
+    this.action(sAction, oData, sData, isDisableLoading, isConfirm);
+};
+
+BxDolGrid.prototype.actionWithSelected = function (sActionData, sAction, oData, sData, isDisableLoading, isConfirm) {    
+
+    var sDataAdd = '';
+    jQuery("#" + this._sIdTable + " input[name=" + this._sObject + "_check]:checked").each(function () {
+        if (sDataAdd.length)   
+            sDataAdd += "&";
+        sDataAdd += 'ids[]=' + $(this).val();
+    });
+
+    if (!sDataAdd.length)
+        return;
+
+    if (typeof(sData) == "undefined" || !sData.length) {
+        sData = sDataAdd;
+    } else {
+        if ('&' != sData[sData.length-1])
+            sData += '&';
+        sData += sDataAdd;
+    } 
+
+    this.action(sAction, oData, sData, isDisableLoading, isConfirm);
+};
+
+BxDolGrid.prototype.action = function (sAction, oData, sData, isDisableLoading, isConfirm) {
+    var $this = this;
+    var fPerform = function() {
+    	var i;
+    	var sUrl = sUrlRoot + "grid.php?o=" + $this._sObject + "&a=" + sAction;
+
+    	if (typeof($this._oQueryAppend) == 'object')
+            oData = $.extend({}, $this._oQueryAppend, oData);
+
+        for (i in oData)
+            sUrl += "&" + i + "=" + encodeURIComponent(oData[i]);
+        if (typeof(sData) != 'undefined')
+            sUrl += '&' + sData;
+        if ($this._sFilter.length)
+            sUrl += '&' + $this._oOptions.filter_get + '=' + encodeURIComponent ($this._sFilter);
+        if ($this._sOrderField.length)
+            sUrl += '&' + $this._oOptions.order_get_field + '=' + encodeURIComponent ($this._sOrderField);
+        if ($this._sOrderDir.length)
+            sUrl += '&' + $this._oOptions.order_get_dir + '=' + encodeURIComponent ($this._sOrderDir);
+        sUrl += '&_r=' + Math.random();
+
+        if (typeof(isDisableLoading) == 'undefined' || !isDisableLoading)
+            $this.loading(true);
+
+        $.getJSON(sUrl, function (oData) {
+            $this.processJson(oData, sAction, isDisableLoading);
+        });
+    };
+
+    if(typeof(isConfirm) != 'undefined' && parseInt(isConfirm) == 1)
+    	bx_confirm(typeof(this._oConfirmMessages[sAction]) == 'undefined' ? _t('_sys_grid_confirmation') : this._oConfirmMessages[sAction], function() {
+    		fPerform();
+    	});
+    else
+    	fPerform();
+};
+
+BxDolGrid.prototype.processJson = function (oData, sAction, isDisableLoading) {
+	var $this = this;
+    var fContinue = function() {
+    	if (oData && undefined != oData.grid) {        
+            $('#' + $this._sIdContainer).html($(oData.grid).find('#' + $this._sIdContainer).html());
+            $('#' + $this._sIdWrapper).find('.bx-grid-footer').html($(oData.grid).find('.bx-grid-footer').html());
+            $this._onDataReloaded(true);
+        }
+    	if (oData && undefined != oData.blink) {
+            if ('object' == typeof(oData.blink)) {
+                for(var i in oData.blink)
+                    $this.blink($this._sObject + '_row_' + oData.blink[i]);
+            } else {
+                $this.blink($this._sObject + '_row_' + oData.blink);
+            }
+        }
+        if (oData && undefined != oData.disable) {        
+            if ('object' == typeof(oData.disable)) {
+                for(var i in oData.disable)
+                    $this.enable($this._sObject + '_row_' + oData.disable[i], false);
+            } else {
+                $this.enable($this._sObject + '_row_' + oData.disable, false);
+            }
+        }
+        if (oData && undefined != oData.enable) {
+            if ('object' == typeof(oData.enable)) {
+                for(var i in oData.enable)
+                    $this.enable($this._sObject + '_row_' + oData.enable[i], true);
+            } else {
+                $this.enable($this._sObject + '_row_' + oData.enable, true);
+            }
+        }
+        if (oData && undefined != oData.popup) {
+            var sId = 'grid-popup-' + $this._sObject + '-' + sAction;
+            $('#' + sId).remove();
+            if ('object' == typeof(oData.popup)) {
+                var o = $.extend({}, $this._oPopupOptions, oData.popup.options);
+                $('<div id="' + sId + '" style="display: none;"></div>').prependTo('body').html(oData.popup.html);
+                $('#' + sId).dolPopup(o);
+            } else {
+                $('<div id="' + sId + '" style="display: none;"></div>').prependTo('body').html(oData.popup);
+                $('#' + sId).dolPopup($this._oPopupOptions);
+            }        
+        }
+        if (oData && undefined != oData.eval) {
+            eval(oData.eval);
+        }
+    };
+
+    if (typeof(isDisableLoading) == 'undefined' || !isDisableLoading)
+        this.loading(false);
+
+    if (oData && undefined != oData.msg) {
+        bx_alert(oData.msg, function() {
+        	fContinue();
+        });
+    }
+    else
+    	fContinue();
+};
+
+BxDolGrid.prototype.loading = function (bShow) {
+    bx_loading(this._sIdContainer, bShow);
+};
+
+BxDolGrid.prototype.reload = function (iStart, iPerPage) {
+    var oData = this._getActionDataForReload(iStart, iPerPage);
+    this.action('display', oData);
+};
+
+BxDolGrid.prototype._getActionDataForReload = function (iStart, iPerPage) {
+    var oData = {};
+
+    if (typeof(iStart) != 'undefined') {
+        oData[this._oOptions.paginate_get_start] = iStart;
+        this._oOptions.start = iStart;
+    } 
+    else {
+        oData[this._oOptions.paginate_get_start] = this._oOptions.start;
+    }
+
+    if (this._oOptions.paginate_get_per_page.length) {
+        if (typeof(iPerPage) != 'undefined') {
+            oData[this._oOptions.paginate_get_per_page] = iPerPage;
+            this._oOptions.per_page = iPerPage;
+        }
+        else {
+            oData[this._oOptions.paginate_get_per_page] = this._oOptions.per_page;
+        }
+    }
+
+    return oData;
+};
+
+BxDolGrid.prototype._unbindActionsSingle = function (eElement) {
+    var elements;
+    
+    if ('undefined' == typeof(eElement))
+        elements = jQuery('#' + this._sIdWrapper + ' *[bx_grid_action_single]');
+    else
+        elements = eElement.find('*[bx_grid_action_single]');
+
+    elements.unbind('click');
+};
+
+BxDolGrid.prototype._bindActionsSingle = function (eElement) {
+    var $this = this;
+    var elements;
+
+    if ('undefined' == typeof(eElement))
+        elements = jQuery('#' + this._sIdWrapper + ' *[bx_grid_action_single]');
+    else
+        elements = eElement.find('*[bx_grid_action_single]');
+
+    elements.not('.bx-btn-disabled').bind('click', function () {
+        if ($(this).hasClass('bx-btn-disabled'))
+            return;
+        var sAction = $(this).attr('bx_grid_action_single');
+        var sActionConfirm = $(this).attr('bx_grid_action_confirm');
+        var sActionData = $(this).attr('bx_grid_action_data');
+        var iActionResetPaginate = parseInt($(this).attr('bx_grid_action_reset_paginate'));
+        var oData = iActionResetPaginate ? {} : $this._getActionDataForReload();
+        oData = $this._checkAppend(this, oData);
+        $this.actionWithId (sActionData, sAction, oData, '', false, sActionConfirm);
+    });
+};
+
+BxDolGrid.prototype._bindActions = function (isSkipSearchInput) {
+    var $this = this;
+
+    jQuery('#' + this._sIdContainer).bind('scroll', function () {
+        if (-$(this).find('.bx-grid-table').position().left >= ($(this).find('.bx-grid-table').width() - $(this).width()))
+            $(this).parents('.bx-grid-table-wrapper2').addClass('bx-grid-table-hide-shadow-after');
+        else
+            $(this).parents('.bx-grid-table-wrapper2').removeClass('bx-grid-table-hide-shadow-after');
+        
+        if ($(this).find('.bx-grid-table').position().left >= 0)
+            $(this).parents('.bx-grid-table-wrapper2').addClass('bx-grid-table-hide-shadow-before');
+        else
+            $(this).parents('.bx-grid-table-wrapper2').removeClass('bx-grid-table-hide-shadow-before');
+    });
+
+    jQuery('#' + this._sIdWrapper + ' *[bx_grid_action_bulk]').bind('click', function () {
+        if ($(this).hasClass('bx-btn-disabled'))
+            return;
+        var sAction = $(this).attr('bx_grid_action_bulk');
+        var sActionConfirm = $(this).attr('bx_grid_action_confirm');
+        var sActionData = $(this).attr('bx_grid_action_data');
+        var iActionResetPaginate = parseInt($(this).attr('bx_grid_action_reset_paginate'));
+        var oData = iActionResetPaginate ? {} : $this._getActionDataForReload();
+        oData = $this._checkAppend(this, oData);
+        $this.actionWithSelected (sActionData, sAction, oData, '', false, sActionConfirm);
+    });
+
+    this._bindActionsSingle ();
+    
+    if (jQuery('#' + this._sIdWrapper + ' .bx-switcher-cont input').length) 
+        jQuery('#' + this._sIdWrapper).addWebForms(); 
+    jQuery('#' + this._sIdWrapper + ' .bx-switcher-cont input').bind('change', function () {
+        var sAction = $(this).attr('bx_grid_action_single');
+        var sActionConfirm = $(this).attr('bx_grid_action_confirm');
+        var sActionData = $(this).attr('bx_grid_action_data');
+        var iActionResetPaginate = parseInt($(this).attr('bx_grid_action_reset_paginate'));
+        var oData = iActionResetPaginate ? {} : $this._getActionDataForReload();
+        $this.actionWithId (sActionData, sAction, $.extend({}, oData, {checked:this.checked ? 1 : 0}), '', false, sActionConfirm);
+    });
+
+    if (typeof(isSkipSearchInput) == 'undefined' || false == isSkipSearchInput) {
+
+        jQuery('#' + this._sIdWrapper + ' *[bx_grid_action_independent]').bind('click', function () {
+            if ($(this).hasClass('bx-btn-disabled'))
+                return;
+            var sAction = $(this).attr('bx_grid_action_independent');
+            var sActionConfirm = $(this).attr('bx_grid_action_confirm');
+            var iActionResetPaginate = parseInt($(this).attr('bx_grid_action_reset_paginate'));
+            var oData = iActionResetPaginate ? {} : $this._getActionDataForReload();
+            oData = $this._checkAppend(this, oData);
+            $this.action (sAction, oData, '', false, sActionConfirm);
+        });    
+
+        var callbackSearch = function (sTerm) {
+            clearTimeout($this._iSearchTimeoutId);
+            $this._iSearchTimeoutId = setTimeout(function () {
+            	if(glGrids[$this._sObject])
+            		glGrids[$this._sObject].setFilter(sTerm, true);
+            }, 500);
+        };
+        
+        jQuery('#bx-grid-search-' + this._sObject).bind({
+            keyup: function (e) {
+                callbackSearch(this.value);
+            },
+            focusout: function (e) {
+                if (0 == this.value.length)
+                    this.value = $this._sSearchInputText;
+                callbackSearch(this.value == $this._sSearchInputText ? '' : this.value);
+            },
+            focusin: function (e) {
+                if (this.value == $this._sSearchInputText)
+                    this.value = '';
+            }
+        });
+    }
+};
+
+BxDolGrid.prototype._onDataReloaded = function (isSkipSearchInput) {
+
+    jQuery('#' + this._sIdTable + ' tbody tr:odd').addClass('bx-def-color-bg-hl');
+    
+    jQuery('#' + this._sIdTable).bxTime();
+
+    if (this._oOptions.sorting)
+        this.orderable();
+
+    if (this._oOptions.sortable && 0 == this._sFilter.length && 0 == this._sOrderField.length)
+        this.sortable();
+
+    this._bindActions(isSkipSearchInput);
+
+    this.onDataReloaded(isSkipSearchInput);
+};
+
+BxDolGrid.prototype.onDataReloaded = function (isSkipSearchInput) {};
+
+BxDolGrid.prototype._checkAppend = function (oInput, oData) {
+	var oAppend = $.parseJSON($(oInput).attr('bx_grid_action_append'));
+    if(typeof(oAppend) == 'object')
+        oData = $.extend({}, oData, oAppend);
+
+    return oData;
+};
+
+/** @} */

+ 47 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/js/editor.tinymce.js

@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function bx_editor_insert_img (sEditorId, sImgId, sImgUrl, sClasses) {
+
+    var eEditor = 'object' == typeof(tinymce) ? tinymce.get(sEditorId) : undefined;
+    if ('undefined' == typeof(eEditor))
+        return;
+
+    if (eEditor.dom && eEditor.dom.doc && eEditor.dom.doc.getElementById(sImgId))
+        return;
+
+    if ('undefined' == typeof(sClasses))
+        sClasses = '';
+
+    eEditor.execCommand('mceInsertContent', false, '<img id="' + sImgId + '" class="' + sClasses + '" src="' + sImgUrl + '" />');
+}
+
+function bx_editor_remove_img (aEditorIds, aMarkers) {
+    if ('object' != typeof(tinymce))
+        return;
+
+    var aEditors = [];
+    for (var i = 0; i < aEditorIds.length; i++)
+        aEditors.push(tinymce.get(aEditorIds[i]));
+
+    for (var j = 0; j < aEditors.length; j++) {
+        var eEditor = aEditors[j];
+        if (typeof(eEditor) != 'object' || !eEditor.dom || !eEditor.dom.doc) 
+        	continue;
+
+        // delete images in html editor
+        for (var k = 0; k < aMarkers.length; k++) {
+            var jFiles = $(eEditor.dom.doc).find(aMarkers[k]);
+            jFiles.each(function () { 
+                eEditor.execCommand('mceRemoveNode', false, this); 
+            });
+        }
+    }
+}
+
+/** @} */

+ 907 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/js/functions.js

@@ -0,0 +1,907 @@
+/**
+ * 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);
+
+	    	$('#' + oPopup.attr('id')).remove();
+	        oPopup.hide().prependTo('body').bxTime().dolPopup(oOptions);
+	    }
+
+	    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)
+{
+    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"
+
+		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')) {
+		    $.post(url, function(data) {
+		        $block.html(data);
+		        $block.css('position', blockPos).bxTime();
+		        if ($.isFunction($.fn.addWebForms))
+		            $block.addWebForms();
+
+		        bx_activate_anim_icons();
+
+		        if (typeof callback == 'function')
+		            callback.apply($block);
+		    });		
+		} 
+		else {
+		    $block.load(url + '&_r=' + Math.random(), function() {
+		        $(this).css('position', blockPos).bxTime();
+		        if ($.isFunction($.fn.addWebForms))
+		            $(this).addWebForms();
+
+		        bx_activate_anim_icons();
+
+		        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) {
+
+    sUrl = location.href;
+
+    sUrl = sUrl.replace(/start=\d+/, '').replace(/per_page=\d+/, '').replace(/[&\?]+$/, '');
+    sUrl = bx_append_url_params(sUrl, 'start=' + parseInt(iStart) + '&per_page=' + parseInt(iPerPage));
+    if ('undefined' != typeof(sAdditionalUrlParams))
+        sUrl = bx_append_url_params(sUrl, sAdditionalUrlParams);
+
+    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) {
+    getHtmlData($('#bx-page-block-' + iBlockID), bx_append_url_params(sUrl, 'dynamic=tab&pageBlock=' + iBlockID));
+    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 : '';
+
+	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);
+
+    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 bx-btn-height">' + (bUseSvgLoading ? bx_loading_svg('colored', 'bx-btn-height') : '') + '</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))}, 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) {
+    var options = options || {};
+    var o = $.extend({}, $.fn.dolPopupDefaultOptions, options, {pointer:{el:$(e)}});
+    if ($(jSel + ':visible').length) 
+        $(jSel).dolPopupHide(); 
+    else 
+        $(jSel).dolPopup(o);
+}
+
+/**
+ * Show pointer popup with menu from existing HTML.
+ * @param jSel - jQuery 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 (jSel, e, sPosition) {
+    var options = options || {};
+    var eSlider = $(jSel);    
+
+    if ('undefined' == typeof(e))
+        e = eSlider.data('data-control-btn');
+
+    var eIcon = $(e).find('.sys-icon-a');    
+
+    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 ('block' == sPosition)
+            fPositionBlock();
+        else
+            fPositionSite();
+        
+        eSlider.slideDown();
+
+        eSlider.data('data-control-btn', e);        
+    };    
+    
+    if ('undefined' == typeof(sPosition))
+        sPosition = 'block';
+
+    if ($(jSel + ':visible').length) {
+        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-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-main:visible, .bx-popup-slide-wrapper:visible').each(function () {
+        bx_menu_slide('#' + 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 .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();
+	}
+}
+/**
+ * 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) {
+
+    if ('undefined' != typeof(mixedLoadingElement))
+        bx_loading($(mixedLoadingElement), true);
+
+    $.post(sUrlRoot + 'menu.php', {o:'sys_set_acl_level', 'profile_id': iProfileId, 'acl_level_id': iAclLevel}, function(data) {
+
+        if ('undefined' != typeof(mixedLoadingElement))
+            bx_loading($(mixedLoadingElement), false);
+
+        if (data.length) {
+            bx_alert(data);
+        } else if ($(mixedLoadingElement).hasClass('bx-popup-applied')) {
+            $(mixedLoadingElement).dolPopupHide().remove();
+        }
+    });
+}
+
+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 ('undefined' == typeof(sLang)) {
+        sLang = glBxTimeLang;
+    } else {
+        glBxTimeLang = sLang;
+    }
+
+    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
+            s = moment(sTime).locale(sLang).format($(this).attr('data-bx-format')); // custom 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;
+    }; 
+} (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 ? '?' : '&';
+
+    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;
+}
+
+function bx_search_on_type (e, n, sFormSel, sResultsContSel, sLoadingContSel, bSortResults) {
+    if ('undefined' != typeof(e) && 13 == e.keyCode) {
+        $(sFormSel).find('input[name=live_search]').val(0);
+        $(sFormSel).submit();
+        return false;
+    }
+
+    if ('undefined' != typeof(glBxSearchTimeoutHandler) && glBxSearchTimeoutHandler)
+        clearTimeout(glBxSearchTimeoutHandler);
+
+    glBxSearchTimeoutHandler = setTimeout(function () {
+        bx_search (n, sFormSel, sResultsContSel, sLoadingContSel, bSortResults);
+    }, 500);
+
+    return true;
+}
+
+function bx_search (n, sFormSel, sResultsContSel, sLoadingContSel, bSortResults) {
+    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 (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;
+            });
+        } 
+        $(sResultsContSel).html(data).bxTime();
+    });
+
+    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 ('undefined' == typeof(sColor))
+        sColor = glBxAnimIconColor;
+    else
+       glBxAnimIconColor = sColor;
+
+    $('.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_style(sUrl)
+{
+	$("head").append($("<link rel='stylesheet' href='" + sUrl + "' type='text/css' />"));
+}
+
+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,
+		select: function(e, ui) {
+			$(this).val(ui.item.label);
+			$(this).trigger('superselect', ui.item);
+					
+			if (typeof onSelect === 'function')
+				onSelect(ui);
+			
+			e.preventDefault();			
+		}
+	});
+
+	$('#' + 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-thumb-size bx-def-margin-sec-right" src="' + item.thumb + '">';
+
+			$(this).before('<b class="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)
+{
+	$(document).dolPopupAlert({
+		message: sMessage,
+		onClickOk: fOnClickOk
+	});
+}
+
+function bx_confirm(sMessage, fOnClickYes, fOnClickNo)
+{
+	$(document).dolPopupConfirm({
+		message: sMessage,
+		onClickYes: fOnClickYes,
+		onClickNo: fOnClickNo
+	});
+}
+
+function bx_prompt(sMessage, sValue, fOnClickOk, fOnClickCancel)
+{
+	$(document).dolPopupPrompt({
+		message: sMessage,
+		value: sValue,
+		onClickOk: fOnClickOk,
+		onClickCancel: fOnClickCancel
+	});
+}
+/** @} */

+ 749 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/js/jquery.dolPopup.js

@@ -0,0 +1,749 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+(function($) {
+
+    $.fn.dolPopupDefaultOptions = {
+        parent: 'body', // parent element selector
+        closeOnOuterClick: true,
+        removeOnClose: false,
+        closeElement: '.bx-popup-element-close', // link to element which will close popup
+        position: 'centered', // | 'absolute' | 'fixed' | event | element,
+        fog: {color: '#fff', opacity: .7}, // {color, opacity},
+        pointer: false, // {el:(string_id|jquery_object), align: (left|right|center)},
+        left: 0, // only for fixed or absolute
+        top: 0, // only for fixed
+        bottom: 'auto', // only for fixed or absolute
+        moveToDocRoot: true,
+        onBeforeShow: function () {},
+        onShow: function () {},
+        onBeforeHide: function () {},
+        onHide: function () {},
+        speed: 200
+    }; 
+
+    $.fn.dolPopupDefaultPointerOptions = {
+        align: 'right',
+        offset: '0 0',
+        offset_pointer: '0 0'
+    }; 
+
+    $.fn.dolPopupCenter = function () {
+        var dy = $(window).height() > $(this).outerHeight() ? ($(window).height() - $(this).outerHeight()) / 2 : 0;
+        var dx = $(window).width() > $(this).outerWidth() ? ($(window).width() - $(this).outerWidth()) / 2 : 0;
+        this.css("position","absolute");        
+        this.css("top", Math.max(0, dy + $(window).scrollTop()) + "px");
+        this.css("left", Math.max(0, dx + $(window).scrollLeft()) + "px");
+        return this;
+    }
+
+    function _getScrollbarWidth () {
+        if ($(document.body).height() <= $(window).height()) {
+            return 0;
+        }
+
+        var outer = document.createElement("div"),
+            inner = document.createElement("div"),
+            widthNoScroll,
+            widthWithScroll;
+
+        outer.style.visibility = "hidden";
+        outer.style.width = "100px";
+        document.body.appendChild(outer);
+
+        widthNoScroll = outer.offsetWidth;
+
+        // Force scrollbars
+        outer.style.overflow = "scroll";
+
+        // Add innerdiv
+        inner.style.width = "100%";
+        outer.appendChild(inner);
+
+        widthWithScroll = inner.offsetWidth;
+
+        // Remove divs
+        outer.parentNode.removeChild(outer);
+
+        return widthNoScroll - widthWithScroll;
+    };
+
+    function _dolPopupLockScreen (bLock) {
+        var eBody = $(document.body),
+            eBodyHtml = $("html, body"),
+            iPaddingRight = parseInt(eBody.css("padding-right")) + ((bLock ? 1 : -1) * _getScrollbarWidth());
+
+        eBody.css("padding-right", iPaddingRight + "px");
+        bLock ? eBodyHtml.addClass('bx-popup-lock') : eBodyHtml.removeClass('bx-popup-lock');
+    };
+
+    $.fn.dolPopup = function(options) {
+        var $this = this;
+        var options = options || {};
+        var o = $.extend({}, $.fn.dolPopupDefaultOptions, options);
+
+        if (false != o.pointer) {
+            o.fog = false;
+            o.pointer = $.extend({}, $.fn.dolPopupDefaultPointerOptions, $(document).data('bx-popup-options') ? $(document).data('bx-popup-options') : {}, o.pointer);
+        }
+
+        if (o.fog && !$('#bx-popup-fog').length) {
+            $('<div id="bx-popup-fog" class="bx-def-z-index-overlay" style="display: none;">&nbsp;</div>')
+                .prependTo('body');
+        }
+
+        $('#bx-popup-fog').css({
+            position: 'fixed',
+            top: 0,
+            left: 0,
+            width: $(window).width(),
+            height: $(window).height(),
+            opacity: o.fog.opacity,
+            backgroundColor: o.fog.color,
+            '-webkit-backface-visibility': 'hidden'
+        });
+        
+        $(window).on('resize.popupFog', function () {
+            $('#bx-popup-fog').css({
+                width: $(window).width(),
+                height: $(window).height()
+            });
+        }).on('keyup.popup', function(oEvent) {
+            if(o.closeOnOuterClick && oEvent.which == 27)
+                $this.dolPopupHide();
+        });
+
+        return this.each(function() {
+            var $el = $(this);
+
+            $el.addClass('bx-def-z-index-modal bx-popup-responsive');
+
+            // element must have id
+            if (!$el.attr('id'))
+                return false;
+
+            if ($el.is(":visible")) { // if popup is shown - hide it
+                $el.dolPopupHide();
+                return true;
+            } 
+
+            if (false != o.pointer)
+                $el.addClass('bx-popup-with-pointer'); // to make all popups with pointer not responsive add: .removeClass('bx-popup-responsive')
+
+            // default style for correct positioning
+            $el.css({
+                display: 'block',
+                visibility: 'hidden',
+                position: 'absolute',
+                top: 0,
+                left: 0
+            });
+
+            if (o.moveToDocRoot)
+                $el.detach().appendTo('body');
+
+            setTimeout(function() { // timeout is needed for some browsers to render element
+
+                if (o.fog)
+                    _dolPopupLockScreen(true);
+
+                // remove any transitions before setting popup position
+                $el.removeClass('bx-popup-transitions bx-popup-inactive'); 
+
+                // set popup position
+                $el._dolPopupSetPosition(o);
+
+                if (!$el.hasClass('bx-popup-applied')) { // do this only once
+                    $el.addClass('bx-popup-applied');
+
+                    // save options for element
+                    $el.data('bx-popup-options', o);
+
+                    // attach event for "close element"
+                    if (o.closeElement) {
+                        $(o.closeElement, $el)
+                            .css('cursor', 'hand')
+                            .click(function() {
+                                $el.dolPopupHide();
+                            });
+                    }
+
+                    // attach event for outer click checking
+                    if (o.closeOnOuterClick) {
+                        var fCallback = function(e) {
+                            if ($el.hasClass('bx-popup-applied') && $el.is(':visible')) {
+                                if ($(e.target).parents('#' + $el.attr('id')).length == 0) {
+                                    $el.dolPopupHide();
+                                }
+                            }
+
+                            return true;
+                        }
+
+                        $(document).on({
+                            click: fCallback,
+                            touchend: fCallback
+                        });
+                    }
+                }
+
+                if(typeof(o.onBeforeShow) == 'function')
+                	o.onBeforeShow($el);
+                else if(typeof(o.onBeforeShow) == 'string')
+                	eval(o.onBeforeShow);
+
+                // transition effect
+                if (o.moveToDocRoot) { 
+                    // we need to disable classes with "max-height:100%", since popup isn't in the root element, so animation will not work when moveToDocRoot option is set 
+                    $el.addClass('bx-popup-inactive');
+                    setTimeout(function () {
+                        $el.addClass('bx-popup-transitions bx-popup-active');
+                    }, 10);
+                }
+
+                // Don't should popup with pointer if there is no parent. Checking before showing. 
+                if($(o.pointer.el).length > 0 && !$.contains(document, o.pointer.el[0])) {
+            		$el.remove();
+            		return;
+                }
+
+                // show popup
+                $el.css({display: 'block', visibility: 'visible'});
+                if (o.fog) {
+                	var oOnHide = function() {
+                		$(window).triggerHandler('resize.popupFog');
+                	};
+                	
+                    if (o.speed > 0)
+                        $('#bx-popup-fog').fadeIn(o.speed, oOnHide);
+                    else
+                        $('#bx-popup-fog').show(oOnHide);
+                }
+
+                setTimeout(function () {
+                	// Don't should popup with pointer if there is no parent. Checking after showing.
+                	if($(o.pointer.el).length > 0 && !$.contains(document, o.pointer.el[0])) {
+                		$el.remove();
+                		return;
+                	}
+
+                	if(typeof(o.onShow) == 'function')
+                		o.onShow($el);
+                    else if(typeof(o.onShow) == 'string')
+                    	eval(o.onShow);
+
+                    $el.find('input[type=text]:first').focus(); // put cursor to the first input element
+                }, o.speed);
+
+            }, 100);
+        });
+    };
+
+    $.fn.dolPopupInline = function(oOptionsCustom) {
+    	if(typeof(oOptionsCustom) == 'undefined' || typeof(oOptionsCustom) != 'object')
+            oOptionsCustom = {};
+
+    	var oOptionsTotal = $.extend({}, oOptionsCustom, {
+        	position: 'fixed',
+        	top: '0px',
+        	fog: false,
+        	closeElement: '.bx-popup-close',
+        	onBeforeShow: function(oPopup) {
+        		if(typeof(oOptionsCustom.onBeforeShow) == 'function')
+        			oOptionsCustom.onBeforeShow(oPopup);
+
+        		$('body').addClass('bx-popup-inline-holder');
+        	},
+        	onBeforeHide: function(oPopup) {
+        		if(typeof(oOptionsCustom.onBeforeHide) == 'function')
+        			oOptionsCustom.onBeforeHide(oPopup);
+
+        		$('body').removeClass('bx-popup-inline-holder');
+        	},
+        	onHide: function(oPopup) {
+        		if(typeof(oOptionsCustom.onHide) == 'function')
+        			oOptionsCustom.onHide(oPopup);
+
+        		if(oOptionsCustom.removeOnClose == true)
+        			$(oPopup).remove();
+        	}
+        });
+
+    	return this.each(function() {
+    		$(this).addClass('bx-popup-inline').dolPopup(oOptionsTotal);
+    	});
+    };
+
+    $.fn.dolPopupFullScreen = function(oOptions) {
+    	return this.each(function() {
+    		$(this).addClass('bx-popup-full-screen').dolPopup(oOptions);
+    	});
+    };
+
+    $.fn.dolPopupHide = function(options) { 
+
+        if ('undefined' == typeof(options) || 'object' != typeof(options))
+            options = {};
+
+        return this.each(function() {
+            var $el = $(this);
+
+            if (!$el.hasClass('bx-popup-applied'))
+                return false;
+
+            if (!$el.is(':visible') || 'hidden' == $el.css('visibility') || 'none' == $el.css('display'))
+                return false;            
+
+            var o = $.extend({}, $el.data('bx-popup-options'), options);
+
+            if (!o)
+                return false;
+
+            $(window).off('resize.popupWindow');
+            $(window).off('resize.popupPointer');
+            $(window).off('resize.popupFog');
+            $(window).off('keyup.popup');
+
+            if(typeof(o.onBeforeHide) == 'function')
+            	o.onBeforeHide($el);
+            else if(typeof(o.onBeforeHide) == 'string')
+            	eval(o.onBeforeHide);
+
+            $el.removeClass('bx-popup-active').addClass('bx-popup-inactive');
+
+            var onHide = null;
+            if(typeof(o.onHide) == 'function')
+            	onHide = o.onHide;
+            else if(typeof(o.onHide) == 'string')
+            	onHide = function(oPopup) {
+            		eval(o.onHide);
+            	};
+
+            if (o.speed > 0) {
+
+                if (o.fog)
+                    $('#bx-popup-fog').fadeOut(o.speed);
+
+                setTimeout(function () {
+                    $el.hide();
+
+                    if(typeof(onHide) == 'function')
+                    	onHide($el);
+
+                    if(o.removeOnClose == true)
+                		$el.remove();
+
+                }, o.speed);
+
+            } else {
+
+                if (o.fog)
+                    $('#bx-popup-fog').hide();
+
+                $el.hide(function() {
+                	if(typeof(onHide) == 'function')
+                    	onHide($el);
+
+                	if(o.removeOnClose == true)
+                		$el.remove();
+                });
+            }
+            
+            _dolPopupLockScreen(false);
+        });
+    };
+
+
+    $.fn.dolPopupAjax = function(options) { 
+        options = options || {};
+        options = $.extend({}, $.fn.dolPopupDefaultOptions, options);
+
+        if ('undefined' == typeof(options.url))
+            return;
+
+        if ('undefined' == typeof(options.container))
+            options.container = '.bx-popup-content-wrapped';
+
+        var bFullScreen = false;
+        if (typeof(options.fullScreen) != 'undefined')
+        	bFullScreen = options.fullScreen;
+
+        var bx_menu_on = function (e, b) {
+            var li = $(e).parents('li:first');   
+            if (!li.length)
+                return;
+            if (b) {
+                var ul = $(e).parents('ul:first');   
+                ul.find('li').removeClass('bx-menu-tab-active');
+                li.addClass('bx-menu-tab-active');
+            } else {
+                li.removeClass('bx-menu-tab-active');
+            }
+        }
+
+        var bx_menu_is_on = function (e) {    
+            var li = $(e).parents('li:first');   
+            if (!li.length)
+                return false;
+            return li.hasClass('bx-menu-tab-active');
+        }
+
+        return this.each(function() {
+            var e = $(this);
+
+            // get id
+            var sPopupId = '';
+            var sIdPrefix = 'bx-popup-ajax-wrapper-';
+            if(typeof(e.attr('bx-popup-id')) != 'undefined')
+            	sPopupId = sIdPrefix + e.attr('bx-popup-id');
+            else if(typeof(options.id) != 'undefined')
+            	switch(typeof(options.id)) {
+            		case 'string':
+            			sPopupId = sIdPrefix + options.id;
+            			break;
+
+            		case 'object':
+            			sPopupId = typeof(options.id.force) != 'undefined' && options.id.force ? options.id.value : sIdPrefix + options.id.value;
+            			break;
+            	}
+            else
+            	sPopupId = sIdPrefix + parseInt(2147483647 * Math.random());
+
+            var oPointerOptions = $.isWindow(e[0]) ? false : $.extend({}, {el:$(e), align:'center'}, options.pointer);
+            if ($('#' + sPopupId + ':visible').length) { // if popup exists and is shown - hide it
+                
+                $('#' + sPopupId).dolPopupHide();
+
+            } else if ($('#' + sPopupId).length) { // if popup exists but not shown - unhide it
+
+                if (!$.isWindow(e[0]))
+                    bx_menu_on(e, true);
+
+                var oPopupOptions = $.extend({}, options, {
+                    pointer: oPointerOptions,
+                    onHide: function (oPopup) {
+                        if (!$.isWindow(e[0]))
+                            bx_menu_on(e, false);
+
+                        if(typeof(options.onHide) == 'function')
+                        	options.onHide(oPopup);
+                    }
+                });
+
+                if(bFullScreen)
+                	$('#' + sPopupId).dolPopupFullScreen(oPopupOptions);
+                else
+                	$('#' + sPopupId).dolPopup(oPopupOptions);
+
+            } else { // if popup doesn't exists - create new one from provided url
+
+                if (!$.isWindow(e[0]))
+                    bx_menu_on(e, true);
+
+                $('<div id="' + sPopupId + '" style="display:none;">' + $('#bx-popup-loading').html() + '</div>').appendTo(options.parent).find(options.container).hide();
+
+                $('#' + sPopupId).addClass($('#bx-popup-loading').attr('class'));
+
+                var oLoading = $('#' + sPopupId + ' .bx-popup-loading-wrapped');
+                bx_loading_content(oLoading, true, true);
+
+                var oPopupOptions = $.extend({}, options, {
+                    pointer: oPointerOptions,
+                    onHide: function (oPopup) {
+                        if (!$.isWindow(e[0]))
+                            bx_menu_on(e, false);
+
+                        if(typeof(options.onHide) == 'function')
+                        	options.onHide(oPopup);
+                    }
+                });
+
+                if(bFullScreen)
+                	$('#' + sPopupId).dolPopupFullScreen(oPopupOptions);
+                else
+                	$('#' + sPopupId).dolPopup(oPopupOptions);
+
+                var fOnLoad = function() {
+                	bx_loading_content(oLoading, false);
+
+                	$('#' + sPopupId + ' ' + options.container).bxTime().show();
+
+					$('#' + sPopupId)._dolPopupSetPosition({
+						pointer: oPointerOptions
+					});
+                };
+
+                var sUrl = (options.url.indexOf('http://') == 0 || options.url.indexOf('https://') == 0 || options.url.indexOf('/') == 0 ? '' : sUrlRoot) + options.url;
+
+                $('#' + sPopupId).find(options.container).load(sUrl, function () {
+                    var f = function () {
+                    	if($('#' + sPopupId).find('img').length > 0 && !$('#' + sPopupId).find('img').get(0).complete)
+                    	
+                    		$('#' + sPopupId).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
+                });
+            }
+        });
+    };
+
+
+    $.fn.dolPopupImage = function(sUrl, sLoadingElement) {
+
+        return this.each(function() {
+
+            var ePopup = $(this);
+            var k = .97,
+                w = $(window).width(),
+                h = $(window).height(),
+                eImg = new Image();
+
+            if (!ePopup.find('img').size())
+                return;
+
+            ePopup.filter('.bx-popup-wrapper').addClass('bx-popup-image-wrapper');
+
+            eImg.src = sUrl;
+
+            if ('undefined' != typeof(sLoadingElement))
+                bx_loading (sLoadingElement, true);
+
+            $(eImg).on('load', function () {
+
+                var ri = eImg.width / eImg.height;
+                var rs = w/h;
+
+                if ('undefined' != typeof(sLoadingElement))
+                    bx_loading (sLoadingElement, false);
+
+                ePopup.find('img').attr('src', sUrl);
+
+                if ('undefined' != typeof(window.matchMedia) && window.matchMedia("(max-width:720px)").matches)
+                    k = 1.0;
+
+                // fit image into the browser window
+                if (eImg.width < w * k && eImg.height < h * k) {
+                    ePopup.find('img').css({
+                        width: 'auto',
+                        height: 'auto'
+                    });
+                }
+                else {
+                    ePopup.find('img').css({
+                        width: rs > ri ? parseInt(k * eImg.width * h/eImg.height) + 'px' : 'auto',
+                        height: rs > ri ? 'auto' : parseInt(k * eImg.height * w/eImg.width) + 'px',
+                    }).addClass('bx-constrain-height');
+                }
+                
+                // show popup
+                ePopup.dolPopup();
+
+                // hide popup upon clicking/tapping on the image
+                var fCallback = function (e) {
+                    e.stopPropagation();
+                    ePopup.dolPopupHide();
+                };
+                ePopup.find('img').on({
+                    click: fCallback,
+                    touchend: fCallback
+                });
+
+            });
+        });
+    };
+
+    $.fn.dolPopupAlert = function(options) {
+    	var oAPopup = $('#bx-popup-alert');
+
+    	if(options.message != undefined && options.message.length > 0)
+    		oAPopup.find('.popup_alert_text').html(options.message);
+
+    	oAPopup.find('.popup_alert_ok').bind('click', function() {
+			if(options.onClickOk && typeof(options.onClickOk) == 'function')
+				options.onClickOk(oAPopup);
+
+			oAPopup.dolPopupHide();
+		});
+
+		var fOnHide = options.onHide;
+		options.onHide = function(oPopup) {
+			if(typeof(fOnHide) == 'function')
+				fOnHide(oAPopup);
+
+			oAPopup.find('.bx-btn').unbind('click');
+		};
+
+		oAPopup.dolPopup(options);
+    };
+
+    $.fn.dolPopupConfirm = function(options) {
+    	var oCPopup = $('#bx-popup-confirm');
+
+    	if(options.message != undefined && options.message.length > 0)
+    		oCPopup.find('.popup_confirm_text').html(options.message);
+
+    	oCPopup.find('.popup_confirm_yes').bind('click', function(event) {
+            event.stopPropagation();
+
+			if(options.onClickYes && typeof(options.onClickYes) == 'function')
+				options.onClickYes(oCPopup);
+
+			oCPopup.dolPopupHide();
+		});
+
+		oCPopup.find('.popup_confirm_no').bind('click', function(event) {
+            event.stopPropagation();
+
+			if (options.onClickNo && typeof(options.onClickNo) == 'function')
+				options.onClickNo(oCPopup);
+
+			oCPopup.dolPopupHide();
+		});
+
+		var fOnHide = options.onHide;
+		options.onHide = function(oPopup) {
+			if(typeof(fOnHide) == 'function')
+				fOnHide(oCPopup);
+
+			oCPopup.find('.bx-btn').unbind('click');
+		};
+
+		oCPopup.dolPopup(options);
+    };
+
+    $.fn.dolPopupPrompt = function(options) {
+    	var oPPopup = $('#bx-popup-prompt');
+
+    	oPPopup.setValue = function(mixedValue) {
+    		return oPPopup.find('[name="bx-popup-prompt-value"]').val(mixedValue);
+    	};
+
+    	oPPopup.getValue = function() {
+    		return oPPopup.find('[name="bx-popup-prompt-value"]').val();
+    	};   		
+
+    	if(options.message != undefined && options.message.length > 0)
+    		oPPopup.find('.popup_prompt_text').html(options.message);
+
+    	if(options.value != undefined && options.value.length > 0)
+    		oPPopup.setValue(options.value);
+
+    	oPPopup.find('.popup_prompt_ok').bind('click', function() {
+			if(options.onClickOk && typeof(options.onClickOk) == 'function')
+				options.onClickOk(oPPopup);
+
+			oPPopup.dolPopupHide();
+		});
+
+		oPPopup.find('.popup_prompt_cancel').bind('click', function() {
+			if(options.onClickCancel && typeof(options.onClickCancel) == 'function')
+				options.onClickCancel(oPPopup);
+
+			oPPopup.dolPopupHide();
+		});
+
+		var fOnHide = options.onHide;
+		options.onHide = function(oPopup) {
+			if(typeof(fOnHide) == 'function')
+				fOnHide(oPPopup);
+
+			oPPopup.setValue('');
+			oPPopup.find('.bx-btn').unbind('click');
+		};
+
+		oPPopup.dolPopup(options);
+    };
+
+    $.fn._dolPopupSetPosition = function(options) {
+
+        var o = $.extend({}, $.fn.dolPopupDefaultOptions, options);
+
+        if (undefined != o.pointer && false != o.pointer)
+            o.pointer = $.extend({}, $.fn.dolPopupDefaultPointerOptions, $(document).data('bx-popup-options') ? $(document).data('bx-popup-options') : {}, o.pointer);
+
+        return this.each(function() {
+            var $el = $(this);
+            
+            if (o.pointer != false) {                
+                
+                var yOffset = 3;
+                var ePointAt = 'string' == o.pointer.el ? $(o.pointer.el) : o.pointer.el;
+                if (!ePointAt)
+                    ePointAt = $('body');
+
+                var aOffset = ('' + o.pointer.offset).split(' ', 2);
+                var aOffsetPointer = ('' + o.pointer.offset_pointer).split(' ', 2);
+                if (undefined == aOffset[0] || undefined == aOffset[1])
+                    aOffset = [0, 0];
+                if (undefined == aOffsetPointer[0] || undefined == aOffsetPointer[1])
+                    aOffsetPointer = [0, 0];
+
+                $el.position({
+                    of: ePointAt,
+                    my: o.pointer.align + '+' + parseInt(aOffset[0]) + ' top+' + yOffset + '+' + parseInt(aOffset[1] - 0),
+                    at: o.pointer.align + ' bottom',
+                    collision: 'flip none'
+                });
+
+                $el.find('.bx-popup-box-pointer').css('display', 'block').position({
+                    of: ePointAt,
+                    my: 'center+' + (parseInt(aOffsetPointer[0]) + parseInt(aOffset[0])) + ' top+' + yOffset + '+' + (parseInt(aOffsetPointer[1]) + parseInt(aOffset[1]) + 0),
+                    at: 'center bottom'
+                });
+
+                $(window).on('resize.popupPointer', function() {
+                    $el.position({
+                        of: ePointAt,
+                        my: o.pointer.align + '+' + parseInt(aOffset[0]) + ' top+' + yOffset + '+' + parseInt(aOffset[1]),
+                        at: o.pointer.align + ' bottom',
+                        collision: 'flip none'
+                    });
+                });
+
+            } else if (o.position == 'fixed' || o.position == 'absolute') {
+
+                $el.css({
+                    position: o.position,
+                    left: o.left,
+                    top: o.top,
+                    bottom: o.bottom
+                });
+
+            } else if (o.position == 'centered') {
+
+            	var oPosition = function(oElement) {
+                    oElement.dolPopupCenter();
+            	};
+
+            	oPosition($el);
+
+                // attach window resize event
+                $(window).on('resize.popupWindow', function() {
+                	oPosition($el);
+                }).on('scroll', function() {
+                	oPosition($el);
+                });
+
+            } else if (typeof o.position == 'object') {
+
+                $el.position({
+                    of: o.position,
+                    my: 'center top',
+                    at: 'center bottom',
+                    collision: 'flip flip'
+                });
+
+            }
+        });
+    };
+
+})(jQuery);
+
+/** @} */

+ 1627 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/inc/utils.inc.php

@@ -0,0 +1,1627 @@
+<?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_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;
+    }
+
+    if (get_magic_quotes_gpc() && $isCheckMagicQuotes)
+        $mixedData = stripslashes($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+)[\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_TS, $mixedParams, $isCheckMagicQuotes);
+        $iDay   = $m[3];
+        $iMonth = $m[2];
+        $iYear  = $m[1];
+        $iH = $m[4];
+        $iM = $m[5];
+        $iS = isset($m[6]) ? $m[6] : 0;
+        $sFunc = BX_DATA_DATETIME_TS_UTC == $iDataType ? 'gmmktime' : 'mktime';
+        $iRet = $sFunc ($iH, $iM, $iS, $iMonth, $iDay, $iYear);
+        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_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", (int)$mixedData);
+
+    case BX_DATA_HTML:
+        return bx_linkify_html($mixedData, 'class="' . BX_DOL_LINK_CLASS . '"');
+    case BX_DATA_TEXT_MULTILINE:
+        return $mixedData;
+    case BX_DATA_TEXT:
+    default:
+        return htmlspecialchars_adv($mixedData);
+    }
+}
+
+/*
+ * 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 = BxDolProfile::getInstance($iProfileId);
+
+    $oAccount = $iAccountId ? BxDolAccount::getInstance($iAccountId) : ($oProfile ? $oProfile->getAccountObject() : null);
+
+    $oEmailTemplates = BxDolEmailTemplates::getInstance();
+
+    if (!$oAccount || !$oProfile || !$oEmailTemplates)
+        return false;
+
+    $aTemplate = $oEmailTemplates->parseTemplate($sTemplateName, $aReplaceVars, $iAccountId, (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 $sRecipientEmail - Email where email should be send
+ * @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($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 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}";
+
+    $sMailSubject = !isset($aCustomHeaders['Subject']) ? '=?UTF-8?B?' . base64_encode( $sMailSubject ) . '?=' : $aCustomHeaders['Subject'];
+
+    // build data for alert handler
+    $bResult = null;
+    $aAlert = array(
+        'email' => $sRecipientEmail,
+        'subject' => $sMailSubject,
+        'body' => $sMailBody,
+        'header' => $sMailHeader,
+        'params' => $sMailParameters,
+        'recipient' => $aRecipientInfo,
+        'html' => 'html' == $sEmailFlag ? true : false,
+        'custom_headers' => $aCustomHeaders,
+        'override_result' => &$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);
+    }
+
+    // send mail or put it into queue
+    $bResult = false;
+    if(!$bAddToQueue || !($bResult = BxDolQueueEmail::getInstance()->add($sRecipientEmail, $sMailSubject, $sMailBody, $sMailHeader, $sMailParameters)))
+        $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 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 = '', $sFileName = 'debug.log')
+{
+    $sCont =
+        '--- ' . date('r') . ' (' . BX_DOL_START . ") ---\n" .
+        $sDesc . "\n" .
+        print_r($mWhat, true) . "\n\n\n";
+
+    $rFile = fopen(BX_DIRECTORY_PATH_LOGS . $sFileName, 'a');
+    fwrite($rFile, $sCont);
+    fclose($rFile);
+}
+
+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']) {
+
+        require_once( BX_DIRECTORY_PATH_PLUGINS . 'htmlpurifier/HTMLPurifier.standalone.php' );
+
+        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 HTMLPurifier_Filter_YouTube(), new HTMLPurifier_Filter_YoutubeIframe(), new HTMLPurifier_Filter_AddBxLinksClass()));
+
+	    $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', 'Number');
+		}
+
+        $oHtmlPurifier = new HTMLPurifier($oConfig);
+    }
+
+    if (!$GLOBALS['logged']['admin'])
+        $val = $oHtmlPurifier->purify($val);
+
+    bx_alert('system', 'clear_xss', 0, 0, array('oHtmlPurifier' => $oHtmlPurifier, 'return_data' => &$val));
+
+    return $val;
+}
+
+//--------------------------------------- friendly permalinks --------------------------------------//
+//------------------------------------------- main functions ---------------------------------------//
+function uriGenerate ($s, $sTable, $sField, $sEmpty = '-')
+{
+    $s = uriFilter($s, $sEmpty);
+    if(uriCheckUniq($s, $sTable, $sField))
+        return $s;
+
+    // cut off redundant part
+    if(get_mb_len($s) > 240)
+        $s = get_mb_substr($s, 0, 240);
+
+    // try to add date
+    $s .= '-' . date('Y-m-d');
+    if(uriCheckUniq($s, $sTable, $sField))
+        return $s;
+
+    // try to add number
+    for($i = 0 ; $i < 999 ; ++$i)
+        if(uriCheckUniq($s . '-' . $i, $sTable, $sField))
+            return ($s . '-' . $i);
+
+    return rand(0, 999999999);
+}
+
+function uriFilter ($s, $sEmpty = '-')
+{
+    if (BxTemplConfig::getInstance()->bAllowUnicodeInPreg)
+        $s = get_mb_replace ('/[^\pL^\pN]+/u', '-', $s); // unicode characters
+    else
+        $s = get_mb_replace ('/([^\d^\w]+)/u', '-', $s); // latin characters only
+
+    $s = get_mb_replace ('/([-^]+)/', '-', $s);
+    $s = get_mb_replace ('/([-]+)$/', '', $s); // remove trailing dash
+    if (!$s) $s = $sEmpty;
+    return $s;
+}
+
+function uriCheckUniq ($s, $sTable, $sField)
+{
+    $oDb = BxDolDb::getInstance();
+
+    $sSql = $oDb->prepare("SELECT 1 FROM `$sTable` WHERE `$sField`=? LIMIT 1", $s);
+    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';
+        } else {
+            $sPath = BX_DIRECTORY_PATH_MODULES . BxDolTemplate::getInstance()->getPath() . 'data/template/system/scripts/' . $sClassName . '.php';
+        }
+
+        if (file_exists($sPath)) {
+            require_once($sPath);
+            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)
+    	$sFileUrl = bx_append_url_params($sFileUrl, $aParams);
+
+    $sResult = '';
+    if(function_exists('curl_init')) {
+        $rConnect = curl_init();
+
+        curl_setopt($rConnect, CURLOPT_USERAGENT, 'UNA ' . BX_DOL_VERSION);
+        curl_setopt($rConnect, CURLOPT_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 (!ini_get('open_basedir'))
+            curl_setopt($rConnect, CURLOPT_FOLLOWLOCATION, 1);
+
+        if ($aHeaders)
+            curl_setopt($rConnect, CURLOPT_HTTPHEADER, $aHeaders);
+
+        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));
+        }
+
+        $sAllCookies = '';
+        foreach($_COOKIE as $sKey=>$sValue){
+            $sAllCookies .= $sKey . '=' . $sValue . ';';
+        }
+        curl_setopt($rConnect, CURLOPT_COOKIE, $sAllCookies);
+
+        if ($aCustomCurlParams)
+            foreach ($aCustomCurlParams as $sName => $mixedValue)
+                curl_setopt($rConnect, constant($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.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];
+
+        if (preg_match("/<title[^>]*>(.*)<\/title>/i", $sContent, $aMatch))
+            $aResult['title'] = $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); 
+            }
+
+        }
+    }
+
+    return $aResult;
+}
+
+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 .= "!?=/&+,.";
+
+    srand((double)microtime()*1000000);
+    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);
+    }
+}
+
+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_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;
+}
+
+function bx_append_url_params ($sUrl, $mixedParams)
+{
+    if (!$mixedParams)
+        return $sUrl;
+
+    $sParams = false == strpos($sUrl, '?') ? '?' : '&';
+
+    if (is_array($mixedParams)) {
+        foreach($mixedParams as $sKey => $sValue) {
+            if (!is_array($sValue)) {
+                $sParams .= $sKey . '=' . $sValue . '&';
+            }
+            else {
+                foreach($sValue as $sSubValue)
+                    $sParams .= $sKey . '[]=' . $sSubValue . '&';
+            }
+        }
+        $sParams = substr($sParams, 0, -1);
+    } else {
+        $sParams .= $mixedParams;
+    }
+    return $sUrl . $sParams;
+}
+
+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;
+}
+
+function bx_trim_nl_duplicates($s)
+{
+    $sStrip = implode('', array_keys(get_html_translation_table(HTML_ENTITIES)));
+    return implode('', array_filter(explode("\r\n", $s), function($s) use ($sStrip) {
+        return trim(strip_tags($s), $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 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();
+}
+
+/**
+ * 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)
+{
+    return BxDolService::call($mixed, $sMethod, $aParams, $sClass, $bIgnoreCache);
+}
+
+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 . '}', $sValue, $mixed);
+    }
+
+    return $mixed;
+}
+
+function bx_site_hash($sSalt = '', $isSkipVersion = false)
+{
+    return md5($sSalt . ($isSkipVersion ? '' : bx_get_ver()) . 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) ? '' : '<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) {
+        $sPort = empty($aUrl['port']) || 80 == $aUrl['port'] || 443 == $aUrl['port'] ? '' : ':' . $aUrl['port'];
+        header("Location:{$aUrl['scheme']}://{$aUrl['host']}{$sPort}{$_SERVER['REQUEST_URI']}", true, 301);
+        exit;
+    }
+    
+    return $bRedirectRequired;
+}
+
+/**
+ * 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'
+ * @return true on success or false on error
+ */
+function bx_smart_readfile($sPath, $sFilename = '', $sMimeType = 'application/octet-stream', $iCacheAge = 0, $sCachePrivacy = 'public')
+{
+    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("Accept-Ranges: 0-$length");
+    if ($sFilename)
+        header('Content-Disposition: inline; 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);
+
+
+    $buffer = 1024 * 8;
+    while(!feof($fp) && ($p = ftell($fp)) <= $end) {
+
+        if ($p + $buffer > $end) {
+            $buffer = $end - $p + 1;
+        }
+        set_time_limit(0);
+        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,6})(:[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--)
+    {
+        $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) {
+            $sAttrs .= ' target="_blank" ';
+            if ($bAddNofollow)
+                $sAttrs .= ' rel="nofollow" ';
+        }
+
+        $text = substr_replace($text, '<a ' . $sAttrs . ' 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) {
+        $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)
+{
+    $a = array(
+        'http://',
+        'https://',
+        'https/',
+        'http/',
+        'www.',
+        '.com'
+    );
+    foreach ($a as $s)
+        if (false !== strpos($sContent, $s))
+            return true;
+
+    return false;
+}
+
+/** @} */

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

+ 93 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/classes/BxBaseModGeneralCmtsSearchResult.php

@@ -0,0 +1,93 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModGeneralCmtsSearchResult extends BxBaseModGeneralSearchResult
+{
+	protected $sModule;
+	protected $sModuleObjectComments;
+
+    function __construct($sMode = '', $aParams = array())
+    {
+        parent::__construct($sMode, $aParams);
+
+        $this->oModule = $this->getMain();
+        $this->sModuleObjectComments = $this->oModule->_oConfig->CNF['OBJECT_COMMENTS'];
+
+        $this->aCurrent = array(
+        	'name' => $this->oModule->_oConfig->getName() . '_cmts',
+            'module_name' => $this->oModule->_oConfig->getName(),
+            'object_metatags' => 'sys_cmts',
+        	'title' => '',
+            'table' => $this->oModule->_oConfig->getDbPrefix() . 'cmts',
+            'ownFields' => array('cmt_id', 'cmt_object_id', 'cmt_author_id', 'cmt_text', 'cmt_time'),
+            'searchFields' => array('cmt_text'),
+            'restriction' => array(
+                'author' => array('value' => '', 'field' => 'cmt_author_id', 'operator' => '='),
+            ),
+            'paginate' => array('start' => 0),
+            'sorting' => 'last',
+			'ident' => 'cmt_id'
+        );
+
+        $this->sBrowseUrl = BX_DOL_SEARCH_KEYWORD_PAGE;
+    }
+
+    function getMain()
+    {
+        return BxDolModule::getInstance($this->sModule);
+    }
+
+    function displaySearchUnit($aData)
+    {
+		$oCmts = BxDolCmts::getObjectInstance($this->sModuleObjectComments, $aData['object_id']);
+		$oCmts->addCssJs();
+
+		if($this->_bLiveSearch) {
+			$aCnf = &$this->oModule->_oConfig->CNF;
+
+			return $oCmts->getCommentLiveSearch($aData['id'], array(
+				'txt_sample_single' => isset($aCnf['T']['txt_sample_comment_single']) ? $aCnf['T']['txt_sample_comment_single'] : ''
+			));
+		}
+		else
+			return $oCmts->getComment(isset($aData['cmt_id']) ? $aData['cmt_id'] : $aData['id'], array(), array('type' => BX_CMT_DISPLAY_FLAT, 'view_only' => true));
+    }
+
+    function displayResultBlock ()
+    {
+        $s = parent::displayResultBlock ();
+        $s = '<ul class="cmts">' . $s . '</ul>';
+        return $s;
+    }
+
+    function getAlterOrder()
+    {
+        if($this->aCurrent['sorting'] == 'last')
+            return array(
+            	'order' => " ORDER BY `" . $this->aCurrent['table'] . "`.`cmt_time` DESC"
+            );
+
+        return array();
+    }
+
+    function _getPseud ()
+    {
+        return array(
+            'id' => 'cmt_id',
+        	'object_id' => 'cmt_object_id',
+            'author' => 'cmt_author_id',
+            'text' => 'cmt_text',
+            'added' => 'cmt_time'
+        );
+    }
+}
+
+/** @} */

+ 201 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/classes/BxBaseModGeneralDb.php

@@ -0,0 +1,201 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/*
+ * Module database queries
+ */
+class BxBaseModGeneralDb extends BxDolModuleDb
+{
+    protected $_oConfig;
+
+    public function __construct(&$oConfig)
+    {
+        parent::__construct($oConfig);
+        $this->_oConfig = $oConfig;
+    }
+
+    public function getContentInfoById ($iContentId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($CNF['TABLE_ENTRIES']) || empty($CNF['FIELD_ID']))
+            return array();
+
+        $sQuery = $this->prepare ("SELECT * FROM `" . $CNF['TABLE_ENTRIES'] . "` WHERE `" . $CNF['FIELD_ID'] . "` = ?", $iContentId);
+        return $this->getRow($sQuery);
+    }
+
+    function getEntriesBy($aParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+    	$aMethod = array('name' => 'getAll', 'params' => array(0 => 'query', 1 => array()));
+        $sSelectClause = $sJoinClause = $sWhereClause = $sOrderClause = $sLimitClause = "";
+
+        $sSelectClause = "`" . $CNF['TABLE_ENTRIES'] . "`.*";
+
+        switch($aParams['type']) {
+            case 'id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1]['id'] = (int)$aParams['id'];
+
+                $sWhereClause .= " AND `" . $CNF['TABLE_ENTRIES'] . "`.`" . $CNF['FIELD_ID'] . "` = :id";
+                break;
+
+            case 'author':
+            	$aMethod['name'] = 'getRow';
+                $aMethod['params'][1]['author'] = (int)$aParams['author'];
+
+                $sWhereClause .= " AND `" . $CNF['TABLE_ENTRIES'] . "`.`" . $CNF['FIELD_AUTHOR'] . "` = :author";
+                break;
+
+            case 'search_ids':
+                $aMethod['name'] = 'getColumn';
+
+                $sSelectClause = "`" . $CNF['TABLE_ENTRIES'] . "`.`" . $CNF['FIELD_ID'] . "`";
+
+                if (!empty($aParams['start']) && !empty($aParams['per_page']))
+                    $sLimitClause = $this->prepareAsString("?, ?", $aParams['start'], $aParams['per_page']);
+                elseif (!empty($aParams['per_page']))
+                    $sLimitClause = $this->prepareAsString("?", $aParams['per_page']);
+        
+                $sWhereConditions = "1";
+                foreach($aParams['search_params'] as $sSearchParam => $aSearchParam) {
+                    $sSearchValue = "";
+                    switch ($aSearchParam['operator']) {
+                        case 'like':
+                            $sSearchValue = " LIKE " . $this->escape("%" . $aSearchParam['value'] . "%");
+                            break;
+
+                        case 'in':
+                            $sSearchValue = " IN (" . $this->implode_escape($aSearchParam['value']) . ")";
+                            break;
+
+                        case 'and':
+                            $iResult = 0;
+                            if (is_array($aSearchParam['value']))
+                                foreach ($aSearchParam['value'] as $iValue)
+                                    $iResult |= pow (2, $iValue - 1);
+                            else 
+                                $iResult = (int)$aSearchParam['value'];
+
+                            $sSearchValue = " & " . $iResult . "";
+                            break;
+
+                        case 'locate':
+                            if(!isset($CNF['OBJECT_METATAGS']))
+                                break;
+
+                            list($fLatitude, $fLongitude, $sCountry, $sState, $sCity, $sZip) = $aSearchParam['value'];
+
+                            $aSql = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS'])->locationsGetAsSQLPart($CNF['TABLE_ENTRIES'], $CNF['FIELD_ID'], $sCountry, $sState, $sCity, $sZip);
+
+                            if(!empty($aSql['where'])) {
+                                $sWhereConditions .= $aSql['where'];
+
+                                if(!empty($aSql['join']))
+                                    $sJoinClause .= $aSql['join'];
+                            }
+                            break;
+
+                        case 'between':
+                            if(!is_array($aSearchParam['value']) || count($aSearchParam['value']) != 2) 
+                                break;
+
+                            $sWhereConditions .= " AND `" . $CNF['TABLE_ENTRIES'] . "`.`" . $sSearchParam . "` >= :" . $sSearchParam . "_from";
+                            $sWhereConditions .= " AND `" . $CNF['TABLE_ENTRIES'] . "`.`" . $sSearchParam . "` <= :" . $sSearchParam . "_to";
+
+                            $aMethod['params'][1][$sSearchParam . "_from"] = $aSearchParam['value'][0]; 
+                            $aMethod['params'][1][$sSearchParam . "_to"] = $aSearchParam['value'][1]; 
+                            break;
+
+                        default:
+                            if(empty($aSearchParam['operator']))
+                                break;
+
+                            $sSearchValue = " " . $aSearchParam['operator'] . " :" . $sSearchParam;
+                            $aMethod['params'][1][$sSearchParam] = $aSearchParam['value'];                             
+                    }
+
+                    if(!empty($sSearchValue))
+                        $sWhereConditions .= " AND `" . $CNF['TABLE_ENTRIES'] . "`.`" . $sSearchParam . "`" . $sSearchValue;
+                }
+
+                $sWhereClause .= " AND (" . $sWhereConditions . ")"; 
+
+                $sOrderClause .=  "`" . $CNF['TABLE_ENTRIES'] . "`.`" . $CNF['FIELD_ADDED'] . "` ASC";
+                break;
+
+            case 'all_ids':
+                $aMethod['name'] = 'getColumn';
+
+                $sSelectClause = "`" . $CNF['TABLE_ENTRIES'] . "`.`" . $CNF['FIELD_ID'] . "`";
+                $sOrderClause .=  "`" . $CNF['TABLE_ENTRIES'] . "`.`" . $CNF['FIELD_ADDED'] . "` ASC";
+                break;
+
+            case 'all':
+                $sOrderClause .=  "`" . $CNF['TABLE_ENTRIES'] . "`.`" . $CNF['FIELD_ADDED'] . "` ASC";
+                break;
+        }
+
+        if(!empty($sOrderClause))
+            $sOrderClause = 'ORDER BY ' . $sOrderClause;
+
+        if(!empty($sLimitClause))
+            $sLimitClause = 'LIMIT ' . $sLimitClause;
+
+        $aMethod['params'][0] = "SELECT " . $sSelectClause . " FROM `" . $CNF['TABLE_ENTRIES'] . "` " . $sJoinClause . " WHERE 1 " . $sWhereClause . " " . $sOrderClause . " " . $sLimitClause;
+		return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+
+    public function getEntriesByAuthor ($iProfileId)
+    {
+        $sQuery = $this->prepare ("SELECT * FROM `" . $this->_oConfig->CNF['TABLE_ENTRIES'] . "` WHERE `" . $this->_oConfig->CNF['FIELD_AUTHOR'] . "` = ? ORDER BY `" . $this->_oConfig->CNF['FIELD_ADDED'] . "` DESC", $iProfileId);
+        return $this->getAll($sQuery);
+    }
+
+    public function getEntriesNumByAuthor ($iProfileId)
+    {
+        $sQuery = $this->prepare ("SELECT COUNT(*) FROM `" . $this->_oConfig->CNF['TABLE_ENTRIES'] . "` WHERE `" . $this->_oConfig->CNF['FIELD_AUTHOR'] . "` = ?", $iProfileId);
+        return $this->getOne($sQuery);
+    }
+
+    public function alterFulltextIndex ()
+    {
+        $CNF = $this->_oConfig->CNF;        
+
+        $bFulltextIndex = false;
+        $aIndexes = $this->getAll("SHOW INDEXES FROM `" . $CNF['TABLE_ENTRIES'] . "`");
+
+        foreach ($aIndexes as $r) {
+            if ($CNF['TABLE_ENTRIES_FULLTEXT'] == $r['Key_name']) {
+                $bFulltextIndex = true;
+                break;
+            }
+        }
+
+        if ($bFulltextIndex)
+            $this->query("ALTER TABLE `" . $CNF['TABLE_ENTRIES'] . "` DROP INDEX `" . $CNF['TABLE_ENTRIES_FULLTEXT'] . "`");
+
+        $sFields = getParam($CNF['PARAM_SEARCHABLE_FIELDS']);
+        if (!$sFields || !($aFields = explode(',', $sFields)))
+            return true;
+
+        $sFields = '';
+        foreach ($aFields as $s)
+            $sFields .= "`$s`,";
+
+        return $this->query("ALTER TABLE `" . $CNF['TABLE_ENTRIES'] . "` ADD FULLTEXT `" . $CNF['TABLE_ENTRIES_FULLTEXT'] . "` (" . trim($sFields, ', ') . ")");
+    }
+
+}
+
+/** @} */

+ 285 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/classes/BxBaseModGeneralFormEntry.php

@@ -0,0 +1,285 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Create/edit entry form
+ */
+class BxBaseModGeneralFormEntry extends BxTemplFormView
+{
+    protected $MODULE;
+
+    protected $_oModule;
+
+    protected $_aMetatagsFieldsWithKeywords = array();
+    protected $_oMetatagsObject = null;
+    protected $_oMetatagsContentId = 0;
+
+    protected $_sGhostTemplate = 'form_ghost_template.html';
+
+    public function __construct($aInfo, $oTemplate = false)
+    {
+        parent::__construct($aInfo, $oTemplate);
+
+        $this->_oModule = BxDolModule::getInstance($this->MODULE);
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        
+        if (isset($CNF['FIELD_ADDED']) && isset($this->aInputs[$CNF['FIELD_ADDED']])) {
+            $this->aInputs[$CNF['FIELD_ADDED']]['date_filter'] = BX_DATA_INT;
+            $this->aInputs[$CNF['FIELD_ADDED']]['date_format'] = BX_FORMAT_DATE;
+        }
+
+        if (isset($CNF['FIELD_CHANGED']) && isset($this->aInputs[$CNF['FIELD_CHANGED']])) {
+            $this->aInputs[$CNF['FIELD_CHANGED']]['date_filter'] = BX_DATA_INT;
+            $this->aInputs[$CNF['FIELD_CHANGED']]['date_format'] = BX_FORMAT_DATE;
+        }
+
+        if (isset($this->aInputs[$CNF['FIELD_TEXT']]) && isset($CNF['FIELD_TEXT_ID'])) {
+            $this->aInputs[$CNF['FIELD_TEXT']]['attrs'] = array_merge (
+                array ('id' => $CNF['FIELD_TEXT_ID']),
+                is_array($this->aInputs[$CNF['FIELD_TEXT']]['attrs']) ? $this->aInputs[$CNF['FIELD_TEXT']]['attrs'] : array ()
+            );
+        }
+        
+        if (isset($CNF['FIELD_LOCATION_PREFIX']) && isset($this->aInputs[$CNF['FIELD_LOCATION_PREFIX']])) {
+            $this->aInputs[$CNF['FIELD_LOCATION_PREFIX']]['manual_input'] = true;
+        }
+
+        if (isset($CNF['FIELD_PHOTO']) && isset($this->aInputs[$CNF['FIELD_PHOTO']])) {
+            $this->aInputs[$CNF['FIELD_PHOTO']]['storage_object'] = $CNF['OBJECT_STORAGE'];
+            $this->aInputs[$CNF['FIELD_PHOTO']]['uploaders'] = !empty($this->aInputs[$CNF['FIELD_PHOTO']]['value']) ? unserialize($this->aInputs[$CNF['FIELD_PHOTO']]['value']) : $CNF['OBJECT_UPLOADERS'];
+            $this->aInputs[$CNF['FIELD_PHOTO']]['images_transcoder'] = $CNF['OBJECT_IMAGES_TRANSCODER_PREVIEW'];
+            $this->aInputs[$CNF['FIELD_PHOTO']]['storage_private'] = 0;
+            $this->aInputs[$CNF['FIELD_PHOTO']]['multiple'] = true;
+            $this->aInputs[$CNF['FIELD_PHOTO']]['content_id'] = 0;
+            $this->aInputs[$CNF['FIELD_PHOTO']]['ghost_template'] = '';
+        }
+    }
+
+    function initChecker ($aValues = array (), $aSpecificValues = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        
+        if (!empty($CNF['FIELD_LOCATION_PREFIX']) && isset($this->aInputs[$CNF['FIELD_LOCATION_PREFIX']]) && isset($aValues[$CNF['FIELD_ID']]) && !empty($CNF['OBJECT_METATAGS']) && ($oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS'])) && $oMetatags->locationsIsEnabled())
+            $this->aInputs[$CNF['FIELD_LOCATION_PREFIX']]['value'] = $oMetatags->locationsString($aValues[$CNF['FIELD_ID']], false);
+
+        if (isset($CNF['FIELD_PHOTO']) && isset($this->aInputs[$CNF['FIELD_PHOTO']])) {
+
+            $aContentInfo = false;
+            if ($aValues && !empty($aValues['id'])) {
+                $aContentInfo = $this->_oModule->_oDb->getContentInfoById ($aValues['id']);
+                $this->aInputs[$CNF['FIELD_PHOTO']]['content_id'] = $aValues['id'];
+            }
+
+            $this->aInputs[$CNF['FIELD_PHOTO']]['ghost_template'] = $this->_oModule->_oTemplate->parseHtmlByName($this->_sGhostTemplate, $this->_getPhotoGhostTmplVars($aContentInfo));
+        }
+        
+        parent::initChecker ($aValues, $aSpecificValues);
+    }
+    
+    public function insert ($aValsToAdd = array(), $isIgnore = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (isset($CNF['FIELD_AUTHOR']) && empty($aValsToAdd[$CNF['FIELD_AUTHOR']]))
+            $aValsToAdd[$CNF['FIELD_AUTHOR']] = bx_get_logged_profile_id ();
+
+        if (isset($CNF['FIELD_ADDED']) && empty($aValsToAdd[$CNF['FIELD_ADDED']]) && empty($this->getCleanValue($CNF['FIELD_ADDED'])))
+            $aValsToAdd[$CNF['FIELD_ADDED']] = time();
+
+        if (isset($CNF['FIELD_CHANGED']) && empty($aValsToAdd[$CNF['FIELD_CHANGED']]) && empty($this->getCleanValue($CNF['FIELD_CHANGED'])))
+            $aValsToAdd[$CNF['FIELD_CHANGED']] = time();
+
+        if (CHECK_ACTION_RESULT_ALLOWED === $this->_oModule->checkAllowedSetThumb() && isset($CNF['FIELD_THUMB'])) {
+            $aThumb = isset($_POST[$CNF['FIELD_THUMB']]) ? bx_process_input ($_POST[$CNF['FIELD_THUMB']], BX_DATA_INT) : false;
+            $aValsToAdd[$CNF['FIELD_THUMB']] = 0;
+            if (!empty($aThumb) && is_array($aThumb) && ($iFileThumb = array_pop($aThumb)))
+                $aValsToAdd[$CNF['FIELD_THUMB']] = $iFileThumb;
+        }
+        
+        return parent::insert ($aValsToAdd, $isIgnore);
+    }
+
+    function update ($iContentId, $aValsToAdd = array(), &$aTrackTextFieldsChanges = null)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (isset($CNF['FIELD_CHANGED']) && empty($aValsToAdd[$CNF['FIELD_CHANGED']]) && empty($this->getCleanValue($CNF['FIELD_CHANGED'])))
+            $aValsToAdd[$CNF['FIELD_CHANGED']] = time();
+
+        if (CHECK_ACTION_RESULT_ALLOWED === $this->_oModule->checkAllowedSetThumb($iContentId) && isset($CNF['FIELD_THUMB']) && isset($CNF['FIELD_PHOTO']) && isset($this->aInputs[$CNF['FIELD_PHOTO']])) {
+            $aThumb = bx_process_input (bx_get($CNF['FIELD_THUMB']), BX_DATA_INT);
+            $aValsToAdd[$CNF['FIELD_THUMB']] = 0;
+            if (!empty($aThumb) && is_array($aThumb) && ($iFileThumb = array_pop($aThumb)))
+                $aValsToAdd[$CNF['FIELD_THUMB']] = $iFileThumb;
+        }
+        
+        return parent::update ($iContentId, $aValsToAdd, $aTrackTextFieldsChanges);
+    }
+
+    protected function _associalFileWithContent($oStorage, $iFileId, $iProfileId, $iContentId, $sPictureField = '')
+    {
+        $oStorage->updateGhostsContentId ($iFileId, $iProfileId, $iContentId, $this->_isAdmin($iContentId));
+    }
+
+    protected function _getPhotoGhostTmplVars($aContentInfo = array())
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+    	return array (
+			'name' => $this->aInputs[$CNF['FIELD_PHOTO']]['name'],
+			'content_id' => $this->aInputs[$CNF['FIELD_PHOTO']]['content_id'],
+			'editor_id' => isset($CNF['FIELD_TEXT_ID']) ? $CNF['FIELD_TEXT_ID'] : '',
+			'thumb_id' => isset($CNF['FIELD_THUMB']) && isset($aContentInfo[$CNF['FIELD_THUMB']]) ? $aContentInfo[$CNF['FIELD_THUMB']] : 0,
+			'bx_if:set_thumb' => array (
+				'condition' => CHECK_ACTION_RESULT_ALLOWED === $this->_oModule->checkAllowedSetThumb($this->aInputs[$CNF['FIELD_PHOTO']]['content_id']),
+				'content' => array (
+					'name_thumb' => isset($CNF['FIELD_THUMB']) ? $CNF['FIELD_THUMB'] : '',
+    				'txt_pict_use_as_thumb' => _t(!empty($CNF['T']['txt_pict_use_as_thumb']) ? $CNF['T']['txt_pict_use_as_thumb'] : '_sys_txt_form_entry_input_picture_use_as_thumb')
+				),
+			),
+		);
+    }
+    
+    function delete ($iContentId, $aContentInfo = array())
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // delete associated files
+
+        if (!empty($CNF['OBJECT_STORAGE'])) {
+            $oStorage = BxDolStorage::getObjectInstance($CNF['OBJECT_STORAGE']);
+            if ($oStorage)
+                $oStorage->queueFilesForDeletionFromGhosts($aContentInfo[$CNF['FIELD_AUTHOR']], $iContentId);
+        }
+
+        // delete associated objects data
+
+        if (!empty($CNF['OBJECT_VIEWS'])) {
+            $o = BxDolView::getObjectInstance($CNF['OBJECT_VIEWS'], $iContentId);
+            if ($o) $o->onObjectDelete();
+        }
+
+        if (!empty($CNF['OBJECT_VOTES'])) {
+            $o = BxDolVote::getObjectInstance($CNF['OBJECT_VOTES'], $iContentId);
+            if ($o) $o->onObjectDelete();
+        }
+
+        if (!empty($CNF['OBJECT_COMMENTS'])) {
+            $o = BxDolCmts::getObjectInstance($CNF['OBJECT_COMMENTS'], $iContentId);
+            if ($o) $o->onObjectDelete();
+        }
+
+        if (!empty($CNF['OBJECT_METATAGS'])) {
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+            $oMetatags->onDeleteContent($iContentId);
+        }
+
+        // delete db record
+
+        return parent::delete($iContentId);
+    }
+
+    protected function _isAdmin ($iContentId = 0)
+    {
+        return $this->_oModule->_isModerator();
+    }
+
+    public function processFiles ($sFieldFile, $iContentId = 0, $isAssociateWithContent = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (!isset($this->aInputs[$sFieldFile]))
+            return true;
+
+        $mixedFileIds = $this->getCleanValue($sFieldFile);
+        if(!$mixedFileIds)
+            return true;
+
+        $oStorage = BxDolStorage::getObjectInstance($this->aInputs[$sFieldFile]['storage_object']);
+        if (!$oStorage)
+            return false;
+
+        $iProfileId = $this->getContentOwnerProfileId($iContentId);
+
+        $aGhostFiles = $oStorage->getGhosts ($iProfileId, $isAssociateWithContent ? 0 : $iContentId, true, $this->_isAdmin($iContentId));
+        if (!$aGhostFiles)
+            return true;
+
+        foreach ($aGhostFiles as $aFile) {
+            if (is_array($mixedFileIds) && !in_array($aFile['id'], $mixedFileIds))
+                continue;
+
+            if ($aFile['private'])
+                $oStorage->setFilePrivate ($aFile['id'], 1);
+
+            if ($iContentId)
+                $this->_associalFileWithContent($oStorage, $aFile['id'], $iProfileId, $iContentId, $sFieldFile);
+        }
+
+        return true;
+    }
+
+    function _deleteFile ($iFileId, $sStorage = '')
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (!$iFileId)
+            return true;
+
+		$sStorage = !empty($sStorage) ? $sStorage : $CNF['OBJECT_STORAGE'];
+		$oStorage = BxDolStorage::getObjectInstance($sStorage);
+        if (!$oStorage)
+            return false;
+
+        if (!$oStorage->getFile($iFileId))
+            return true;
+
+        $iProfileId = bx_get_logged_profile_id();
+        return $oStorage->deleteFile($iFileId, $iProfileId);
+    }
+
+    function addCssJs ()
+    {
+        if (!isset($this->aParams['view_mode']) || !$this->aParams['view_mode']) {
+            if (self::$_isCssJsAdded)
+                return;
+            $this->_oModule->_oTemplate->addCss('forms.css');
+            $this->_oModule->_oTemplate->addJs('modules/base/general/js/|forms.js');
+        }
+
+        return parent::addCssJs ();
+    }
+    
+    function genViewRowValue(&$aInput)
+    {
+        $s = parent::genViewRowValue($aInput);
+
+        if ($this->_oMetatagsObject && in_array($aInput['name'], $this->_aMetatagsFieldsWithKeywords) && $s)
+            $s = $this->_oMetatagsObject->metaParse($this->_oMetatagsContentId, $s);
+
+        return $s;
+    }
+
+    function setMetatagsKeywordsData($iId, $a, $o)
+    {
+        $this->_oMetatagsContentId = $iId;
+        $this->_aMetatagsFieldsWithKeywords = $a;
+        $this->_oMetatagsObject = $o;
+    }
+
+    function getContentOwnerProfileId ($iContentId)
+    {
+        return $this->_oModule->serviceGetContentOwnerProfileId($iContentId);
+    }
+}
+
+/** @} */

+ 439 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/classes/BxBaseModGeneralFormsEntryHelper.php

@@ -0,0 +1,439 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Entry forms helper functions
+ */
+class BxBaseModGeneralFormsEntryHelper extends BxDolProfileForms
+{
+    protected $_oModule;
+
+    public function __construct($oModule)
+    {
+        parent::__construct();
+        $this->_oModule = $oModule;
+    }
+
+    public function getObjectStorage()
+    {
+        return BxDolStorage::getObjectInstance($this->_oModule->_oConfig->CNF['OBJECT_STORAGE']);
+    }
+
+    public function getObjectFormAdd ($sDisplay = false)
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (false === $sDisplay)
+            $sDisplay = $CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD'];
+        
+        return BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $sDisplay, $this->_oModule->_oTemplate);
+    }
+
+    public function getObjectFormEdit ($sDisplay = false)
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (false === $sDisplay)
+            $sDisplay = $CNF['OBJECT_FORM_ENTRY_DISPLAY_EDIT'];
+
+        return BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $sDisplay, $this->_oModule->_oTemplate);
+    }
+
+	public function getObjectFormView ($sDisplay = false)
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (false === $sDisplay)
+            $sDisplay = $CNF['OBJECT_FORM_ENTRY_DISPLAY_VIEW'];
+
+        return BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $sDisplay, $this->_oModule->_oTemplate);
+    }
+
+    public function getObjectFormDelete ($sDisplay = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (false === $sDisplay)
+            $sDisplay = $CNF['OBJECT_FORM_ENTRY_DISPLAY_DELETE'];
+
+        return BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $sDisplay, $this->_oModule->_oTemplate);
+    }
+
+    public function viewDataEntry ($iContentId)
+    {
+        // get content data and profile info
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        if (!$aContentInfo)
+            return MsgBox(_t('_sys_txt_error_entry_is_not_defined'));
+
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->checkAllowedView($aContentInfo)))
+            return MsgBox($sMsg);
+
+        return $this->_oModule->_oTemplate->entryText($aContentInfo);
+    }
+
+    public function addData ($iProfile, $aValues, $sDisplay = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // check and display form
+        $oForm = $this->getObjectFormAdd($sDisplay);
+        if (!$oForm)
+            return array('code' => 1, 'message' => '_sys_txt_error_occured');
+
+        $oForm->aFormAttrs['method'] = BX_DOL_FORM_METHOD_SPECIFIC;
+        $oForm->aParams['csrf']['disable'] = true;
+        if(!empty($oForm->aParams['db']['submit_name']))
+            $aValues[$oForm->aParams['db']['submit_name']] = $oForm->aInputs[$oForm->aParams['db']['submit_name']]['value'];
+
+        $oForm->initChecker(array(), $aValues);
+        if (!$oForm->isSubmittedAndValid())
+            return array('code' => 2, 'message' => '_sys_txt_error_occured');
+
+        // insert data into database
+        $aValsToAdd = array ();
+        if(isset($CNF['FIELD_AUTHOR']))
+            $aValsToAdd[$CNF['FIELD_AUTHOR']] = $iProfile;
+
+        $iContentId = $oForm->insert($aValsToAdd);
+        if (!$iContentId) {
+            if (!$oForm->isValid())
+                return array('code' => 2, 'message' => '_sys_txt_error_occured');
+            else
+                return array('code' => 3, 'message' => '_sys_txt_error_entry_creation');
+        }
+
+        $sResult = $this->onDataAddAfter(BxDolProfile::getInstance($iProfile)->getAccountId(), $iContentId);
+        if($sResult)
+            return array('code' => 4, 'message' => $sResult);
+
+        // process uploaded files
+        if (isset($CNF['FIELD_PHOTO']))
+            $oForm->processFiles ($CNF['FIELD_PHOTO'], $iContentId, true);
+        
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        return array('code' => 0, 'message' => '', 'content' => $aContentInfo);
+    }
+
+    public function addDataForm ($sDisplay = false, $sCheckFunction = false)
+    {
+        if (!$sCheckFunction)
+            $sCheckFunction = 'checkAllowedAdd';
+        
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->$sCheckFunction())) {
+            $oProfile = BxDolProfile::getInstance();
+            if ($oProfile && ($aProfileInfo = $oProfile->getInfo()) && $aProfileInfo['type'] == 'system' && is_subclass_of($this->_oModule, 'BxBaseModProfileModule') && $this->_oModule->serviceActAsProfile()) // special check for system profile is needed, because of incorrect error message
+                return MsgBox(_t('_sys_txt_access_denied'));
+            else
+                return MsgBox($sMsg);
+        }
+
+        // check and display form
+        $oForm = $this->getObjectFormAdd($sDisplay);
+        if (!$oForm)
+            return MsgBox(_t('_sys_txt_error_occured'));
+
+        $oForm->initChecker();
+
+        if (!$oForm->isSubmittedAndValid())
+            return $oForm->getCode();
+
+        // insert data into database
+        $aValsToAdd = array ();
+        $iContentId = $oForm->insert ($aValsToAdd);
+        if (!$iContentId) {
+            if (!$oForm->isValid())
+                return $oForm->getCode();
+            else
+                return MsgBox(_t('_sys_txt_error_entry_creation'));
+        }
+
+        $sResult = $this->onDataAddAfter (getLoggedId(), $iContentId);
+        if ($sResult)
+            return $sResult;
+
+        // process uploaded files
+        if (isset($CNF['FIELD_PHOTO']))
+            $oForm->processFiles ($CNF['FIELD_PHOTO'], $iContentId, true);
+        
+        // perform action
+        $this->_oModule->checkAllowedAdd(true);
+
+        // redirect
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        $this->redirectAfterAdd($aContentInfo);
+    }
+    
+    public function redirectAfterAdd($aContentInfo)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        $this->_redirectAndExit('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+    }
+
+    public function editDataForm ($iContentId, $sDisplay = false, $sCheckFunction = false)
+    {
+        if (!$sCheckFunction)
+            $sCheckFunction = 'checkAllowedEdit';
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // get content data and profile info
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        if (!$aContentInfo)
+            return MsgBox(_t('_sys_txt_error_entry_is_not_defined'));
+
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->$sCheckFunction($aContentInfo)))
+            return MsgBox($sMsg);
+
+        // check and display form
+        $oForm = $this->getObjectFormEdit($sDisplay);
+        if (!$oForm)
+            return MsgBox(_t('_sys_txt_error_occured'));
+
+        $aSpecificValues = array();        
+        if (!empty($CNF['OBJECT_METATAGS'])) {
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+            if ($oMetatags->locationsIsEnabled())
+                $aSpecificValues = $oMetatags->locationGet($iContentId, empty($CNF['FIELD_LOCATION_PREFIX']) ? '' : $CNF['FIELD_LOCATION_PREFIX']);
+        }
+        $oForm->initChecker($aContentInfo, $aSpecificValues);
+
+        if (!$oForm->isSubmittedAndValid())
+            return $oForm->getCode();
+
+        // update data in the DB
+        $aTrackTextFieldsChanges = null;
+
+        $this->onDataEditBefore ($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $aTrackTextFieldsChanges);
+
+        if (!$oForm->update ($aContentInfo[$CNF['FIELD_ID']], array(), $aTrackTextFieldsChanges)) {
+            if (!$oForm->isValid())
+                return $oForm->getCode();
+            else
+                return MsgBox(_t('_sys_txt_error_entry_update'));
+        }
+
+        $sResult = $this->onDataEditAfter ($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $aTrackTextFieldsChanges, $oProfile, $oForm);
+        if ($sResult)
+            return $sResult;
+
+        // process uploaded files
+        if (isset($CNF['FIELD_PHOTO']))
+            $oForm->processFiles ($CNF['FIELD_PHOTO'], $iContentId, false);
+
+        // perform action
+        $this->_oModule->checkAllowedEdit($aContentInfo, true);
+        
+        // redirect
+        $this->redirectAfterEdit($aContentInfo);
+    }
+
+    protected function redirectAfterEdit($aContentInfo)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        $this->_redirectAndExit('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+    }
+
+    public function deleteDataForm ($iContentId, $sDisplay = false, $sCheckFunction = false)
+    {
+        if (!$sCheckFunction)
+            $sCheckFunction = 'checkAllowedDelete';
+        
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // get content data and profile info
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        if (!$aContentInfo)
+            return MsgBox(_t('_sys_txt_error_entry_is_not_defined'));
+
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->$sCheckFunction($aContentInfo)))
+            return MsgBox($sMsg);
+
+        // check and display form
+        $oForm = $this->getObjectFormDelete($sDisplay);
+        if (!$oForm)
+            return MsgBox(_t('_sys_txt_error_occured'));
+
+        $oForm->initChecker($aContentInfo);
+
+        if (!$oForm->isSubmittedAndValid())
+            return $oForm->getCode();
+
+        if ($sError = $this->deleteData($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $oProfile, $oForm))
+            return MsgBox($sError);
+
+        // perform action
+        $this->_oModule->checkAllowedDelete($aContentInfo, true);
+
+        // redirect
+        $this->redirectAfterDelete($aContentInfo);
+    }
+
+    protected function redirectAfterDelete($aContentInfo)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        $this->_redirectAndExit($CNF['URL_HOME'], true, array(
+            'account_id' => getLoggedId(),
+            'profile_id' => bx_get_logged_profile_id(),
+        ));
+    }
+
+    /**
+     * Delete data entry
+     * @param $iContentId entry id
+     * @param $oForm optional content info array
+     * @param $aContentInfo optional content info array
+     * @param $oProfile optional content author profile
+     * @return error string on error or empty string on success
+     */
+    public function deleteData ($iContentId, $aContentInfo = false, $oProfile = null, $oForm = null)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (!$aContentInfo || !$oProfile)
+            list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+
+        if (!$aContentInfo)
+            return _t('_sys_txt_error_entry_is_not_defined');
+
+        if (!$oForm)
+            $oForm = $this->getObjectFormDelete();
+
+        if (!$oForm->delete ($aContentInfo[$CNF['FIELD_ID']], $aContentInfo))
+            return _t('_sys_txt_error_entry_delete');
+
+        if ($sResult = $this->onDataDeleteAfter ($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $oProfile))
+            return $sResult;
+
+        // create an alert
+        bx_alert($this->_oModule->getName(), 'deleted', $aContentInfo[$CNF['FIELD_ID']]);
+
+        return '';
+    }
+
+    public function viewDataForm ($iContentId, $sDisplay = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // get content data and profile info
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        if (!$aContentInfo)
+            return MsgBox(_t('_sys_txt_error_entry_is_not_defined'));
+
+        // check access
+        if ($sMsg = $this->_processPermissionsCheckForViewDataForm ($aContentInfo, $oProfile))
+            return MsgBox($sMsg);
+
+        // get form
+        $oForm = $this->getObjectFormView($sDisplay);
+        if (!$oForm)
+            return MsgBox(_t('_sys_txt_error_occured'));
+
+        // process metatags
+        if (!empty($CNF['OBJECT_METATAGS'])) {
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+            if ($oMetatags->keywordsIsEnabled()) {
+                $aFields = $oMetatags->metaFields($aContentInfo, $CNF, $CNF['OBJECT_FORM_ENTRY_DISPLAY_VIEW']);
+                $oForm->setMetatagsKeywordsData($iContentId, $aFields, $oMetatags);
+            }
+        }        
+
+        // display profile
+        $oForm->initChecker($aContentInfo);
+        return $oForm->getCode();
+    }
+
+    protected function _processPermissionsCheckForViewDataForm ($aContentInfo, $oProfile)
+    {
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->checkAllowedView($aContentInfo)))
+            return $sMsg;
+
+        return '';
+    }
+
+    public function onDataDeleteAfter ($iContentId, $aContentInfo, $oProfile)
+    {
+        return '';
+    }
+
+    public function onDataEditBefore ($iContentId, $aContentInfo, &$aTrackTextFieldsChanges)
+    {
+    }
+
+    public function onDataEditAfter ($iContentId, $aContentInfo, $aTrackTextFieldsChanges, $oProfile, $oForm)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (!empty($CNF['OBJECT_METATAGS'])) { // && isset($aTrackTextFieldsChanges['changed_fields'][$CNF['FIELD_TEXT']])) { // TODO: check if aTrackTextFieldsChanges works 
+            list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+            $oMetatags->metaAddAuto($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $CNF, $CNF['OBJECT_FORM_ENTRY_DISPLAY_EDIT']);
+            if (isset($CNF['FIELD_LOCATION_PREFIX']) && isset($oForm->aInputs[$CNF['FIELD_LOCATION_PREFIX']]) && $oMetatags->locationsIsEnabled())
+                $oMetatags->locationsAddFromForm($aContentInfo[$CNF['FIELD_ID']], empty($CNF['FIELD_LOCATION_PREFIX']) ? '' : $CNF['FIELD_LOCATION_PREFIX']);
+        }
+
+        return '';
+    }
+
+    public function onDataAddAfter ($iAccountId, $iContentId)
+    {
+        $this->_processMetas($iAccountId, $iContentId);
+
+        return '';
+    }
+
+    protected function _processMetas($iAccountId, $iContentId)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (!empty($CNF['OBJECT_METATAGS'])) {
+            list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+            if ($aContentInfo)
+                $oMetatags->metaAddAuto($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $CNF, $CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD']);
+            if ($oMetatags->locationsIsEnabled() && $aContentInfo)
+                $oMetatags->locationsAddFromForm($aContentInfo[$CNF['FIELD_ID']], $CNF['FIELD_LOCATION_PREFIX']);
+        }
+    }
+
+    protected function prepareCustomRedirectUrl($s, $aContentInfo)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aContentInfo[$CNF['FIELD_ID']], $this->_oModule->getName());
+
+        $aMarkers = array(
+            '{profile_id}',
+            '{content_id}',
+            '{module}',
+        );
+        $aReplacements = array(
+            $oProfile ? $oProfile->id() : 0,
+            $aContentInfo[$CNF['FIELD_ID']],
+            $this->_oModule->getName(),
+        );
+        $s = str_replace($aMarkers, $aReplacements, $s);
+
+        $s = BxDolPermalinks::getInstance()->permalink($s);
+
+        if (false === strpos($s, 'http://') && false === strpos($s, 'https://'))
+            $s = BX_DOL_URL_ROOT . $s;
+        
+        return $s;
+    }
+}
+
+/** @} */

+ 153 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/classes/BxBaseModGeneralMenuSnippetMeta.php

@@ -0,0 +1,153 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModGeneralMenuSnippetMeta extends BxTemplMenuCustom
+{
+    protected $_sModule;
+    protected $_oModule;
+
+    protected $_iContentId;
+    protected $_aContentInfo;
+
+    public function __construct($aObject, $oTemplate = false)
+    {
+        parent::__construct($aObject, $oTemplate);
+
+        $this->_oModule = BxDolModule::getInstance($this->_sModule);
+    }
+
+    public function getCode()
+    {
+        if(empty($this->_aContentInfo) || !is_array($this->_aContentInfo))
+            return '';
+
+        return parent::getCode();
+    }
+
+    public function setContentId($iContentId)
+    {
+        $this->_iContentId = (int)$iContentId;
+
+        $this->_aContentInfo = $this->_oModule->_oDb->getContentInfoById($this->_iContentId);
+        if($this->_aContentInfo)
+            $this->addMarkers(array('content_id' => (int)$this->_iContentId));
+    }
+
+    protected function _getMenuItemAuthor($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $oProfile = BxDolProfile::getInstance($this->_aContentInfo[$CNF['FIELD_AUTHOR']]);
+        if(!$oProfile) 
+            $oProfile = BxDolProfileUndefined::getInstance();
+
+        return $this->_oTemplate->getUnitMetaItemLink($oProfile->getDisplayName(), array(
+            'href' => $oProfile->getUrl(),
+            'class' => 'bx-base-text-unit-author'
+        ));
+    }
+
+    protected function _getMenuItemDate($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        return $this->_oTemplate->getUnitMetaItemText(bx_time_js($this->_aContentInfo[$CNF['FIELD_ADDED']], BX_FORMAT_DATE));
+    }
+
+    protected function _getMenuItemCategory($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($CNF['OBJECT_CATEGORY']) || empty($CNF['FIELD_CATEGORY']) || empty($this->_aContentInfo[$CNF['FIELD_CATEGORY']]))
+            return false;
+
+        $oCategory = BxDolCategory::getObjectInstance($CNF['OBJECT_CATEGORY']);
+        if(!$oCategory)
+            return false;
+
+        $sTitle = $oCategory->getCategoryTitle($this->_aContentInfo[$CNF['FIELD_CATEGORY']]);
+        return $this->_oTemplate->getUnitMetaItemCustom($oCategory->getCategoryLink($sTitle, $this->_aContentInfo[$CNF['FIELD_CATEGORY']]));
+    }
+
+    protected function _getMenuItemTags($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($CNF['OBJECT_METATAGS']))
+            return false;
+
+        $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+        if(!$oMetatags || !$oMetatags->keywordsIsEnabled())
+            return false;
+
+        return $this->_oTemplate->getUnitMetaItemCustom($oMetatags->getKeywordsList($this->_aContentInfo[$CNF['FIELD_ID']], 3));
+    }
+
+    protected function _getMenuItemViews($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($CNF['FIELD_VIEWS']) || empty($this->_aContentInfo[$CNF['FIELD_VIEWS']]))
+            return false;
+
+        return $this->_oTemplate->getUnitMetaItemText(_t('_view_n_views', $this->_aContentInfo[$CNF['FIELD_VIEWS']]));
+    }
+
+    protected function _getMenuItemRating($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($CNF['OBJECT_VOTES_STARS']))
+            return false;
+
+        $oVotes = BxDolVote::getObjectInstance($CNF['OBJECT_VOTES_STARS'], $this->_aContentInfo[$CNF['FIELD_ID']]);
+        if(!$oVotes)
+            return false;
+
+        return $this->_oTemplate->getUnitMetaItemCustom($oVotes->getElementInline(array('show_counter' => true)));
+    }
+
+    protected function _getMenuItemComments($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(empty($CNF['OBJECT_COMMENTS']) || empty($CNF['FIELD_COMMENTS']) || empty($this->_aContentInfo[$CNF['FIELD_COMMENTS']]))
+            return false;
+
+        $oComments = BxDolCmts::getObjectInstance($CNF['OBJECT_COMMENTS'], $this->_aContentInfo[$CNF['FIELD_ID']]);
+        if(!$oComments || !$oComments->isEnabled())
+            return false;
+
+        return $this->_oTemplate->getUnitMetaItemLink(_t('_cmt_txt_n_comments', $this->_aContentInfo[$CNF['FIELD_COMMENTS']]), array(
+            'href' => $oComments->getListUrl()
+        ));
+    }
+
+    protected function _getMenuItemDefault($aItem)
+    {
+        $sResult = '';
+
+        if(!empty($aItem['link']))
+            $sResult = $this->_oTemplate->getUnitMetaItemLink($aItem['title'], array(
+                'href' => $aItem['link']
+            ));
+        else if(!empty($aItem['onclick']))
+            $sResult = $this->_oTemplate->getUnitMetaItemButtonSmall($aItem['title'], array(
+            	'onclick' => $aItem['onclick']
+            ));
+        else 
+            $sResult = $this->_oTemplate->getUnitMetaItemText($aItem['title']);
+        
+        return $sResult;
+    }
+}
+
+/** @} */

+ 1316 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/classes/BxBaseModGeneralModule.php

@@ -0,0 +1,1316 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+bx_import('BxDolAcl');
+
+/**
+ * Base module class.
+ */
+class BxBaseModGeneralModule extends BxDolModule
+{
+    protected $_iProfileId;
+    protected $_aSearchableNamesExcept;
+
+    function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+
+        $this->_iProfileId = bx_get_logged_profile_id();
+        $this->_aSearchableNamesExcept = array(
+            'allow_view_to'
+        );
+    }
+
+    // ====== ACTIONS METHODS
+
+    public function actionRss ()
+    {
+        $aArgs = func_get_args();
+        $this->_rss($aArgs);
+    }
+
+    // ====== SERVICE METHODS    
+    public function serviceGetAuthor ($iContentId)
+    {
+        $mixedResult = $this->_getFieldValue('FIELD_AUTHOR', $iContentId);
+        return $mixedResult !== false ? (int)$mixedResult : 0; 
+    }
+
+    public function serviceGetDateAdded ($iContentId)
+    {
+        $mixedResult = $this->_getFieldValue('FIELD_ADDED', $iContentId);
+        return $mixedResult !== false ? (int)$mixedResult : 0; 
+    }
+
+    public function serviceGetDateChanged ($iContentId)
+    {
+        $mixedResult = $this->_getFieldValue('FIELD_CHANGED', $iContentId);
+        return $mixedResult !== false ? (int)$mixedResult : 0;
+    }
+
+    public function serviceGetLink ($iContentId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if(empty($CNF['URI_VIEW_ENTRY']))
+            return '';
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo))
+            return '';
+
+        return BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+    }
+
+    public function serviceGetTitle ($iContentId)
+    {
+        $mixedResult = $this->_getFieldValue('FIELD_TITLE', $iContentId);
+        return $mixedResult !== false ? $mixedResult : '';
+    }
+
+    public function serviceGetText ($iContentId)
+    {
+        $mixedResult = $this->_getFieldValue('FIELD_TEXT', $iContentId);
+        return $mixedResult !== false ? $mixedResult : '';
+    }
+
+    public function serviceGetInfo ($iContentId, $bSearchableFieldsOnly = true)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aContentInfo = $this->_getFields($iContentId);
+        if(empty($aContentInfo))
+            return array();
+
+        if(!$bSearchableFieldsOnly)
+            return $aContentInfo;
+
+        if(empty($CNF['PARAM_SEARCHABLE_FIELDS']))
+            return array();
+
+        $aFields = explode(',', getParam($CNF['PARAM_SEARCHABLE_FIELDS']));
+        if(empty($aFields))
+            return array();
+
+        $aResult = array();
+        foreach($aFields as $sField)
+            if(isset($aContentInfo[$sField]))
+                $aResult[$sField] = $aContentInfo[$sField];
+
+        return $aResult;
+    }
+
+    public function serviceGetSearchResultUnit ($iContentId, $sUnitTemplate = '')
+    {
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo))
+            return '';
+
+        if(empty($sUnitTemplate))
+            $sUnitTemplate = 'unit.html';
+
+        return $this->_oTemplate->unit($aContentInfo, true, $sUnitTemplate);
+    }
+
+    public function serviceGetAll ($aParams = array())
+    {
+        if(empty($aParams) || !is_array($aParams))
+            $aParams = array('type' => 'all');
+
+        return $this->_oDb->getEntriesBy($aParams);
+    }
+
+    public function serviceGetAllByAuthor ($iProfileId)
+    {
+        return $this->_oDb->getEntriesByAuthor((int)$iProfileId);
+    }
+
+    public function serviceGetSearchableFieldsExtended()
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if(empty($CNF['OBJECT_FORM_ENTRY']) || empty($CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD']))
+            return array();
+
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD'], $this->_oTemplate);
+        if(!$oForm)
+            return array();
+
+        $aResult = array();
+        if(!empty($CNF['FIELD_AUTHOR']) && !in_array($CNF['FIELD_AUTHOR'], $this->_aSearchableNamesExcept))
+            $aResult[$CNF['FIELD_AUTHOR']] = array(
+                'type' => 'text_auto', 
+                'caption' => $CNF['T']['form_field_author'],
+                'values' => '',
+                'pass' => ''
+            );
+
+        foreach ($oForm->aInputs as $aInput)
+            if (in_array($aInput['type'], BxDolSearchExtended::$SEARCHABLE_TYPES) && !in_array($aInput['name'], $this->_aSearchableNamesExcept))
+                $aResult[$aInput['name']] = array(
+                    'type' => $aInput['type'], 
+                    'caption' => $aInput['caption_src'],
+                    'values' => !empty($aInput['values_src']) ? $aInput['values_src'] : '',
+                    'pass' => !empty($aInput['db']['pass']) ? $aInput['db']['pass'] : '',
+                );
+
+        return $aResult;
+    }
+
+    public function serviceGetSearchResultExtended($aParams, $iStart = 0, $iPerPage = 0)
+    {
+        if(empty($aParams) || !is_array($aParams))
+            return array();
+
+        return $this->_oDb->getEntriesBy(array('type' => 'search_ids', 'search_params' => $aParams, 'start' => $iStart, 'per_page' => $iPerPage));
+    }
+
+    public function serviceModuleIcon ()
+    {
+        return isset($this->_oConfig->CNF['ICON']) ? $this->_oConfig->CNF['ICON'] : '';
+    }
+
+    public function serviceGetSearchableFields ()
+    {
+        $CNF = $this->_oConfig->CNF;
+
+        if (!isset($CNF['PARAM_SEARCHABLE_FIELDS']) || !isset($CNF['OBJECT_FORM_ENTRY']) || !isset($CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD']))
+            return array();
+
+        $aTextTypes = array('text', 'textarea');
+        $aTextFields = array();
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD'], $this->_oTemplate);
+        foreach ($oForm->aInputs as $r) {
+            if (in_array($r['type'], $aTextTypes))
+                $aTextFields[$r['name']] = $r['caption'];
+        }
+        return $aTextFields;
+    }
+    
+	public function serviceManageTools($sType = 'common')
+    {
+        $oGrid = BxDolGrid::getObjectInstance($this->_oConfig->getGridObject($sType));
+        if(!$oGrid)
+            return '';
+
+		$CNF = &$this->_oConfig->CNF;
+
+		$sMenu = '';
+		if(BxDolAcl::getInstance()->isMemberLevelInSet(192)) {
+			$oPermalink = BxDolPermalinks::getInstance();
+
+			$aMenuItems = array();
+			if(!empty($CNF['OBJECT_GRID_COMMON']) && !empty($CNF['T']['menu_item_manage_my']))
+				$aMenuItems[] = array('id' => 'manage-common', 'name' => 'manage-common', 'class' => '', 'link' => $oPermalink->permalink($CNF['URL_MANAGE_COMMON']), 'target' => '_self', 'title' => _t($CNF['T']['menu_item_manage_my']), 'active' => 1);
+			if(!empty($CNF['OBJECT_GRID_ADMINISTRATION']) && !empty($CNF['T']['menu_item_manage_all']))
+				$aMenuItems[] = array('id' => 'manage-administration', 'name' => 'manage-administration', 'class' => '', 'link' => $oPermalink->permalink($CNF['URL_MANAGE_ADMINISTRATION']), 'target' => '_self', 'title' => _t($CNF['T']['menu_item_manage_all']), 'active' => 1);
+
+			if(count($aMenuItems) > 1) {
+	            $oMenu = new BxTemplMenu(array(
+	            	'template' => 'menu_vertical.html', 
+	            	'menu_items' => $aMenuItems
+	            ), $this->_oTemplate);
+	            $oMenu->setSelected($this->_aModule['name'], 'manage-' . $sType);
+	            $sMenu = $oMenu->getCode();
+			}
+		}
+
+		if(!empty($CNF['OBJECT_MENU_SUBMENU'])) {
+			BxDolMenu::getObjectInstance($CNF['OBJECT_MENU_SUBMENU'])->setSelected($this->_aModule['name'], $CNF['URI_MANAGE_COMMON']);
+		}
+
+        $this->_oTemplate->addCss(array('manage_tools.css'));
+        $this->_oTemplate->addJs(array('manage_tools.js'));
+        $this->_oTemplate->addJsTranslation(array('_sys_grid_search'));
+        return array(
+        	'content' => $this->_oTemplate->getJsCode('manage_tools', array('sObjNameGrid' => $this->_oConfig->getGridObject($sType))) . $oGrid->getCode(),
+        	'menu' => $sMenu
+        );
+    }
+
+    public function serviceGetMenuAddonManageTools()
+    {
+    	return 0;
+    }
+    
+    public function serviceGetMenuAddonManageToolsProfileStats()
+    {
+    	return 0;
+    }
+
+    /**
+     * My entries actions block
+     */
+    public function serviceMyEntriesActions ($iProfileId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if(empty($CNF['OBJECT_MENU_ACTIONS_MY_ENTRIES']))
+            return false;
+
+        if (!$iProfileId)
+            $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if (!$iProfileId || !($oProfile = BxDolProfile::getInstance($iProfileId)))
+            return false;
+
+        if ($iProfileId != $this->_iProfileId)
+            return false;
+
+        $oMenu = BxTemplMenu::getObjectInstance($CNF['OBJECT_MENU_ACTIONS_MY_ENTRIES']);
+        return $oMenu ? $oMenu->getCode() : false;
+    }
+
+	/**
+     * Display featured entries
+     * @return HTML string
+     */
+    public function serviceBrowseFeatured ($sUnitView = false, $bEmptyMessage = false, $bAjaxPaginate = true)
+    {
+        return $this->_serviceBrowse ('featured', $sUnitView ? array('unit_view' => $sUnitView) : false, BX_DB_PADDING_DEF, $bEmptyMessage, $bAjaxPaginate);
+    }
+
+	/**
+     * Display entries favored by a member
+     * @return HTML string
+     */
+    public function serviceBrowseFavorite ($iProfileId = 0, $aParams = array())
+    {
+        $oProfile = null;
+        if((int)$iProfileId)
+            $oProfile = BxDolProfile::getInstance($iProfileId);
+        if(!$oProfile && bx_get('profile_id') !== false)
+            $oProfile = BxDolProfile:: getInstance(bx_process_input(bx_get('profile_id'), BX_DATA_INT));
+        if(!$oProfile)
+            $oProfile = BxDolProfile::getInstance();
+        if(!$oProfile)
+            return '';
+
+        $bEmptyMessage = false;
+        if(isset($aParams['empty_message'])) {
+            $bEmptyMessage = (bool)$aParams['empty_message'];
+            unset($aParams['empty_message']);
+        }
+
+        return $this->_serviceBrowse ('favorite', array_merge(array('user' => $oProfile->id()), $aParams), BX_DB_PADDING_DEF, $bEmptyMessage);
+    }
+
+    public function serviceFormsHelper ()
+    {
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'FormsEntryHelper';
+        return new $sClass($this);
+    }
+
+	/**
+     * Add entry using provided fields' values.
+     * @return HTML string
+     */
+    public function serviceEntityAdd ($iProfile, $aValues)
+    {
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'FormsEntryHelper';
+        $oFormsHelper = new $sClass($this);
+        return $oFormsHelper->addData($iProfile, $aValues);
+    }
+
+	/**
+     * Perform redirect after content creation
+     * @return nothing, rediret header is sent
+     */    
+    public function serviceRedirectAfterAdd($aContentInfo)
+    {
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'FormsEntryHelper';
+        $oFormsHelper = new $sClass($this);
+        $oFormsHelper->redirectAfterAdd($aContentInfo);
+    }
+
+    /**
+     * Create entry form
+     * @return HTML string
+     */
+    public function serviceEntityCreate ($sDisplay = false)
+    {
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'FormsEntryHelper';
+        $oFormsHelper = new $sClass($this);
+        return $oFormsHelper->addDataForm($sDisplay);
+    }
+
+    public function serviceEntityEdit ($iContentId = 0, $sDisplay = false)
+    {
+        return $this->_serviceEntityForm ('editDataForm', $iContentId, $sDisplay);
+    }
+
+    public function serviceEntityDelete ($iContentId = 0)
+    {
+        return $this->_serviceEntityForm ('deleteDataForm', $iContentId);
+    }
+
+    public function serviceEntityTextBlock ($iContentId = 0)
+    {
+        return $this->_serviceEntityForm ('viewDataEntry', $iContentId);
+    }
+
+    public function serviceEntityInfo ($iContentId = 0, $sDisplay = false)
+    {
+        return $this->_serviceEntityForm ('viewDataForm', $iContentId, $sDisplay);
+    }
+
+	public function serviceEntityInfoFull ($iContentId = 0)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	$sDisplayName = 'OBJECT_FORM_ENTRY_DISPLAY_VIEW_FULL';
+        return $this->_serviceEntityForm ('viewDataForm', $iContentId, !empty($CNF[$sDisplayName]) ? $CNF[$sDisplayName] : false);
+    }
+
+	public function serviceEntityInfoExtended ($iContentId = 0)
+    {
+        return $this->_serviceTemplateFunc ('entryInfo', $iContentId);
+    }
+
+    /**
+     * Entry location info
+     */
+    public function serviceEntityLocation ($iContentId = 0)
+    {
+        $iContentId = $this->_getContent($iContentId, false);
+        if($iContentId === false)
+            return false;
+
+        return $this->_oTemplate->entryLocation ($iContentId);
+    }
+
+    /**
+     * Entry comments
+     */
+    public function serviceEntityComments ($iContentId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if(empty($CNF['OBJECT_COMMENTS']))
+            return '';
+
+        return $this->_entityComments($CNF['OBJECT_COMMENTS'], $iContentId);
+    }
+
+    /**
+     * Entry attachments block
+     */
+    public function serviceEntityAttachments ($iContentId = 0)
+    {
+        return $this->_serviceTemplateFunc ('entryAttachments', $iContentId);
+    }
+    
+    /**
+     * Delete content entry
+     * @param $iContentId content id 
+     * @return error message or empty string on success
+     */
+    public function serviceDeleteEntity ($iContentId, $sFuncDelete = 'deleteData')
+    {
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_oConfig->getClassPrefix() . 'FormsEntryHelper';
+        $oFormsHelper = new $sClass($this);
+        return $oFormsHelper->$sFuncDelete($iContentId);
+    }
+
+    /**
+     * Entry actions and social sharing block
+     */
+    public function serviceEntityAllActions ($iContentId = 0)
+    {
+        return $this->_oTemplate->entryAllActions('', $this->serviceEntitySocialSharing($iContentId));
+    }
+
+    /**
+     * Entry actions block
+     */
+    public function serviceEntityActions ($iContentId = 0)
+    {
+        $iContentId = $this->_getContent($iContentId, false);
+        if($iContentId === false)
+            return false;
+
+        $oMenu = BxTemplMenu::getObjectInstance($this->_oConfig->CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY']);
+        return $oMenu ? $oMenu->getCode() : false;
+    }
+
+    /**
+     * Entry social sharing block
+     */
+    public function serviceEntitySocialSharing ($iContentId = 0)
+    {
+        $mixedContent = $this->_getContent($iContentId);
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        $CNF = &$this->_oConfig->CNF;
+        return $this->_entitySocialSharing ($iContentId, array(
+            'id_timeline' => $iContentId,
+        	'id_thumb' => !empty($CNF['FIELD_THUMB']) && !empty($aContentInfo[$CNF['FIELD_THUMB']]) ? $aContentInfo[$CNF['FIELD_THUMB']] : '',
+        	'title' => !empty($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : '',
+        	'object_storage' => !empty($CNF['OBJECT_STORAGE']) ? $CNF['OBJECT_STORAGE'] : '',
+            'object_transcoder' => false,
+        	'object_view' => !empty($CNF['OBJECT_VIEWS']) ? $CNF['OBJECT_VIEWS'] : '',
+        	'object_vote' => !empty($CNF['OBJECT_VOTES']) ? $CNF['OBJECT_VOTES'] : '',
+        	'object_favorite' => !empty($CNF['OBJECT_FAVORITES']) ? $CNF['OBJECT_FAVORITES'] : '',
+            'object_feature' => !empty($CNF['OBJECT_FEATURED']) ? $CNF['OBJECT_FEATURED'] : '',
+        	'object_report' => !empty($CNF['OBJECT_REPORTS']) ? $CNF['OBJECT_REPORTS'] : '',
+        	'uri_view_entry' => !empty($CNF['URI_VIEW_ENTRY']) ? $CNF['URI_VIEW_ENTRY'] : '',
+        ));
+    }
+
+	/**
+     * Data for Notifications module
+     */
+    public function serviceGetNotificationsData()
+    {
+    	$sModule = $this->_aModule['name'];
+
+    	$sEventPrivacy = $sModule . '_allow_view_event_to';
+		if(BxDolPrivacy::getObjectInstance($sEventPrivacy) === false)
+			$sEventPrivacy = '';
+
+        return array(
+            'handlers' => array(
+                array('group' => $sModule . '_object', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'added', 'module_name' => $sModule, 'module_method' => 'get_notifications_post', 'module_class' => 'Module', 'module_event_privacy' => $sEventPrivacy),
+                array('group' => $sModule . '_object', 'type' => 'update', 'alert_unit' => $sModule, 'alert_action' => 'edited'),
+                array('group' => $sModule . '_object', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'deleted'),
+
+                array('group' => $sModule . '_comment', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'commentPost', 'module_name' => $sModule, 'module_method' => 'get_notifications_comment', 'module_class' => 'Module', 'module_event_privacy' => $sEventPrivacy),
+                array('group' => $sModule . '_comment', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'commentRemoved'),
+
+                array('group' => $sModule . '_reply', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'replyPost', 'module_name' => $sModule, 'module_method' => 'get_notifications_reply', 'module_class' => 'Module', 'module_event_privacy' => $sEventPrivacy),
+                array('group' => $sModule . '_reply', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'replyRemoved'),
+
+                array('group' => $sModule . '_vote', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'doVote', 'module_name' => $sModule, 'module_method' => 'get_notifications_vote', 'module_class' => 'Module', 'module_event_privacy' => $sEventPrivacy),
+                array('group' => $sModule . '_vote', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'undoVote'),
+            ),
+            'alerts' => array(
+                array('unit' => $sModule, 'action' => 'added'),
+                array('unit' => $sModule, 'action' => 'edited'),
+                array('unit' => $sModule, 'action' => 'deleted'),
+
+                array('unit' => $sModule, 'action' => 'commentPost'),
+                array('unit' => $sModule, 'action' => 'commentRemoved'),
+
+                array('unit' => $sModule, 'action' => 'replyPost'),
+                array('unit' => $sModule, 'action' => 'replyRemoved'),
+
+                array('unit' => $sModule, 'action' => 'doVote'),
+                array('unit' => $sModule, 'action' => 'undoVote'),
+            )
+        );
+    }
+
+    /**
+     * Entry post for Notifications module
+     */
+    public function serviceGetNotificationsPost($aEvent)
+    {
+		$CNF = &$this->_oConfig->CNF;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($aEvent['object_id']);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return array();
+
+        $sEntryUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+        $sEntryCaption = isset($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : strmaxtextlen($aContentInfo[$CNF['FIELD_TEXT']], 20, '...');
+
+		return array(
+			'entry_sample' => $CNF['T']['txt_sample_single'],
+			'entry_url' => $sEntryUrl,
+			'entry_caption' => $sEntryCaption,
+			'entry_author' => $aContentInfo[$CNF['FIELD_AUTHOR']],
+			'entry_privacy' => '', //may be empty or not specified. In this case Public privacy will be used.
+			'lang_key' => '', //may be empty or not specified. In this case the default one from Notification module will be used.
+		);
+    }
+
+	/**
+     * Entry post comment for Notifications module
+     */
+    public function serviceGetNotificationsComment($aEvent)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	$iContentId = (int)$aEvent['object_id'];
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return array();
+
+		$oComment = BxDolCmts::getObjectInstance($CNF['OBJECT_COMMENTS'], $iContentId);
+        if(!$oComment || !$oComment->isEnabled())
+            return array();
+
+        $sEntryUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+        $sEntryCaption = isset($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : strmaxtextlen($aContentInfo[$CNF['FIELD_TEXT']], 20, '...');
+
+		return array(
+			'entry_sample' => $CNF['T']['txt_sample_single'],
+			'entry_url' => $sEntryUrl,
+			'entry_caption' => $sEntryCaption,
+			'entry_author' => $aContentInfo[$CNF['FIELD_AUTHOR']],
+			'subentry_sample' => $CNF['T']['txt_sample_comment_single'],
+			'subentry_url' => $oComment->getViewUrl((int)$aEvent['subobject_id']),
+			'lang_key' => '', //may be empty or not specified. In this case the default one from Notification module will be used.
+		);
+    }
+
+	/**
+     * Entry post reply for Notifications module
+     */
+    public function serviceGetNotificationsReply($aEvent)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	$oComment = BxDolCmts::getObjectInstance($CNF['OBJECT_COMMENTS'], 0, false);
+        if(!$oComment || !$oComment->isEnabled())
+            return array();
+
+    	$iParentId = (int)$aEvent['object_id'];
+        $aParentInfo = $oComment->getQueryObject()->getCommentsBy(array('type' => 'id', 'id' => $iParentId));
+        if(empty($aParentInfo) || !is_array($aParentInfo))
+            return array();
+
+        $iObjectId = (int)$aParentInfo['cmt_object_id'];
+		$oComment->init($iObjectId);
+
+		return array(
+		    'object_id' => $iObjectId,
+			'entry_sample' => '_cmt_txt_sample_comment_single',
+			'entry_url' => $oComment->getViewUrl($iParentId),
+			'entry_caption' => strmaxtextlen($aParentInfo['cmt_text'], 20, '...'),
+			'entry_author' => (int)$aParentInfo['cmt_author_id'],
+			'subentry_sample' => '_cmt_txt_sample_reply_to',
+			'subentry_url' => $oComment->getViewUrl((int)$aEvent['subobject_id']),
+			'lang_key' => '', //may be empty or not specified. In this case the default one from Notification module will be used.
+		);
+    }
+
+	/**
+     * Entry post vote for Notifications module
+     */
+    public function serviceGetNotificationsVote($aEvent)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	$iContentId = (int)$aEvent['object_id'];
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return array();
+
+		$oVote = BxDolVote::getObjectInstance($CNF['OBJECT_VOTES'], $iContentId);
+        if(!$oVote || !$oVote->isEnabled())
+            return array();
+
+        $sEntryUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+        $sEntryCaption = isset($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : strmaxtextlen($aContentInfo[$CNF['FIELD_TEXT']], 20, '...');
+
+		return array(
+			'entry_sample' => $CNF['T']['txt_sample_single'],
+			'entry_url' => $sEntryUrl,
+			'entry_caption' => $sEntryCaption,
+			'entry_author' => $aContentInfo[$CNF['FIELD_AUTHOR']],
+			'subentry_sample' => $CNF['T']['txt_sample_vote_single'],
+			'lang_key' => '', //may be empty or not specified. In this case the default one from Notification module will be used.
+		);
+    }
+
+    /**
+     * Data for Timeline module
+     */
+    public function serviceGetTimelineData()
+    {
+    	$sModule = $this->_aModule['name'];
+
+        return array(
+            'handlers' => array(
+                array('group' => $sModule . '_object', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'added', 'module_name' => $sModule, 'module_method' => 'get_timeline_post', 'module_class' => 'Module',  'groupable' => 0, 'group_by' => ''),
+                array('group' => $sModule . '_object', 'type' => 'update', 'alert_unit' => $sModule, 'alert_action' => 'edited'),
+                array('group' => $sModule . '_object', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'deleted')
+            ),
+            'alerts' => array(
+                array('unit' => $sModule, 'action' => 'added'),
+                array('unit' => $sModule, 'action' => 'edited'),
+                array('unit' => $sModule, 'action' => 'deleted'),
+            )
+        );
+    }
+
+    /**
+     * Entry post for Timeline module
+     */
+    public function serviceGetTimelinePost($aEvent, $aBrowseParams = array())
+    {
+        $aContentInfo = $this->_oDb->getContentInfoById($aEvent['object_id']);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return '';
+
+        $CNF = &$this->_oConfig->CNF;
+        
+        //--- Views
+        $oViews = isset($CNF['OBJECT_VIEWS']) ? BxDolView::getObjectInstance($CNF['OBJECT_VIEWS'], $aEvent['object_id']) : null;
+
+        $aViews = array();
+        if ($oViews && $oViews->isEnabled())
+            $aViews = array(
+                'system' => $CNF['OBJECT_VIEWS'],
+                'object_id' => $aContentInfo[$CNF['FIELD_ID']],
+                'count' => $aContentInfo['views']
+            );
+
+        //--- Votes
+        $oVotes = isset($CNF['OBJECT_VOTES']) ? BxDolVote::getObjectInstance($CNF['OBJECT_VOTES'], $aEvent['object_id']) : null;
+
+        $aVotes = array();
+        if ($oVotes && $oVotes->isEnabled())
+            $aVotes = array(
+                'system' => $CNF['OBJECT_VOTES'],
+                'object_id' => $aContentInfo[$CNF['FIELD_ID']],
+                'count' => $aContentInfo['votes']
+            );
+
+		//--- Reports
+        $oReports = isset($CNF['OBJECT_REPORTS']) ? BxDolReport::getObjectInstance($CNF['OBJECT_REPORTS'], $aEvent['object_id']) : null;
+
+        $aReports = array();
+        if ($oReports && $oReports->isEnabled())
+            $aReports = array(
+                'system' => $CNF['OBJECT_REPORTS'],
+                'object_id' => $aContentInfo[$CNF['FIELD_ID']],
+                'count' => $aContentInfo['reports']
+            );
+
+        //--- Comments
+        $oCmts = isset($CNF['OBJECT_COMMENTS']) ? BxDolCmts::getObjectInstance($CNF['OBJECT_COMMENTS'], $aEvent['object_id']) : null;
+
+        $aComments = array();
+        if($oCmts && $oCmts->isEnabled())
+            $aComments = array(
+                'system' => $CNF['OBJECT_COMMENTS'],
+                'object_id' => $aContentInfo[$CNF['FIELD_ID']],
+                'count' => $aContentInfo['comments']
+            );
+
+        //--- Title & Description
+        $sTitle = !empty($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : '';
+        if(empty($sTitle) && !empty($aContentInfo[$CNF['FIELD_TEXT']]))
+            $sTitle = $aContentInfo[$CNF['FIELD_TEXT']];
+
+        return array(
+            'owner_id' => $aContentInfo[$CNF['FIELD_AUTHOR']],
+            'icon' => !empty($CNF['ICON']) ? $CNF['ICON'] : '',
+        	'sample' => isset($CNF['T']['txt_sample_single_with_article']) ? $CNF['T']['txt_sample_single_with_article'] : $CNF['T']['txt_sample_single'],
+        	'sample_wo_article' => $CNF['T']['txt_sample_single'],
+    	    'sample_action' => isset($CNF['T']['txt_sample_single_action']) ? $CNF['T']['txt_sample_single_action'] : '',
+            'url' => BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]),
+            'content' => $this->_getContentForTimelinePost($aEvent, $aContentInfo, $aBrowseParams), //a string to display or array to parse default template before displaying.
+            'date' => $aContentInfo[$CNF['FIELD_ADDED']],
+            'views' => $aViews,
+            'votes' => $aVotes,
+            'reports' => $aReports,
+            'comments' => $aComments,
+            'title' => $sTitle, //may be empty.
+            'description' => '' //may be empty.
+        );
+    }
+
+    /**
+     * Check particular action permission without content
+     * @param $sAction action to check, for example: Browse, Add
+     * @param $iContentId content ID
+     * @return message on error, or CHECK_ACTION_RESULT_ALLOWED when allowed
+     */ 
+    public function serviceCheckAllowed($sAction, $isPerformAction = false)
+    {
+        $sMethod = 'checkAllowed' . bx_gen_method_name($sAction);
+        if (!method_exists($this, $sMethod))
+            return _t('_sys_request_method_not_found_cpt');
+
+        return $this->$sMethod($isPerformAction);
+    }
+    
+    /**
+     * Check particular action permission with content
+     * @param $sAction action to check, for example: View, Edit
+     * @param $iContentId content ID
+     * @return message on error, or CHECK_ACTION_RESULT_ALLOWED when allowed
+     */ 
+    public function serviceCheckAllowedWithContent($sAction, $iContentId, $isPerformAction = false)
+    {
+        if (!$iContentId || !($aContentInfo = $this->_oDb->getContentInfoById($iContentId)))
+            return _t('_sys_request_page_not_found_cpt');
+
+        $sMethod = 'checkAllowed' . bx_gen_method_name($sAction);
+        if (!method_exists($this, $sMethod))
+            return _t('_sys_request_method_not_found_cpt');
+
+        return $this->$sMethod($aContentInfo, $isPerformAction);
+    }
+    
+    public function serviceCheckAllowedCommentsView($iContentId, $sObjectComments) 
+    {
+        return $this->serviceCheckAllowedWithContent('comments_view', $iContentId);
+    }
+    
+    public function serviceCheckAllowedCommentsPost($iContentId, $sObjectComments) 
+    {
+        return $this->serviceCheckAllowedWithContent('comments_post', $iContentId);
+    }
+
+    public function serviceGetContentOwnerProfileId ($iContentId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        // file owner must be author of the content or profile itself in case of profile based module
+        if ($iContentId) {
+            if ($this instanceof iBxDolProfileService) {
+                $oProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName());
+            }
+            else {
+                $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+                $oProfile = $aContentInfo ? BxDolProfile::getInstance($aContentInfo[$CNF['FIELD_AUTHOR']]) : null;
+            }
+            $iProfileId = $oProfile ? $oProfile->id() : bx_get_logged_profile_id();
+        }
+        else {
+            $iProfileId = bx_get_logged_profile_id();
+        }
+
+        return $iProfileId;
+    }
+    
+    // ====== PERMISSION METHODS
+
+    public function checkAllowedSetThumb ($iContentId = 0)
+    {
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+    
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make "true === " checking.
+     */
+    public function checkAllowedBrowse ()
+    {
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedView ($aDataEntry, $isPerformAction = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        // moderator and owner always have access
+        if (!empty($this->_iProfileId) && ((int)$aDataEntry[$CNF['FIELD_AUTHOR']] == (int)$this->_iProfileId || $this->_isModerator($isPerformAction)))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // check ACL
+        $aCheck = checkActionModule($this->_iProfileId, 'view entry', $this->getName(), $isPerformAction);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+
+        // check privacy
+        if (!empty($CNF['OBJECT_PRIVACY_VIEW'])) {
+            $oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW']);
+            if ($oPrivacy && !$oPrivacy->check($aDataEntry[$CNF['FIELD_ID']]))
+                return _t('_sys_access_denied_to_private_content');
+        }
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedAdd ($isPerformAction = false)
+    {
+        // check ACL
+        $aCheck = checkActionModule($this->_iProfileId, 'create entry', $this->getName(), $isPerformAction);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedEdit ($aDataEntry, $isPerformAction = false)
+    {
+        // moderator and owner always have access
+        if ($aDataEntry[$this->_oConfig->CNF['FIELD_AUTHOR']] == $this->_iProfileId || $this->_isModerator($isPerformAction))
+            return CHECK_ACTION_RESULT_ALLOWED;
+        return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedDelete (&$aDataEntry, $isPerformAction = false)
+    {
+        // moderator always has access
+        if ($this->_isModerator($isPerformAction))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // check ACL
+        $aCheck = checkActionModule($this->_iProfileId, 'delete entry', $this->getName(), $isPerformAction);
+        if ($aDataEntry[$this->_oConfig->CNF['FIELD_AUTHOR']] == $this->_iProfileId && $aCheck[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedSetMembership (&$aDataEntry, $isPerformAction = false)
+    {
+        // admin always has access
+        if (isAdmin())
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // check ACL
+        $aCheck = checkActionModule($this->_iProfileId, 'set acl level', 'system', $isPerformAction);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedEditAnyEntry ($isPerformAction = false)
+    {
+    	$aCheck = checkActionModule($this->_iProfileId, 'edit any entry', $this->getName(), $isPerformAction);
+    	if($aCheck[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED)
+    		return CHECK_ACTION_RESULT_ALLOWED;
+
+    	return _t('_sys_txt_access_denied');
+    }
+
+	/**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make "true === " checking.
+     */
+    public function checkAllowedCommentsView ($aContentInfo, $isPerformAction = false)
+    {
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+	/**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make "true === " checking.
+     */
+    public function checkAllowedCommentsPost ($aContentInfo, $isPerformAction = false)
+    {
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    public function _serviceBrowse ($sMode, $aParams = false, $iDesignBox = BX_DB_PADDING_DEF, $bDisplayEmptyMsg = false, $bAjaxPaginate = true, $sClassSearchResult = 'SearchResult')
+    {
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->checkAllowedBrowse()))
+            return MsgBox($sMsg);
+
+        bx_import($sClassSearchResult, $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . $sClassSearchResult;
+        $o = new $sClass($sMode, $aParams);
+
+        $o->setDesignBoxTemplateId($iDesignBox);
+        $o->setDisplayEmptyMsg($bDisplayEmptyMsg);
+        $o->setAjaxPaginate($bAjaxPaginate);
+        $o->setUnitParams(array('context' => $sMode));
+
+        if ($o->isError)
+            return '';
+
+        if ($s = $o->processing())
+            return $s;
+        else
+            return '';
+    }
+
+	// ====== COMMON METHODS
+
+    public function getProfileId()
+    {
+    	return bx_get_logged_profile_id();
+    }
+
+	public function getProfileInfo($iUserId = 0)
+    {
+        $oProfile = $this->getObjectUser($iUserId);
+
+        $oAccount = null;
+        if($oProfile && !($oProfile instanceof BxDolProfileUndefined))
+            $oAccount = $oProfile->getAccountObject();
+        $bAccount = !empty($oAccount);
+
+        return array(
+        	'id' => $oProfile->id(),
+            'name' => $oProfile->getDisplayName(),
+        	'email' => $bAccount ? $oAccount->getEmail() : '',
+            'link' => $oProfile->getUrl(),
+            'icon' => $oProfile->getIcon(),
+        	'thumb' => $oProfile->getThumb(),
+        	'avatar' => $oProfile->getAvatar(),
+            'unit' => $oProfile->getUnit(),
+        	'active' => $oProfile->isActive(),
+        );
+    }
+    
+    public function getObjectUser($iUserId = 0)
+    {
+    	bx_import('BxDolProfile');
+        $oProfile = BxDolProfile::getInstance($iUserId);
+        if (!$oProfile) {
+            bx_import('BxDolProfileUndefined');
+            $oProfile = BxDolProfileUndefined::getInstance();
+        }
+
+        return $oProfile;
+    }
+
+    public function getObjectFavorite($sSystem = '', $iId = 0)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if(empty($sSystem) && !empty($CNF['OBJECT_FAVORITES']))
+            $sSystem = $CNF['OBJECT_FAVORITES'];
+
+        if(empty($sSystem))
+            return false;
+
+        $oFavorite = BxDolFavorite::getObjectInstance($sSystem, $iId, true, $this->_oTemplate);
+        if(!$oFavorite->isEnabled())
+            return false;
+
+        return $oFavorite;
+    }
+
+	public function getUserId()
+    {
+        return isLogged() ? bx_get_logged_profile_id() : 0;
+    }
+
+    public function getUserIp()
+    {
+        return getVisitorIP();
+    }
+
+    public function getUserInfo($iUserId = 0)
+    {
+        $oProfile = BxDolProfile::getInstance($iUserId);
+        if (!$oProfile)
+            $oProfile = BxDolProfileUndefined::getInstance();
+
+        return array(
+            $oProfile->getDisplayName(),
+            $oProfile->getUrl(),
+            $oProfile->getThumb(),
+            $oProfile->getUnit(),
+            $oProfile->getUnit(0, array('template' => 'unit_wo_info'))
+        );
+    }
+
+    public function _isModerator ($isPerformAction = false)
+    {
+        return CHECK_ACTION_RESULT_ALLOWED === $this->checkAllowedEditAnyEntry ($isPerformAction);
+    }
+
+    // ====== PROTECTED METHODS
+
+    protected function _serviceEntityForm ($sFormMethod, $iContentId = 0, $sDisplay = false, $sCheckFunction = false)
+    {
+        $iContentId = $this->_getContent($iContentId, false);
+        if($iContentId === false)
+            return false;
+
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'FormsEntryHelper';
+        $oFormsHelper = new $sClass($this);
+        return $oFormsHelper->$sFormMethod((int)$iContentId, $sDisplay, $sCheckFunction);
+    }
+
+    protected function _serviceTemplateFunc ($sFunc, $iContentId, $sFuncGetContent = 'getContentInfoById')
+    {
+        $mixedContent = $this->_getContent($iContentId, $sFuncGetContent);
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        return $this->_oTemplate->$sFunc($aContentInfo);
+    }
+
+    protected function _rss ($aArgs, $sClass = 'SearchResult')
+    {
+        $sMode = array_shift($aArgs);
+
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->checkAllowedBrowse())) {
+            $this->_oTemplate->displayAccessDenied ($sMsg);
+            exit;
+        }
+
+        $aParams = $this->_buildRssParams($sMode, $aArgs);
+
+        bx_import ($sClass, $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . $sClass;
+        $o = new $sClass($sMode, $aParams);
+
+        if ($o->isError)
+            $this->_oTemplate->displayPageNotFound ();
+        else
+            $o->outputRSS();
+
+        exit;
+    }
+
+    protected function _getContent($iContentId = 0, $sFuncGetContent = 'getContentInfoById')
+    {
+        if(!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+
+        if(!$iContentId)
+            return false;
+
+        if(empty($sFuncGetContent) || !method_exists($this->_oDb, $sFuncGetContent))
+            return $iContentId;
+
+        $aContentInfo = $this->_oDb->$sFuncGetContent($iContentId);
+        if(!$aContentInfo)
+            return false;
+
+        return array($iContentId, $aContentInfo);
+    }
+
+    protected function _getContentForTimelinePost($aEvent, $aContentInfo, $aBrowseParams = array())
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+    	$sUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+
+    	//--- Image(s)
+        $aImages = $this->_getImagesForTimelinePost($aEvent, $aContentInfo, $sUrl, $aBrowseParams);
+
+        //--- Video(s)
+        $aVideos = $this->_getVideosForTimelinePost($aEvent, $aContentInfo, $sUrl, $aBrowseParams);
+
+    	return array(
+    		'sample' => isset($CNF['T']['txt_sample_single_with_article']) ? $CNF['T']['txt_sample_single_with_article'] : $CNF['T']['txt_sample_single'],
+    		'sample_wo_article' => $CNF['T']['txt_sample_single'],
+    	    'sample_action' => isset($CNF['T']['txt_sample_single_action']) ? $CNF['T']['txt_sample_single_action'] : '',
+			'url' => $sUrl,
+			'title' => isset($CNF['FIELD_TITLE']) && isset($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : 
+			(isset($CNF['FIELD_TEXT']) && isset($aContentInfo[$CNF['FIELD_TEXT']]) ? strmaxtextlen($aContentInfo[$CNF['FIELD_TEXT']], 20, '...') : ''),
+			'text' => isset($CNF['FIELD_TEXT']) && isset($aContentInfo[$CNF['FIELD_TEXT']]) ? $aContentInfo[$CNF['FIELD_TEXT']] : '',
+			'images' => $aImages,
+            'videos' => $aVideos
+		);
+    }
+
+    protected function _getImagesForTimelinePost($aEvent, $aContentInfo, $sUrl, $aBrowseParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sImage = $sImageOrig = '';
+        if(isset($CNF['FIELD_COVER']) && isset($aContentInfo[$CNF['FIELD_COVER']]) && $aContentInfo[$CNF['FIELD_COVER']]) {
+            $sImage = $this->_oConfig->getImageUrl($aContentInfo[$CNF['FIELD_COVER']], array('OBJECT_IMAGES_TRANSCODER_GALLERY', 'OBJECT_IMAGES_TRANSCODER_THUMB'));
+            $sImageOrig = $this->_oConfig->getImageUrl($aContentInfo[$CNF['FIELD_COVER']], array('OBJECT_IMAGES_TRANSCODER_COVER'));
+        }
+
+        $bImageThumb = isset($CNF['FIELD_THUMB']) && isset($aContentInfo[$CNF['FIELD_THUMB']]) && $aContentInfo[$CNF['FIELD_THUMB']];
+        if($sImage == '' && $bImageThumb)
+            $sImage = $this->_oConfig->getImageUrl($aContentInfo[$CNF['FIELD_THUMB']], array('OBJECT_IMAGES_TRANSCODER_GALLERY', 'OBJECT_IMAGES_TRANSCODER_THUMB'));
+
+        if($sImageOrig == '' && $bImageThumb)
+            $sImageOrig = $this->_oConfig->getImageUrl($aContentInfo[$CNF['FIELD_THUMB']], array('OBJECT_IMAGES_TRANSCODER_COVER'));
+
+        if(empty($sImage))
+            return array();
+
+        if($sImageOrig == '')
+            $sImageOrig = $sImage;
+
+        return array(
+		    array('url' => $sUrl, 'src' => $sImage, 'src_orig' => $sImageOrig),
+		);
+    }
+
+    protected function _getVideosForTimelinePost($aEvent, $aContentInfo, $sUrl, $aBrowseParams = array())
+    {
+        return array();
+    }
+
+    protected function _entityComments ($sObject, $iId = 0)
+    {
+        if (!$iId)
+            $iId = bx_process_input(bx_get('id'), BX_DATA_INT);
+
+        if (!$iId)
+            return false;
+
+        $oCmts = BxDolCmts::getObjectInstance($sObject, $iId);
+        if (!$oCmts || !$oCmts->isEnabled())
+            return false;
+
+        return $oCmts->getCommentsBlock(array(), array('in_designbox' => false, 'show_empty' => true));
+    }
+
+    protected function _entitySocialSharing ($iId, $aParams = array())
+    {
+        $bShowAsButton = !isset($aParams['show_as_button']) || $aParams['show_as_button'] === true;
+
+        $sUrl = !empty($aParams['uri_view_entry']) ? BxDolPermalinks::getInstance()->permalink('page.php?i=' . $aParams['uri_view_entry'] . '&id=' . $iId) : '';
+        $sTitle = !empty($aParams['title']) ? $aParams['title'] : '';
+
+        //--- Comments
+        $sComments = '';
+        $oComments = !empty($aParams['object_comments']) ? BxTemplCmts::getObjectInstance($aParams['object_comments'], $iId) : false;
+        if ($oComments) {
+            $iNum = $oComments->getCommentsCountAll();
+            $sComments = $this->_oTemplate->parseHtmlByName('comments-item.html', array (
+                'class' => 'bx-base-general-comments-' . ($bShowAsButton ? 'button' : 'link'),
+                'class_do' => $bShowAsButton ? ' bx-btn' : '',
+            	'class_counter' => $bShowAsButton ? ' bx-btn-height' : '',
+                'url' => $sUrl . $oComments->getListAnchor(true),
+                'bx_if:comments' => array (
+                    'condition' => $iNum,
+                    'content' => array (
+                        'num' => $iNum,
+                    ),
+                ),
+            ));
+        }
+
+        $aCustomParams = array();
+        $iIdThumb = isset($aParams['id_thumb']) ? (int)$aParams['id_thumb'] : 0;
+        $bSocialSharing = isset($aParams['social_sharing']) ? (bool)$aParams['social_sharing'] : true;
+        if ($iIdThumb && $bSocialSharing) {
+            if(!empty($aParams['object_transcoder']))
+                $o = BxDolTranscoder::getObjectInstance($aParams['object_transcoder']);
+            else if(!empty($aParams['object_storage']))
+                $o = BxDolStorage::getObjectInstance($aParams['object_storage']);
+
+            $sImgUrl = $o ? $o->getFileUrlById($iIdThumb) : '';
+            if($sImgUrl)
+                $aCustomParams = array (
+                    'img_url' => $sImgUrl,
+                    'img_url_encoded' => rawurlencode($sImgUrl),
+                );
+        }
+
+        //TODO: Rebuild using menus engine when it will be ready for such elements like Vote, Share, etc.
+        //--- Views
+        $sViews = '';
+        $oViews = !empty($aParams['object_view']) ? BxDolView::getObjectInstance($aParams['object_view'], $iId) : false;
+        if ($oViews)
+            $sViews = $oViews->getElementBlock(array('show_do_view_as_button' => $bShowAsButton));
+
+        //--- Votes
+        $sVotes = '';
+        $oVotes = !empty($aParams['object_vote']) ? BxDolVote::getObjectInstance($aParams['object_vote'], $iId) : false;
+        if ($oVotes)
+            $sVotes = $oVotes->getElementBlock(array('show_do_vote_as_button' => $bShowAsButton));
+
+        //--- Favorite
+        $sFavorites = '';
+        $oFavorites = !empty($aParams['object_favorite']) ? BxDolFavorite::getObjectInstance($aParams['object_favorite'], $iId) : false;
+        if ($oFavorites)
+            $sFavorites = $oFavorites->getElementBlock(array('show_do_favorite_as_button' => $bShowAsButton));
+
+        //--- Featured
+        $sFeatured = '';
+        $oFeatured = !empty($aParams['object_feature']) ? BxDolFeature::getObjectInstance($aParams['object_feature'], $iId) : false;
+        if ($oFeatured)
+            $sFeatured = $oFeatured->getElementBlock(array('show_do_feature_as_button' => $bShowAsButton));
+
+        //--- Timeline Repost
+        $sRepost = '';
+        $iIdTimeline = isset($aParams['id_timeline']) ? (int)$aParams['id_timeline'] : $iId;
+        if ($iIdTimeline && BxDolRequest::serviceExists('bx_timeline', 'get_repost_element_block'))
+            $sRepost = BxDolService::call('bx_timeline', 'get_repost_element_block', array(bx_get_logged_profile_id(), $this->_aModule['name'], 'added', $iIdTimeline, array('show_do_repost_as_button' => $bShowAsButton)));
+
+        //--- Report
+		$sReport = '';
+        $oReport = !empty($aParams['object_report']) ? BxDolReport::getObjectInstance($aParams['object_report'], $iId) : false;
+        if ($oReport)
+            $sReport = $oReport->getElementBlock(array('show_do_report_as_button' => $bShowAsButton));
+
+        $sSocial = '';
+        if($bSocialSharing) {
+            $oSocial = BxDolMenu::getObjectInstance('sys_social_sharing');
+            $oSocial->addMarkers(array_merge(array(
+                'id' => $iId,
+            	'module' => $this->_aModule['name'],
+            	'url' => BX_DOL_URL_ROOT . $sUrl,
+            	'title' => $sTitle,
+            ), $aCustomParams));
+            $sSocial = $oSocial->getCode();
+        }
+
+        if(empty($sComments)  && empty($sViews) && empty($sVotes) && empty($sFavorites)  && empty($sFeatured) && empty($sRepost) && empty($sReport) && empty($sSocial))
+            return '';
+
+        return $this->_oTemplate->parseHtmlByName('entry-share.html', array(
+            'comments' => $sComments,
+        	'view' => $sViews,
+            'vote' => $sVotes,
+            'favorite' => $sFavorites,
+            'feature' => $sFeatured,
+            'repost' => $sRepost,
+        	'report' => $sReport,
+            'social' => $sSocial,
+        ));
+        //TODO: Rebuild using menus engine when it will be ready for such elements like Vote, Repost, etc.
+    }
+
+    protected function _getFields($iContentId)
+    {
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo))
+            return array();
+
+        return BxDolContentInfo::formatFields($aContentInfo);
+    }
+
+    protected function _getFieldValue($sField, $iContentId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if(empty($CNF[$sField]))
+            return false;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || empty($aContentInfo[$CNF[$sField]]))
+            return false;
+
+        return $aContentInfo[$CNF[$sField]];
+    }
+
+    protected function _getFieldValueThumb($sField, $iContentId, $sTranscoder = '') 
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($sTranscoder) || empty($CNF[$sField]))
+            return false;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || empty($aContentInfo[$CNF[$sField]]))
+            return false;
+
+        $oImagesTranscoder = BxDolTranscoderImage::getObjectInstance($sTranscoder);
+        if(!$oImagesTranscoder)
+            return false;
+
+        return $oImagesTranscoder->getFileUrl($aContentInfo[$CNF[$sField]]);
+    }
+}
+
+/** @} */

+ 125 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/classes/BxBaseModGeneralPageEntry.php

@@ -0,0 +1,125 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+bx_import('BxDolAcl');
+
+/**
+ * Entry create/edit pages
+ */
+class BxBaseModGeneralPageEntry extends BxTemplPage
+{
+    protected $MODULE;
+
+    protected $_oModule;
+    protected $_aContentInfo = false;
+
+    protected $_sCoverClass;
+
+    public function __construct($aObject, $oTemplate = false)
+    {
+        $this->_oModule = BxDolModule::getInstance($this->MODULE);
+
+        parent::__construct($aObject, $oTemplate ? $oTemplate : $this->_oModule->_oTemplate);
+
+        $this->_sCoverClass = $this->_oModule->getName() . '_cover';
+    }
+
+    public function isActive()
+    {
+        return true;
+    }
+
+    public function getCode ()
+    {
+        // check if content exists
+        if (!$this->_aContentInfo || !$this->isActive()) { // if entry is not found - display standard "404 page not found" page
+            $this->_oTemplate->displayPageNotFound();
+            exit;
+        }
+
+        // permissions check 
+        $this->_processPermissionsCheck ();
+
+        // count views
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        if (!empty($CNF['OBJECT_VIEWS'])) {
+            BxDolView::getObjectInstance($CNF['OBJECT_VIEWS'], $this->_aContentInfo[$CNF['FIELD_ID']])->doView();
+        }
+
+        $oCover = BxDolCover::getInstance($this->_oModule->_oTemplate);
+        $oCover->setCoverClass($this->_sCoverClass);
+
+        // set cover image
+        $mixedThumb = $this->_getThumbForMetaObject();
+        if($mixedThumb !== false) {
+            $aCover = array(
+                'id' => $mixedThumb['id']
+            );
+
+            if(!empty($mixedThumb['transcoder']))
+                $aCover['transcoder'] = $mixedThumb['transcoder'];
+            else if(!empty($CNF['OBJECT_IMAGES_TRANSCODER_COVER'])) 
+                $aCover['transcoder'] = $CNF['OBJECT_IMAGES_TRANSCODER_COVER'];
+            else if(!empty($mixedThumb['object']))
+                $aCover['object'] = $mixedThumb['object'];
+
+            $oCover->setCoverImageUrl($aCover);
+        }
+
+        // add content metatags
+        if (!empty($CNF['OBJECT_METATAGS']) && $mixedThumb) {
+            $o = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+            if ($o)
+                $o->addPageMetaInfo($this->_aContentInfo[$CNF['FIELD_ID']], $mixedThumb);
+        }
+
+        $oMenuSubmenu = BxDolMenu::getObjectInstance('sys_site_submenu');
+
+        // add actions menu to submenu
+        if (isset($CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY']))
+            $oMenuSubmenu->setObjectActionsMenu($CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY']);
+
+        // add social sharing menu to submenu
+        $oMenuSubmenu->setServiceSocialSharing(array(
+            'module' => $this->MODULE,
+            'method' => 'entity_social_sharing',
+        ));
+
+        return parent::getCode ();
+    }
+
+    protected function _processPermissionsCheck ()
+    {
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->checkAllowedView($this->_aContentInfo))) {
+            $this->_oTemplate->displayAccessDenied($sMsg);
+            exit;
+        }
+        $this->_oModule->checkAllowedView($this->_aContentInfo, true);
+    }
+
+    protected function _getThumbForMetaObject ()
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+        if (empty($CNF['FIELD_THUMB']) || empty($this->_aContentInfo[$CNF['FIELD_THUMB']]) || empty($CNF['OBJECT_STORAGE']))
+            return false;
+
+        return array('id' => $this->_aContentInfo[$CNF['FIELD_THUMB']], 'object' => $CNF['OBJECT_STORAGE']);
+    }
+
+    protected function _getPageCacheParams ()
+    {
+        if (!$this->_aContentInfo)
+            return '';
+        return $this->_aContentInfo[$this->_oModule->_oConfig->CNF['FIELD_ID']]; // cache is different for every entry
+    }
+}
+
+/** @} */

+ 109 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/classes/BxBaseModGeneralSearchResult.php

@@ -0,0 +1,109 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+define('BX_SYS_PER_PAGE_BROWSE_SHOWCASE', 32);
+
+class BxBaseModGeneralSearchResult extends BxTemplSearchResult
+{
+    protected $oModule;
+    protected $bShowcaseView = false;
+    protected $aUnitViews = array();
+    protected $sUnitViewDefault = 'gallery';
+
+    function __construct($sMode = '', $aParams = array())
+    {
+        parent::__construct();
+    }
+
+    function getMain()
+    {
+        return BxDolModule::getInstance($this->aCurrent['module_name']);
+    }
+
+    function getRssUnitLink (&$a)
+    {
+        $CNF = &$this->oModule->_oConfig->CNF;
+
+        return BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $a[$CNF['FIELD_ID']]);
+    }
+
+    function getRssPageUrl ()
+    {
+        if (false === parent::getRssPageUrl())
+            return false;
+
+        $oPermalinks = BxDolPermalinks::getInstance();
+        return BX_DOL_URL_ROOT . $oPermalinks->permalink($this->aCurrent['rss']['link']);
+    }
+
+    function rss ()
+    {
+        if (!isset($this->aCurrent['rss']))
+            return '';
+
+        $this->aCurrent['paginate']['perPage'] = empty($this->oModule->_oConfig->CNF['PARAM_NUM_RSS']) ? 10 : getParam($this->oModule->_oConfig->CNF['PARAM_NUM_RSS']);
+
+        return parent::rss();
+    }
+
+    /**
+     * Add conditions for private content
+     */
+    protected function addConditionsForPrivateContent($CNF, $oProfile, $aCustomGroup = array()) 
+    {
+        if(empty($CNF['OBJECT_PRIVACY_VIEW']))
+            return;
+
+        $oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW']);
+        if(!$oPrivacy)
+            return;
+
+        $aCondition = $oPrivacy->getContentPublicAsCondition($oProfile ? $oProfile->id() : 0, $aCustomGroup);
+        if(empty($aCondition) || !is_array($aCondition))
+            return;
+
+        if(isset($aCondition['restriction']))
+            $this->aCurrent['restriction'] = array_merge($this->aCurrent['restriction'], $aCondition['restriction']);
+        if(isset($aCondition['join']))
+            $this->aCurrent['join'] = array_merge($this->aCurrent['join'], $aCondition['join']);
+
+        $this->setProcessPrivateContent(false);
+    }
+    
+    function showPagination($bAdmin = false, $bChangePage = true, $bPageReload = true)
+    {
+        if ($this->bShowcaseView){
+            return '';
+        }
+        else{
+            $sTmp = parent::showPagination ($bAdmin, $bChangePage, $bPageReload);
+            if ($sTmp != '')
+                return '<div class="bx-def-margin-top">' . $sTmp . '</div>';
+            else
+                return '';
+        }
+    }
+    
+    function displayResultBlock ()
+    {
+		if ($this->bShowcaseView){
+            $this->addContainerClass(array('bx-def-margin-sec-lefttopright-neg', 'bx-base-unit-showcase-wrapper'));
+            $CNF = &$this->oModule->_oConfig->CNF;
+            $iPerPageInShowCase = isset($CNF['PARAM_PER_PAGE_BROWSE_SHOWCASE']) ? getParam($CNF['PARAM_PER_PAGE_BROWSE_SHOWCASE']) : BX_SYS_PER_PAGE_BROWSE_SHOWCASE;
+			$this->aCurrent['paginate']['perPage'] = empty($iPerPageInShowCase) ? BX_SYS_PER_PAGE_BROWSE_SHOWCASE : $iPerPageInShowCase;
+			$this->oModule->_oTemplate->addCss(array(BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'flickity/|flickity.css'));
+            $this->oModule->_oTemplate->addJs(array('flickity/flickity.pkgd.min.js','modules/base/general/js/|showcase.js'));
+		}
+		return parent::displayResultBlock ();
+    }
+}
+
+/** @} */

+ 304 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/classes/BxBaseModGeneralTemplate.php

@@ -0,0 +1,304 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Module representation.
+ */
+class BxBaseModGeneralTemplate extends BxDolModuleTemplate
+{
+    protected $MODULE;
+    protected $_oModule;
+
+    function __construct(&$oConfig, &$oDb)
+    {
+        parent::__construct($oConfig, $oDb);        
+        $this->addCss ('main.css');
+    }
+
+    public function getModule()
+    {
+        if (!$this->_oModule) {
+            $sName = $this->_oConfig->getName();
+            $this->_oModule = BxDolModule::getInstance($sName);
+        }
+        return $this->_oModule;
+    }
+
+	public function getJsCode($sType, $aParams = array(), $bWrap = true)
+    {
+        $sBaseUri = $this->_oConfig->getBaseUri();
+        $sJsClass = $this->_oConfig->getJsClass($sType);
+        $sJsObject = $this->_oConfig->getJsObject($sType);
+
+        $aParams = array_merge(array(
+            'sActionUri' => $sBaseUri,
+            'sActionUrl' => BX_DOL_URL_ROOT . $sBaseUri,
+            'sObjName' => $sJsObject,
+        	'aHtmlIds' => array(),
+            'oRequestParams' => array()
+        ), $aParams);
+        $sContent = "var " . $sJsObject . " = new " . $sJsClass . "(" . json_encode($aParams) . ");";
+
+        return !$bWrap ? $sContent : $this->_wrapInTagJsCode($sContent);
+    }
+
+    public function getUnitMetaItemLink($sContent, $aAttrs = array())
+    {
+        return $this->getUnitMetaItem('a', $sContent, $aAttrs);
+    }
+
+    public function getUnitMetaItemText($sContent, $aAttrs = array())
+    {
+        return $this->getUnitMetaItem('span', $sContent, $aAttrs);
+    }
+
+    public function getUnitMetaItemButton($sContent, $aAttrs = array())
+    {
+        return $this->getUnitMetaItem('button', $sContent, $aAttrs);
+    }
+
+    public function getUnitMetaItemButtonSmall($sContent, $aAttrs = array())
+    {
+        return $this->getUnitMetaItem('sbutton', $sContent, $aAttrs);
+    }
+
+    public function getUnitMetaItemCustom($sContent)
+    {
+        return $this->getUnitMetaItem('custom', $sContent);
+    }
+
+	function entryText ($aData, $sTemplateName = 'entry-text.html')
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        $aVars = $aData;
+        $aVars['entry_title'] = !empty($CNF['FIELD_TITLE']) && isset($aData[$CNF['FIELD_TITLE']]) ? bx_process_output($aData[$CNF['FIELD_TITLE']]) : '';
+        $aVars['entry_text'] = !empty($CNF['FIELD_TEXT']) && isset($aData[$CNF['FIELD_TEXT']]) ? bx_process_output($aData[$CNF['FIELD_TEXT']], BX_DATA_HTML) : '';
+
+        if (!empty($CNF['OBJECT_METATAGS'])) {
+            $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+
+            // keywords
+            if ($oMetatags->keywordsIsEnabled()) {
+                $aFields = array_merge($oMetatags->metaFields($aData, $CNF, $CNF['OBJECT_FORM_ENTRY_DISPLAY_VIEW']), array('entry_title', 'entry_text'));
+                foreach ($aFields as $sField)
+                    $aVars[$sField] = $oMetatags->keywordsParse($aData[$CNF['FIELD_ID']], $aVars[$sField]);
+            }
+
+            // mentions
+            if ($oMetatags->mentionsIsEnabled()) {
+                $aFields = array_merge($oMetatags->metaFields($aData, $CNF, $CNF['OBJECT_FORM_ENTRY_DISPLAY_VIEW'], true), array('entry_text'));
+                foreach ($aFields as $sField)
+                    $aVars[$sField] = $oMetatags->mentionsParse($aData[$CNF['FIELD_ID']], $aVars[$sField]);
+            }
+            
+            // location
+            $aVars['location'] = $oMetatags->locationsIsEnabled() ? $oMetatags->locationsString($aData[$CNF['FIELD_ID']]) : '';
+        }
+
+        if (empty($aVars['entry_text']))
+            return false;
+
+        unset($aVars['recipients']);
+        
+        return $this->parseHtmlByName($sTemplateName, $aVars);
+    }
+
+    function entryLocation ($iContentId)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        if (empty($CNF['OBJECT_METATAGS']))
+            return '';
+
+        $oMetatags = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);        
+
+        if (!($sLocationString = $oMetatags->locationsString($iContentId)))
+            return '';
+
+        $aVars = array (
+            'location' => $sLocationString
+        );
+        return $this->parseHtmlByName('entry-location.html', $aVars);
+    }
+
+	public function entryInfo($aData, $aValues = array())
+    {
+    	$CNF = $this->_oConfig->CNF;
+        $aValuesDefault = array();
+
+        if (isset($aData[$CNF['FIELD_ADDED']]))
+            $aValuesDefault[] = array(
+                'title' => _t('_sys_txt_field_created'),
+                'value' => bx_time_js($aData[$CNF['FIELD_ADDED']]),
+            );
+
+        if (isset($aData[$CNF['FIELD_CHANGED']]))
+            $aValuesDefault[] = array(
+                'title' => _t('_sys_txt_field_updated'),
+                'value' => bx_time_js($aData[$CNF['FIELD_CHANGED']]),
+            );
+
+        $aValues = array_merge($aValuesDefault, $aValues);
+
+    	return $this->parseHtmlByName('entry-info.html', array(
+    		'bx_repeat:info' => $aValues,
+    	));
+    }
+
+    function entryAllActions ($sActionsEntity, $sActionsSocial)
+    {
+        if(empty($sActionsEntity) && empty($sActionsSocial))
+            return '';
+
+        return $this->parseHtmlByName('entry-all-actions.html', array (
+            'actions_entity' => $sActionsEntity,
+            'actions_social' => $sActionsSocial,
+        ));
+    }
+
+    function entryAttachments ($aData, $aParams = array())
+    {
+        return $this->entryAttachmentsByStorage($this->getModule()->_oConfig->CNF['OBJECT_STORAGE'], $aData, $aParams);
+    }
+
+	function entryAttachmentsByStorage ($sStorage, $aData, $aParams = array())
+    {
+        if (!($a = $this->getAttachments($sStorage, $aData, $aParams)))
+            return '';
+
+    	return $this->parseHtmlByName('attachments.html', array(
+            'bx_repeat:attachments' => $a,
+        ));
+    }
+    
+    protected function getUnitMetaItem($sName, $sContent, $aAttrs = array(), $sTemplate = 'unit_meta_item.html')
+    {
+        if(empty($sContent))
+            return '';
+
+        if(!is_array($aAttrs))
+            $aAttrs = array();
+
+        $aTags = array('span', 'a', 'button', 'sbutton', 'custom');
+
+        $sTmplVarsClass = ''; 
+        if(!empty($aAttrs['class'])) {
+            $sTmplVarsClass = $aAttrs['class'];
+            unset($aAttrs['class']);
+        }
+
+        $aTmplVarsAttrs = array();
+        foreach($aAttrs as $sKey => $sValue)
+            $aTmplVarsAttrs[] = array('key' => $sKey, 'value' => bx_html_attribute($sValue));
+
+        $aTmplVars = array();
+        foreach($aTags as $sTag) {
+            $aTmplVarsTag = array();
+            $bTmplVarsTag = $sTag == $sName;
+            if($bTmplVarsTag)
+                $aTmplVarsTag = array(
+                    'class' => $sTmplVarsClass,
+                    'content' => $sContent,
+                    'bx_repeat:attrs' => $aTmplVarsAttrs
+                );
+
+            $aTmplVars['bx_if:' . $sTag] = array(
+            	'condition' => $bTmplVarsTag,
+                'content' => $aTmplVarsTag
+            );
+        }
+
+        return $this->parseHtmlByName($sTemplate, $aTmplVars);
+    }
+
+	protected function getAttachments ($sStorage, $aData, $aParams = array())
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        $oStorage = BxDolStorage::getObjectInstance($sStorage);
+        $oTranscoder = BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_PREVIEW']);
+        $oTranscoderPreview = isset($CNF['OBJECT_IMAGES_TRANSCODER_PICTURE']) && $CNF['OBJECT_IMAGES_TRANSCODER_PICTURE'] ? BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_IMAGES_TRANSCODER_PICTURE']) : null;
+        $aTranscodersVideo = false;
+
+        if (isset($CNF['OBJECT_VIDEOS_TRANSCODERS']) && $CNF['OBJECT_VIDEOS_TRANSCODERS'])
+            $aTranscodersVideo = array (
+                'poster' => BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_VIDEOS_TRANSCODERS']['poster']),
+                'mp4' => BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_VIDEOS_TRANSCODERS']['mp4']),
+                'webm' => BxDolTranscoderImage::getObjectInstance($CNF['OBJECT_VIDEOS_TRANSCODERS']['webm']),
+            );
+
+        $aGhostFiles = $oStorage->getGhosts ($this->getModule()->serviceGetContentOwnerProfileId($aData[$CNF['FIELD_ID']]), $aData[$CNF['FIELD_ID']]);
+        if (!$aGhostFiles)
+            return false;
+
+        $sFilterField = isset($aParams['filter_field']) ? $aParams['filter_field'] : $CNF['FIELD_THUMB'];
+		if(!empty($sFilterField) && isset($aData[$sFilterField]))
+	        foreach ($aGhostFiles as $k => $a) {
+	            // don't show thumbnail in attachments
+	            if ($a['id'] == $aData[$sFilterField])
+	                unset($aGhostFiles[$k]);
+	        }
+
+        if(!$aGhostFiles)
+            return false;
+
+        $aAttachmnts = array();
+        foreach ($aGhostFiles as $k => $a) {
+
+            $isImage = $oTranscoder && (0 === strncmp('image/', $a['mime_type'], 6)) && $oTranscoder->isMimeTypeSupported($a['mime_type']); // preview for images, transcoder object for preview must be defined
+            $isVideo = $aTranscodersVideo && (0 === strncmp('video/', $a['mime_type'], 6)) && $aTranscodersVideo['poster']->isMimeTypeSupported($a['mime_type']); // preview for videos, transcoder object for video must be defined
+            $sUrlOriginal = $oStorage->getFileUrlById($a['id']);
+            $sImgPopupId = 'bx-messages-atachment-popup-' . $a['id'];
+
+            // images are displayed with preview and popup upon clicking
+            $a['bx_if:image'] = array (
+                'condition' => $isImage,
+                'content' => array (
+                    'url_original' => $isImage && $oTranscoderPreview ? $oTranscoderPreview->getFileUrl($a['id']) : $sUrlOriginal,
+                    'attr_file_name' => bx_html_attribute($a['file_name']),
+                    'popup_id' => $sImgPopupId,
+                    'url_preview' => $isImage ? $oTranscoder->getFileUrl($a['id']) : '',
+                    'popup' =>  BxTemplFunctions::getInstance()->transBox($sImgPopupId, '<img src="' . $sUrlOriginal . '" />', true, true),
+                ),
+            );
+
+            // videos are displayed inline
+            $a['bx_if:video'] = array (
+                'condition' => $isVideo,
+                'content' => array (
+                    'video' => $isVideo && $aTranscodersVideo ? BxTemplFunctions::getInstance()->videoPlayer(
+                        $aTranscodersVideo['poster']->getFileUrl($a['id']), 
+                        $aTranscodersVideo['mp4']->getFileUrl($a['id']), 
+                        $aTranscodersVideo['webm']->getFileUrl($a['id']),
+                        false, ''
+                    ) : '',
+                ),
+            );
+
+            // non-images are displayed as text links to original file
+            $a['bx_if:not_image'] = array (
+                'condition' => !$isImage && !$isVideo,
+                'content' => array (
+                    'url_original' => $sUrlOriginal,
+                    'attr_file_name' => bx_html_attribute($a['file_name']),
+                    'file_name' => bx_process_output($a['file_name']),
+                ),
+            );
+
+            $aAttachmnts[] = $a;
+        }
+
+        return $aAttachmnts;
+    }
+}
+
+/** @} */

+ 8 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/general/js/showcase.js

@@ -0,0 +1,8 @@
+$(document).ready(function () {
+	$('.bx-base-unit-showcase-wrapper').flickity({
+	    cellSelector: '.bx-base-unit-showcase',
+		cellAlign: 'left',
+		pageDots: false,
+		imagesLoaded: true
+	});
+});

+ 101 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/groups/classes/BxBaseModGroupsDb.php

@@ -0,0 +1,101 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/*
+ * Groups module database queries
+ */
+class BxBaseModGroupsDb extends BxBaseModProfileDb
+{
+    public function __construct(&$oConfig)
+    {
+        parent::__construct($oConfig);
+    }
+
+    public function updateAuthorById ($iContentId, $iProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sQuery = "UPDATE `" . $this->_oConfig->CNF['TABLE_ENTRIES'] . "` SET `" . $CNF['FIELD_AUTHOR'] . "` = :author WHERE `" . $CNF['FIELD_ID'] . "` = :id";
+        return $this->query($sQuery, array(
+    		'id' => $iContentId,
+    		'author' => $iProfileId,
+    	));
+    }
+
+    public function toAdmins ($iGroupProfileId, $mixedFansIds)
+    {
+        if (is_array($mixedFansIds)) {
+            foreach ($mixedFansIds as $iFanId)
+                $this->toAdmins ($iFanId);
+            return true;
+        }
+
+        $iFanId = (int)$mixedFansIds;
+        $sQuery = $this->prepare("INSERT IGNORE INTO `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` SET `group_profile_id` = ?, `fan_id` = ?", $iGroupProfileId, $iFanId);
+        if (!$this->res($sQuery))
+            return false;
+
+        $oModule = BxDolModule::getInstance($this->_oConfig->getName());
+        if ($oModule && method_exists($oModule, 'onFanAddedToAdmins'))
+            $oModule->onFanAddedToAdmins($iGroupProfileId, $iFanId);
+
+        return true;
+    }
+
+    public function fromAdmins ($iGroupProfileId, $mixedFansIds)
+    {
+        if (is_array($mixedFansIds)) {
+            foreach ($mixedFansIds as $iFanId)
+                $this->fromAdmins ($iFanId);
+            return true;
+        }
+
+        $iFanId = (int)$mixedFansIds;
+        $sQuery = $this->prepare("DELETE FROM `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` WHERE `group_profile_id` = ? AND `fan_id` = ?", $iGroupProfileId, $iFanId);
+        if (!$this->res($sQuery))
+            return false;
+
+        $oModule = BxDolModule::getInstance($this->_oConfig->getName());
+        if ($oModule && method_exists($oModule, 'onFanRemovedFromAdmins'))
+            $oModule->onFanRemovedFromAdmins($iGroupProfileId, $iFanId);
+        
+        return true;
+    }
+
+    public function deleteAdminsByGroupId ($iGroupProfileId)
+    {
+        $sQuery = $this->prepare("DELETE FROM `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` WHERE `group_profile_id` = ?", $iGroupProfileId);
+        return $this->res($sQuery);
+    }
+
+    public function deleteAdminsByProfileId ($iProfileId)
+    {
+        $sQuery = $this->prepare("DELETE FROM `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` WHERE `fan_id` = ?", $iProfileId);
+        return $this->res($sQuery);
+    }
+
+    public function isAdmin ($iGroupProfileId, $iFanId, $aDataEntry = array())
+    {
+        if (isset($aDataEntry[$this->_oConfig->CNF['FIELD_AUTHOR']]) && $iFanId == $aDataEntry[$this->_oConfig->CNF['FIELD_AUTHOR']])
+            return true;
+
+        $sQuery = $this->prepare("SELECT `id` FROM `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` WHERE `group_profile_id` = ? AND `fan_id` = ?", $iGroupProfileId, $iFanId);
+        return $this->getOne($sQuery) ? true : false;
+    }
+
+    public function getAdmins ($iGroupProfileId)
+    {
+        $sQuery = $this->prepare("SELECT `fan_id` FROM `" . $this->_oConfig->CNF['TABLE_ADMINS'] . "` WHERE `group_profile_id` = ?", $iGroupProfileId);
+        return $this->getColumn($sQuery);
+    }
+}
+
+/** @} */

+ 114 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/groups/classes/BxBaseModGroupsMenuSnippetMeta.php

@@ -0,0 +1,114 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModGroupsMenuSnippetMeta extends BxBaseModProfileMenuSnippetMeta
+{
+    public function __construct($aObject, $oTemplate = false)
+    {
+        parent::__construct($aObject, $oTemplate);
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $this->_aConnectionToFunctionCheck[$CNF['OBJECT_CONNECTIONS']] = array(
+			'add' => 'checkAllowedFanAdd', 
+			'remove' => 'checkAllowedFanRemove'
+        );
+
+        $this->_aConnectionToFunctionTitle[$CNF['OBJECT_CONNECTIONS']] = '_getMenuItemConnectionsTitle';
+    }
+
+    protected function _getMenuItemJoin($aItem)
+    {
+        return $this->_getMenuItemConnection($this->_oModule->_oConfig->CNF['OBJECT_CONNECTIONS'], 'add', $aItem);
+    }
+
+    protected function _getMenuItemLeave($aItem)
+    {
+        return $this->_getMenuItemConnection($this->_oModule->_oConfig->CNF['OBJECT_CONNECTIONS'], 'remove', $aItem);
+    }
+
+    protected function _getMenuItemMembers($aItem)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(!$this->_bContentPublic || !$this->_oContentProfile || empty($CNF['OBJECT_CONNECTIONS']))
+            return false;
+
+        $oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS']);
+        if(!$oConnection)
+            return false;
+
+        $iFriends = $oConnection->getConnectedInitiatorsCount($this->_oContentProfile->id(), true);
+        if(!$iFriends)
+            return false;
+
+        return $this->_oTemplate->getUnitMetaItemText(_t('_sys_menu_item_title_sm_members', $iFriends));
+    }
+
+    protected function _getMenuItemConnectionsTitle($sAction, &$oConnection)
+    {
+        $iProfile = bx_get_logged_profile_id();
+        $iContentProfile = $this->_oContentProfile->id();
+
+        $aResult = array();
+        if($oConnection->isConnectedNotMutual($iProfile, $iContentProfile))
+            $aResult = array(
+                'add' => '',
+                'remove' => _t('_sys_menu_item_title_sm_leave_cancel'),
+            );
+        else if($oConnection->isConnectedNotMutual($iContentProfile, $iProfile))
+            $aResult = array(
+                'add' => _t('_sys_menu_item_title_sm_join_confirm'),
+                'remove' => _t('_sys_menu_item_title_sm_leave_reject'),
+            );
+        else if($oConnection->isConnected($iProfile, $iContentProfile, true))
+            $aResult = array(
+                'add' => '',
+                'remove' => _t('_sys_menu_item_title_sm_leave'),
+            );
+        else
+            $aResult = array(
+                'add' => _t('_sys_menu_item_title_sm_join'),
+                'remove' => '',
+            );
+
+        return !empty($sAction) && isset($aResult[$sAction]) ? $aResult[$sAction] : $aResult;
+    }
+
+    protected function _getMenuItemCountry($aItem)
+    {
+        return $this->_getMenuItemLocation($aItem, true);
+    }
+
+    protected function _getMenuItemCountryCity($aItem)
+    {
+        return $this->_getMenuItemLocation($aItem, false);
+    }
+
+    protected function _getMenuItemLocation($aItem, $bCountryOnly = false)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if(!$this->_bContentPublic || !$this->_oContentProfile || empty($CNF['OBJECT_CONNECTIONS']))
+            return false;
+
+        $oMeta = BxDolMetatags::getObjectInstance($CNF['OBJECT_METATAGS']);
+        if(!$oMeta)
+            return false;
+
+        if (!($sLocation = $oMeta->locationsString($this->_iContentId, false, $bCountryOnly ? array('country_only' => 1) : array('country_city_only' => 1))))
+            return false;
+
+        return $this->_oTemplate->getUnitMetaItemText($sLocation);
+    }
+}
+
+/** @} */

+ 593 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/groups/classes/BxBaseModGroupsModule.php

@@ -0,0 +1,593 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Groups profiles module.
+ */
+class BxBaseModGroupsModule extends BxBaseModProfileModule
+{
+    function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+    }
+
+    /**
+     * Get possible recipients for start conversation form
+     */
+    public function actionAjaxGetInitialMembers ()
+    {
+        $sTerm = bx_get('term');
+
+        $a = BxDolService::call('system', 'profiles_search', array($sTerm), 'TemplServiceProfiles');
+
+        header('Content-Type:text/javascript; charset=utf-8');
+        echo(json_encode($a));
+    }
+
+    public function serviceGetSearchResultUnit ($iContentId, $sUnitTemplate = '')
+    {
+        if(empty($sUnitTemplate))
+            $sUnitTemplate = 'unit.html';
+
+        return parent::serviceGetSearchResultUnit($iContentId, $sUnitTemplate);
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * check if provided profile is member if the group 
+     */ 
+    public function serviceIsFan ($iGroupProfileId, $iProfileId = false) 
+    {
+        $oGroupProfile = BxDolProfile::getInstance($iGroupProfileId);
+        return $this->isFan($oGroupProfile->getContentId(), $iProfileId);
+    }
+
+    /**
+     * 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 
+     * @return false of error, or number of updated records on success
+     */
+    public function serviceReassignEntityAuthor ($iContentId)
+    {
+        $aContentInfo = $this->_oDb->getContentInfoById((int)$iContentId);
+        if (!$aContentInfo)
+            return false;
+
+        if (!($oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName())))
+            return false;
+
+        $aAdmins = $this->_oDb->getAdmins($oGroupProfile->id());
+
+        return $this->_oDb->updateAuthorById($iContentId, $aAdmins ? array_pop($aAdmins) : 0);
+    }
+
+    /**
+     * Reset group's author when author profile is deleted
+     * @param $iProfileId profile id 
+     * @return number of changed items
+     */
+    public function serviceReassignEntitiesByAuthor ($iProfileId)
+    {
+        $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']]) ? 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, $iIgnoreJoinConfirmation = 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 (!($oConnection = BxDolConnection::getObjectInstance($CNF['OBJECT_CONNECTIONS'])))
+            return false;
+
+        $sEntryTitle = $aContentInfo[$CNF['FIELD_NAME']];
+        $sEntryUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+
+        // send invitation to the group 
+        if ($iIgnoreJoinConfirmation && !$oConnection->isConnected((int)$iInitiatorId, $oGroupProfile->id()) && !$oConnection->isConnected($oGroupProfile->id(), (int)$iInitiatorId) && bx_get_logged_profile_id() != $iProfileId) {
+
+            bx_alert($this->getName(), '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));
+
+        }
+        // send notification to group's admins that new connection is pending confirmation 
+        elseif (!$iIgnoreJoinConfirmation && $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 (!$iIgnoreJoinConfirmation && $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)) {
+            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,
+            ));
+            return false;
+        }
+
+        // don't automatically add back connection (mutual) if group requires manual join confirmation
+        if (!$iIgnoreJoinConfirmation && $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 ()
+    {
+        $oGrid = BxDolGrid::getObjectInstance($this->_oConfig->CNF['OBJECT_GRID_CONNECTIONS']);
+        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 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;
+
+        $aProfiles = $this->_oDb->getAdmins($oGroupProfile->id());
+        if(empty($aProfiles) || !is_array($aProfiles))
+            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;
+
+        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 serviceEntityInvite ($iContentId = 0)
+    {
+        return $this->_serviceEntityForm ('editDataForm', $iContentId, $this->_oConfig->CNF['OBJECT_FORM_ENTRY_DISPLAY_INVITE']);
+    }
+    
+    /**
+     * Entry social sharing block
+     */
+    public function serviceEntitySocialSharing ($iContentId = 0)
+    {
+        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;
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType((int)$iContentId, $this->getName());
+        if (!$oGroupProfile)
+            return false;
+
+        $CNF = &$this->_oConfig->CNF;
+        return $this->_entitySocialSharing ($iContentId, array(
+            'id_timeline' => $iContentId,
+            'id_thumb' => 0,
+            'title' => $oGroupProfile->getDisplayName(),
+            'object_storage' => false,
+            'object_transcoder' => false,
+            'object_vote' => $CNF['OBJECT_VOTES'],
+        	'object_favorite' => $CNF['OBJECT_FAVORITES'],
+        	'object_feature' => $CNF['OBJECT_FEATURED'],
+        	'object_report' => $CNF['OBJECT_REPORTS'],
+            'uri_view_entry' => $CNF['URI_VIEW_ENTRY']
+        ));
+    }
+
+	/**
+     * Data for Notifications module
+     */
+    public function serviceGetNotificationsData()
+    {
+    	$sModule = $this->_aModule['name'];
+
+        return array(
+            'handlers' => array(
+                array('group' => $sModule . '_vote', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'doVote', 'module_name' => $sModule, 'module_method' => 'get_notifications_vote', 'module_class' => 'Module'),
+                array('group' => $sModule . '_vote', 'type' => 'delete', 'alert_unit' => $sModule, 'alert_action' => 'undoVote'),
+
+                array('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'),
+
+                array('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']),
+                
+                array('group' => $sModule . '_timeline_post_common', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'timeline_post_common', 'module_name' => $sModule, 'module_method' => 'get_notifications_timeline_post_common', 'module_class' => 'Module'),
+            ),
+            'alerts' => array(
+                array('unit' => $sModule, 'action' => 'doVote'),
+                array('unit' => $sModule, 'action' => 'undoVote'),
+                array('unit' => $sModule, 'action' => 'fan_added'),
+                array('unit' => $sModule, 'action' => 'join_request'),
+                array('unit' => $sModule, 'action' => 'timeline_post_common'),
+            )
+        );
+    }
+
+    /**
+     * 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 instead of Personal Profile of
+         * a member, who performed an action.
+         */
+        $sEntryUrl = $sSubentryUrl = $oGroupProfile->getUrl();
+		return array(
+			'entry_sample' => $CNF['T']['txt_sample_single'],
+			'entry_url' => $sEntryUrl,
+			'entry_caption' => $oGroupProfile->getDisplayName(),
+			'entry_author' => $oGroupProfile->id(),
+			'subentry_sample' => $oProfile->getDisplayName(),
+			'subentry_url' => $sSubentryUrl,
+			'lang_key' => $sLangKey
+		);
+    }
+
+    /**
+     * Entry post for Timeline module
+     */
+    public function serviceGetTimelinePost($aEvent, $aBrowseParams = array())
+    {
+        $a = parent::serviceGetTimelinePost($aEvent, $aBrowseParams);
+
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($aEvent['object_id'], $this->getName());
+
+        $a['content']['url'] = $oGroupProfile->getUrl();
+        $a['content']['title'] = $oGroupProfile->getDisplayName();
+        
+        return $a;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedPost ($aDataEntry, $isPerformAction = false)
+    {
+        if ($this->isFan($aDataEntry[$this->_oConfig->CNF['FIELD_ID']]))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedView ($aDataEntry, $isPerformAction = false)
+    {
+        if ($this->isFan($aDataEntry[$this->_oConfig->CNF['FIELD_ID']]))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return parent::checkAllowedView ($aDataEntry, $isPerformAction);
+    }
+
+    /**
+     * @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)
+    {
+        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);
+    }
+
+    public function checkAllowedManageAdmins ($mixedDataEntry, $isPerformAction = false)
+    {
+        if (is_array($mixedDataEntry)) {
+            $aDataEntry = $mixedDataEntry;
+        }
+        else {
+            $oGroupProfile = BxDolProfile::getInstance((int)$mixedDataEntry);
+            $aDataEntry = $oGroupProfile && $this->getName() == $oGroupProfile->getModule() ? $this->_oDb->getContentInfoById($oGroupProfile->getContentId()) : array();
+        }
+
+        return parent::checkAllowedEdit ($aDataEntry, $isPerformAction);
+    }
+
+    public function checkAllowedEdit ($aDataEntry, $isPerformAction = false)
+    {
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->getName());
+        if ($this->_oDb->isAdmin($oGroupProfile->id(), bx_get_logged_profile_id(), $aDataEntry))
+            return CHECK_ACTION_RESULT_ALLOWED;
+        return parent::checkAllowedEdit ($aDataEntry, $isPerformAction);
+    }
+
+    public function checkAllowedInvite ($aDataEntry, $isPerformAction = false)
+    {
+        return $this->checkAllowedEdit ($aDataEntry, $isPerformAction);
+    }
+    
+    public function checkAllowedChangeCover ($aDataEntry, $isPerformAction = false)
+    {
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->getName());
+        if ($this->_oDb->isAdmin($oGroupProfile->id(), bx_get_logged_profile_id(), $aDataEntry))
+            return CHECK_ACTION_RESULT_ALLOWED;
+        return parent::checkAllowedChangeCover ($aDataEntry, $isPerformAction);
+    }
+
+    public function checkAllowedDelete (&$aDataEntry, $isPerformAction = false)
+    {
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->getName());
+        if ($oGroupProfile && $this->_oDb->isAdmin($oGroupProfile->id(), bx_get_logged_profile_id(), $aDataEntry))
+            return CHECK_ACTION_RESULT_ALLOWED;
+        return parent::checkAllowedDelete ($aDataEntry, $isPerformAction);
+    }
+
+    public function checkAllowedSubscribeAdd (&$aDataEntry, $isPerformAction = false)
+    {
+        if (!$this->isFan($aDataEntry[$this->_oConfig->CNF['FIELD_ID']]))
+            return _t('_sys_txt_access_denied');
+
+        return parent::checkAllowedSubscribeAdd ($aDataEntry, $isPerformAction);
+    }
+
+    protected function _checkAllowedConnect (&$aDataEntry, $isPerformAction, $sObjConnection, $isMutual, $isInvertResult, $isSwap = false)
+    {
+        $sResult = $this->checkAllowedView($aDataEntry);
+
+        $oPrivacy = BxDolPrivacy::getObjectInstance($this->_oConfig->CNF['OBJECT_PRIVACY_VIEW']);
+
+        if (CHECK_ACTION_RESULT_ALLOWED !== $sResult && !$oPrivacy->isPartiallyVisible($aDataEntry[$this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']]))
+            return $sResult;
+
+        return parent::_checkAllowedConnect ($aDataEntry, $isPerformAction, $sObjConnection, $isMutual, $isInvertResult, $isSwap);
+    }
+
+    public function isFan ($iContentId, $iProfileId = false) 
+    {
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->getName());
+
+        return $oGroupProfile && ($oConnection = BxDolConnection::getObjectInstance($this->_oConfig->CNF['OBJECT_CONNECTIONS'])) && $oConnection->isConnected($iProfileId ? $iProfileId : bx_get_logged_profile_id(), $oGroupProfile->id(), true);
+    }
+
+    protected function _getImagesForTimelinePost($aEvent, $aContentInfo, $sUrl, $aBrowseParams = array())
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!isset($CNF['FIELD_PICTURE']) || empty($aContentInfo[$CNF['FIELD_PICTURE']]))
+            return array();
+
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($aEvent['object_id'], $this->getName());
+
+        return array(
+		    array('url' => $sUrl, 'src' => $oGroupProfile->getPicture()),
+		);
+    }
+
+    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);
+    }
+}
+
+/** @} */

+ 90 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/groups/classes/BxBaseModGroupsPrivacy.php

@@ -0,0 +1,90 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModGroupsPrivacy extends BxBaseModProfilePrivacy
+{    
+    public function __construct($aOptions, $oTemplate = false)
+    {
+        parent::__construct($aOptions, $oTemplate);
+
+        $this->_aPrivacyParticallyVisible = array_merge($this->_aPrivacyParticallyVisible, array('c', BX_DOL_PG_FRIENDS));
+    }
+
+	/**
+     * Check whethere viewer is a member of dynamic group.
+     *
+     * @param  mixed   $mixedGroupId   dynamic group ID.
+     * @param  integer $iObjectOwnerId object owner ID.
+     * @param  integer $iViewerId      viewer ID.
+     * @return boolean result of operation.
+     */
+    public function isDynamicGroupMember($mixedGroupId, $iObjectOwnerId, $iViewerId, $iObjectId)
+    {
+        if ('c' == $mixedGroupId)
+            return $this->isClosedGroupAccess($iObjectOwnerId, $iViewerId, $iObjectId);
+
+        if ('s' == $mixedGroupId)
+            return $this->isSecretGroupAccess($iObjectOwnerId, $iViewerId, $iObjectId);
+
+        return false;
+    }
+
+    public function isClosedGroupAccess ($iObjectOwnerId, $iViewerId, $iObjectId)
+    {
+        if (!($oConnection = BxDolConnection::getObjectInstance($this->_oModule->_oConfig->CNF['OBJECT_CONNECTIONS'])))
+            return false;
+
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType($iObjectId, $this->MODULE);
+        return $oConnection->isConnected($iViewerId, $oGroupProfile->id(), true);
+    }
+
+    public function isSecretGroupAccess ($iObjectOwnerId, $iViewerId, $iObjectId)
+    {
+        return $this->isClosedGroupAccess($iObjectOwnerId, $iViewerId, $iObjectId);
+    }
+
+    static function getGroupChooser ($sObject, $iOwnerId = 0, $aParams = array())
+    {
+        if (!$iOwnerId)
+    		$iOwnerId = bx_get_logged_profile_id();
+
+		$aParams['dynamic_groups'] = array(
+			array ('key' => '', 'value' => '----'),
+			array ('key' => 'c', 'value' => _t('_sys_ps_group_title_closed')),
+			array ('key' => 's', 'value' => _t('_sys_ps_group_title_secret')),
+		);
+
+        return parent::getGroupChooser($sObject, $iOwnerId, $aParams);
+
+        return $a;
+    }
+
+    protected function getGroups() 
+    {
+        $a = parent::getGroups();
+    
+        foreach ($a as $i => $r) {
+            if (BX_DOL_PG_FRIENDS == $r['key']) {
+                unset($a[$i]);
+                break;
+            }
+        }
+
+        return $a;
+    }
+
+    protected function getObjectInfo($sAction, $iObjectId)
+    {
+        return BxDolPrivacy::getObjectInfo($sAction, $iObjectId);
+    }
+}
+
+/** @} */

+ 86 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/groups/classes/BxBaseModGroupsSearchResult.php

@@ -0,0 +1,86 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseGroups Base classes for groups modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModGroupsSearchResult extends BxBaseModProfileSearchResult
+{    
+    public function __construct($sMode = '', $aParams = array())
+    {
+        if (!isset($aParams['unit_views']))
+            $aParams['unit_views'] = array('gallery' => 'unit.html', 'showcase' => 'unit_with_cover_showcase.html');
+        parent::__construct($sMode, $aParams);
+    }
+
+    protected function addConditionsForPrivateContent($CNF, $oProfile, $aCustomGroup = array()) 
+    {
+        if(empty($CNF['OBJECT_PRIVACY_VIEW']))
+            return;
+
+        $oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW']);
+        if(!$oPrivacy)
+            return;
+
+        parent::addConditionsForPrivateContent($CNF, $oProfile, array_merge($aCustomGroup, $oPrivacy->getPartiallyVisiblePrivacyGroups()));
+    }
+
+    protected function _setAuthorConditions($sMode, $aParams, &$oProfileAuthor)
+    {
+        $CNF = &$this->oModule->_oConfig->CNF;
+
+        $oProfileAuthor = BxDolProfile::getInstance((int)$aParams['author']);
+        if (!$oProfileAuthor) 
+            return false;
+
+        $iProfileAuthor = $oProfileAuthor->id();
+        $this->aCurrent['restriction']['owner']['value'] = $iProfileAuthor;
+
+        if(!empty($aParams['per_page']))
+        	$this->aCurrent['paginate']['perPage'] = is_numeric($aParams['per_page']) ? (int)$aParams['per_page'] : (int)getParam($aParams['per_page']);
+
+        $this->sBrowseUrl = 'page.php?i=' . $CNF['URI_JOINED_ENTRIES'] . '&profile_id={profile_id}';
+        $this->aCurrent['title'] = _t($CNF['T']['txt_all_entries_by_author']);
+        $this->aCurrent['rss']['link'] = 'modules/?r=' . $this->oModule->_oConfig->getUri() . '/rss/' . $sMode . '/' . $iProfileAuthor;
+
+        return true;
+    }
+
+    protected function _setFavoriteConditions($sMode, $aParams, &$oProfileAuthor)
+    {
+        $CNF = &$this->oModule->_oConfig->CNF;
+
+        $oProfileAuthor = BxDolProfile::getInstance((int)$aParams['user']);
+        if (!$oProfileAuthor) 
+            return false;
+
+        $iProfileAuthor = $oProfileAuthor->id();
+        $oFavorite = $this->oModule->getObjectFavorite();
+        if(!$oFavorite->isPublic() && $iProfileAuthor != bx_get_logged_profile_id()) 
+            return false;
+
+        $aConditions = $oFavorite->getConditionsTrack($CNF['TABLE_ENTRIES'], 'id', $iProfileAuthor);
+        if(!empty($aConditions) && is_array($aConditions)) {
+            if(empty($this->aCurrent['restriction']) || !is_array($this->aCurrent['restriction']))
+                $this->aCurrent['restriction'] = array();
+            $this->aCurrent['restriction'] = array_merge($this->aCurrent['restriction'], $aConditions['restriction']);
+
+            if(empty($this->aCurrent['join']) || !is_array($this->aCurrent['join']))
+                $this->aCurrent['join'] = array();
+            $this->aCurrent['join'] = array_merge($this->aCurrent['join'], $aConditions['join']);
+        }
+
+        $this->sBrowseUrl = 'page.php?i=' . $CNF['URI_JOINED_ENTRIES'] . '&profile_id={profile_id}';
+        $this->aCurrent['title'] = _t($CNF['T']['txt_all_entries_by_author']);
+        $this->aCurrent['rss']['link'] = 'modules/?r=' . $this->oModule->_oConfig->getUri() . '/rss/' . $sMode . '/' . $iProfileAuthor;
+
+        return true;
+    }
+}
+
+/** @} */

+ 1 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/groups/template/unit_with_cover_showcase.html

@@ -0,0 +1 @@
+<bx_include_auto:unit.html />

+ 206 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/profile/classes/BxBaseModProfileFormsEntryHelper.php

@@ -0,0 +1,206 @@
+<?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 forms helper functions
+ */
+class BxBaseModProfileFormsEntryHelper extends BxBaseModGeneralFormsEntryHelper
+{
+    protected $_bAutoApproval = false;
+
+    public function __construct($oModule)
+    {
+        parent::__construct($oModule);
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $bAutoApproval = isset($CNF['PARAM_AUTOAPPROVAL']) ? (bool)getParam($CNF['PARAM_AUTOAPPROVAL']) : true;
+        $bAdministrator = BxDolAcl::getInstance()->isMemberLevelInSet(array(MEMBERSHIP_ID_ADMINISTRATOR));
+        $this->setAutoApproval(isAdmin() || $bAdministrator || $bAutoApproval);
+    }
+
+    public function isAutoApproval()
+    {
+        return $this->_bAutoApproval;
+    }
+
+    public function setAutoApproval($b)
+    {
+        return ($this->_bAutoApproval = $b);
+    }
+
+    protected function _getProfileAndContentData ($iContentId)
+    {
+        $aContentInfo = array();
+        $oProfile = false;
+
+        $aContentInfo = $this->_oModule->_oDb->getContentInfoById($iContentId);
+        if (!$aContentInfo)
+            return array (false, false);
+
+        $oProfile = BxDolProfile::getInstance($aContentInfo['profile_id']);
+        if (!$oProfile) 
+            $oProfile = BxDolProfileUndefined::getInstance();
+
+        return array ($oProfile, $aContentInfo);
+    }
+
+    public function deleteData ($iContentId, $aContentInfo = false, $oProfile = null, $oForm = null)
+    {
+        if (!$aContentInfo)
+            list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+
+        // delete profile
+        $oProfile = BxDolProfile::getInstance($aContentInfo['profile_id']);
+        if (!$oProfile->delete())
+            return _t('_sys_txt_error_entry_delete');
+
+        return '';
+    }
+
+    public function deleteDataService ($iContentId, $aContentInfo = false, $oProfile = null, $oForm = null)
+    {
+        return parent::deleteData ($iContentId, $aContentInfo, $oProfile, $oForm);
+    }
+
+    public function onDataEditBefore ($iContentId, $aContentInfo, &$aTrackTextFieldsChanges)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        $aProfileInfo = $oProfile->getInfo();
+
+        if (!$this->isAutoApproval() && BX_PROFILE_STATUS_ACTIVE == $aProfileInfo['status'])
+            $aTrackTextFieldsChanges = array ();
+    }
+
+    public function onDataEditAfter ($iContentId, $aContentInfo, $aTrackTextFieldsChanges, $oProfile, $oForm)
+    {
+        if ($s = parent::onDataEditAfter($iContentId, $aContentInfo, $aTrackTextFieldsChanges, $oProfile, $oForm))
+            return $s;
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $aProfileInfo = $oProfile->getInfo();
+
+        // change profile to 'pending' only if profile is 'active'
+        if (!$this->isAutoApproval() && BX_PROFILE_STATUS_ACTIVE == $aProfileInfo['status'] && !empty($aTrackTextFieldsChanges['changed_fields']))
+            $oProfile->disapprove(BX_PROFILE_ACTION_AUTO, 0, $this->_oModule->serviceActAsProfile());
+
+        // process uploaded files
+        if (isset($CNF['FIELD_PICTURE']))
+            $oForm->processFiles($CNF['FIELD_PICTURE'], $iContentId, false);
+        if (isset($CNF['FIELD_COVER']))
+            $oForm->processFiles($CNF['FIELD_COVER'], $iContentId, false);
+
+        // create an alert
+        bx_alert($this->_oModule->getName(), 'edited', $aContentInfo[$CNF['FIELD_ID']]);
+        bx_alert('profile', 'edit', $oProfile->id(), 0, array('content' => $iContentId, 'module' => $this->_oModule->getName()));
+
+        return '';
+    }
+
+    public function onDataAddAfter ($iAccountId, $iContentId)
+    {
+        // parent method isn't processing metas, so we are processing it below
+        if ($s = parent::onDataAddAfter($iAccountId, $iContentId))
+            return $s;
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // add account and content association
+        $iProfileId = BxDolProfile::add(BX_PROFILE_ACTION_MANUAL, $iAccountId, $iContentId, BX_PROFILE_STATUS_PENDING, $this->_oModule->getName());
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+
+        // approve profile if auto-approval is enabled and profile status is 'pending'
+        $sStatus = $oProfile->getStatus();
+        if ($sStatus == BX_PROFILE_STATUS_PENDING && $this->isAutoApproval())
+            $oProfile->approve(BX_PROFILE_ACTION_AUTO, 0, $this->_oModule->serviceActAsProfile());
+
+        // set created profile some default membership
+        if ((int)bx_get('level_id') && bx_srv('bx_acl', 'get_prices', [(int)bx_get('level_id'), true]))
+            $iAclLevel = (int)bx_get('level_id');
+        else
+            $iAclLevel = !isset($CNF['PARAM_DEFAULT_ACL_LEVEL']) ? MEMBERSHIP_ID_STANDARD : 
+               (isAdmin() ? MEMBERSHIP_ID_ADMINISTRATOR : getParam($CNF['PARAM_DEFAULT_ACL_LEVEL']));
+        BxDolAcl::getInstance()->setMembership($iProfileId, $iAclLevel, 0, true);
+
+        // process metas
+        $this->_processMetas($iAccountId, $iContentId);
+        
+        // process uploaded files
+        $oForm = $this->getObjectFormAdd();
+        if (isset($CNF['FIELD_PICTURE']))
+            $oForm->processFiles($CNF['FIELD_PICTURE'], $iContentId, true);
+        if (isset($CNF['FIELD_COVER']))
+            $oForm->processFiles($CNF['FIELD_COVER'], $iContentId, true);
+
+        // alert
+        bx_alert($this->_oModule->getName(), 'added', $iContentId);
+
+        // switch context to the created profile
+        if ($this->_oModule->serviceActAsProfile()) {
+            $oAccount = BxDolAccount::getInstance($iAccountId);
+            $oAccount->updateProfileContext($iProfileId);
+        }
+
+        return '';
+    }
+
+    public function redirectAfterAdd($aContentInfo)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $oSession = BxDolSession::getInstance();
+
+        $sRedirectType = empty($CNF['PARAM_REDIRECT_AADD']) ? BX_DOL_PROFILE_REDIRECT_PROFILE : getParam($CNF['PARAM_REDIRECT_AADD']);
+        $sRedirectDefault = 'page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']];
+
+        $sRedirectUrl = $sRedirectDefault;
+        switch ($sRedirectType) {
+            case BX_DOL_PROFILE_REDIRECT_PROFILE:
+                break;
+
+            // if user just joined the redirect to the page where user comes from. 
+            case BX_DOL_PROFILE_REDIRECT_LAST:
+                $sJoinReferrer = $oSession->getValue('join-referrer');
+                if($sJoinReferrer) {
+                    $sRedirectUrl = $sJoinReferrer;
+                    $oSession->unsetValue('join-referrer');
+                }
+                break;
+
+            case BX_DOL_PROFILE_REDIRECT_CUSTOM:
+                $sRedirectCustom = getParam($CNF['PARAM_REDIRECT_AADD_CUSTOM_URL']);
+                if($sRedirectCustom) {
+                    $sRedirectUrl = $this->prepareCustomRedirectUrl($sRedirectCustom, $aContentInfo);
+                }
+        }
+
+        $sCustomReferrer = $oSession->getValue('custom-referrer');
+        if($sCustomReferrer) {
+            $sRedirectUrl = $sCustomReferrer;
+            $oSession->unsetValue('custom-referrer');
+        }
+
+        if($sRedirectUrl) {
+            if(!bx_has_proto($sRedirectUrl))
+                $sRedirectUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink($sRedirectUrl);
+
+            header('Location: ' . $sRedirectUrl);
+            exit;
+        }
+
+        parent::redirectAfterAdd($aContentInfo);
+    }
+}
+
+/** @} */

+ 908 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/profile/classes/BxBaseModProfileModule.php

@@ -0,0 +1,908 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseProfile Base classes for profile modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+define('BX_DOL_PROFILE_REDIRECT_PROFILE', 'profile');
+define('BX_DOL_PROFILE_REDIRECT_LAST', 'last');
+define('BX_DOL_PROFILE_REDIRECT_CUSTOM', 'custom');
+
+/**
+ * Base class for profile modules.
+ */
+class BxBaseModProfileModule extends BxBaseModGeneralModule implements iBxDolContentInfoService, iBxDolProfileService
+{
+    protected $_iAccountId;
+
+    function __construct(&$aModule)
+    {
+        parent::__construct($aModule);
+        $this->_iAccountId = getLoggedId();
+    }
+
+    public function actionDeleteProfileImg($iFileId, $iContentId, $sFieldPicture) 
+    {
+        $aResult = array();
+        $CNF = &$this->_oConfig->CNF;
+
+        $oSrorage = BxDolStorage::getObjectInstance($CNF['OBJECT_STORAGE']);
+        if (!($aFile = $oSrorage->getFile((int)$iFileId)) || !($aContentInfo = $this->_oDb->getContentInfoById($iContentId)) || $aContentInfo[$sFieldPicture] != (int)$iFileId)
+            $aResult = array('error' => 1, 'msg' => _t('_sys_storage_err_file_not_found'));
+
+        $oAccountProfile = BxDolProfile::getInstanceAccountProfile();
+        if ($oAccountProfile)
+            $iAccountProfileId = $oAccountProfile->id();
+
+        if ((!$aResult && !isLogged()) || (!$aResult && $aFile['profile_id'] != $iAccountProfileId && !$this->_isModerator()))           
+            $aResult = array('error' => 2, 'msg' => _t('_Access denied'));
+
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD'], $this->_oTemplate);
+
+        if (!$aResult && !$oForm->_deleteFile($iContentId, $sFieldPicture, (int)$iFileId, true))
+            $aResult = array('error' => 3, 'msg' => _t('_Failed'));
+        elseif (!$aResult)            
+            $aResult = array('error' => 0, 'msg' => '');
+
+        header('Content-Type: application/json; charset=utf-8');
+        echo json_encode($aResult);
+    }
+
+    // ====== SERVICE METHODS
+    public function serviceGetOptionsRedirectAfterAdd()
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aResult = array();
+        $aChoices = array(BX_DOL_PROFILE_REDIRECT_PROFILE, BX_DOL_PROFILE_REDIRECT_LAST, BX_DOL_PROFILE_REDIRECT_CUSTOM);
+        foreach($aChoices as $sChoice) 
+            $aResult[] = array('key' => $sChoice, 'value' => _t($CNF['T']['option_redirect_aadd_' . $sChoice]));
+
+        return $aResult;
+    }
+
+    public function serviceGetThumb ($iContentId, $sTranscoder = '') 
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($sTranscoder) && !empty($CNF['OBJECT_IMAGES_TRANSCODER_THUMB']))
+            $sTranscoder = $CNF['OBJECT_IMAGES_TRANSCODER_THUMB'];
+
+        $mixedResult = $this->_getFieldValueThumb('FIELD_PICTURE', $iContentId, $sTranscoder);
+        return $mixedResult !== false ? $mixedResult : '';
+    }
+
+    public function serviceGetSearchResultUnit ($iContentId, $sUnitTemplate = '')
+    {
+        if(empty($sUnitTemplate))
+            $sUnitTemplate = 'unit_with_cover.html';
+
+        return parent::serviceGetSearchResultUnit($iContentId, $sUnitTemplate);
+    }
+
+	public function servicePrivateProfileMsg()
+    {
+        return MsgBox(_t('_sys_access_denied_to_private_content'));
+    }
+    
+	public function serviceGetContentInfoById($iContentId)
+    {
+        return $this->_oDb->getContentInfoById((int)$iContentId);
+    }
+
+	public function serviceGetMenuAddonManageTools()
+	{
+		bx_import('SearchResult', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'SearchResult';
+        $o = new $sClass();
+        $o->fillFilters(array(
+			'perofileStatus' => BX_PROFILE_STATUS_PENDING
+        ));
+        $o->unsetPaginate();
+
+        return $o->getNum();
+	}
+
+	public function serviceGetMenuAddonManageToolsProfileStats()
+	{
+		bx_import('SearchResult', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'SearchResult';
+        $o = new $sClass();
+        $o->fillFilters(array(
+			'account_id' => getLoggedId(),
+        	'perofileStatus' => ''
+        ));
+        $o->unsetPaginate();
+
+        return $o->getNum();
+	}
+
+    public function serviceGetMenuAddonFavoritesProfileStats()
+    {
+    	bx_import('SearchResult', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'SearchResult';
+        $o = new $sClass('favorite', array('user' => bx_get_logged_profile_id()));
+        $o->unsetPaginate();
+
+        return $o->getNum();
+    }
+
+    public function serviceGetSubmenuObject ()
+    {
+        return $this->_oConfig->CNF['OBJECT_MENU_SUBMENU_VIEW_ENTRY'];
+    }
+
+    public function serviceGetMenuSetNameForMenuTrigger ($sMenuTriggerName)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if ($CNF['TRIGGER_MENU_PROFILE_VIEW_SUBMENU'] == $sMenuTriggerName)
+            return $CNF['OBJECT_MENU_SUBMENU_VIEW_ENTRY'];
+        elseif ($CNF['TRIGGER_MENU_PROFILE_VIEW_ACTIONS'] == $sMenuTriggerName)
+            return $CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY'];
+        return '';
+    }
+
+	public function serviceGetPageObjectForPageTrigger ($sPageTriggerName)
+    {
+        if (isset($this->_oConfig->CNF['TRIGGER_PAGE_VIEW_ENTRY']) && $this->_oConfig->CNF['TRIGGER_PAGE_VIEW_ENTRY'] == $sPageTriggerName)
+        	return $this->_oConfig->CNF['OBJECT_PAGE_VIEW_ENTRY'];
+
+        return '';
+    }
+
+    public function serviceProfilesSearch ($sTerm, $iLimit)
+    {
+		$aRet = array();
+
+        $a = $this->_oDb->searchByTerm($sTerm, $iLimit);
+        foreach ($a as $r) {
+            $oProfile = BxDolProfile::getInstance($r['profile_id']);
+
+            $aRet[] = array (
+            	'label' => $this->serviceProfileName($r['content_id']), 
+                'value' => $r['profile_id'], 
+                'url' => $oProfile->getUrl(),
+            	'thumb' => $oProfile->getThumb(),
+                'unit' => $oProfile->getUnit(0, array('template' => 'unit_wo_info'))
+            );
+        }
+
+        return $aRet;
+    }
+
+    public function serviceProfileUnit ($iContentId, $aParams = array())
+    {
+        $mixedContent = $this->_getContent($iContentId);
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        $bCheckPrivateContent = isset($aParams['check_private_content']) ? (bool)$aParams['check_private_content'] : true;
+        $sTemplate = (!empty($aParams['template']) ? $aParams['template'] : 'unit') . '.html';
+
+        return $this->_oTemplate->unit($aContentInfo, $bCheckPrivateContent, $sTemplate);
+    }
+
+    public function serviceHasImage ($iContentId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $mixedContent = $this->_getContent($iContentId, 'getContentInfoById');
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        return !empty($aContentInfo[$CNF['FIELD_PICTURE']]);
+    }
+
+    public function serviceProfilePicture ($iContentId)
+    {
+        return $this->_serviceTemplateFunc('urlPicture', $iContentId);
+    }
+
+    public function serviceProfileAvatar ($iContentId)
+    {
+        return $this->_serviceTemplateFunc('urlAvatar', $iContentId);
+    }
+
+    public function serviceProfileEditUrl ($iContentId)
+    {
+        return BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $this->_oConfig->CNF['URI_EDIT_ENTRY'] . '&id=' . $iContentId);
+    }
+
+    public function serviceProfileThumb ($iContentId)
+    {
+        return $this->_serviceTemplateFunc('thumb', $iContentId);
+    }
+
+    public function serviceProfileIcon ($iContentId)
+    {
+        return $this->_serviceTemplateFunc('icon', $iContentId);
+    }
+
+    public function serviceProfileName ($iContentId)
+    {
+        if (!$iContentId)
+            return false;
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if (!$aContentInfo)
+            return false;
+        return bx_process_output($aContentInfo[$this->_oConfig->CNF['FIELD_NAME']]);
+    }
+
+    public function serviceProfileCreateUrl ($bAbsolute = true)
+    {
+    	$CNF = $this->_oConfig->CNF;
+    	if(empty($CNF['URL_CREATE']))
+    		return false;
+
+    	return $bAbsolute ? BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink($CNF['URL_CREATE']) : $CNF['URL_CREATE'];
+    }
+
+    public function serviceProfileUrl ($iContentId)
+    {
+        if (!$iContentId)
+            return false;
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if (!$aContentInfo)
+            return false;
+        $CNF = $this->_oConfig->CNF;
+        return BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aContentInfo[$CNF['FIELD_ID']]);
+    }
+    
+    /**
+     * @see iBxDolProfileService::serviceCheckAllowedProfileView
+     */ 
+    public function serviceCheckAllowedProfileView($iContentId)
+    {        
+        return $this->serviceCheckAllowedWithContent('View', $iContentId);
+    }
+
+    /**
+     * @see iBxDolProfileService::serviceCheckAllowedPostInProfile
+     */ 
+    public function serviceCheckAllowedPostInProfile($iContentId)
+    {
+        return $this->serviceCheckAllowedProfileView($iContentId);
+    }
+
+    /**
+     * Prepare fields from some universal set of fields to fields in particular profile module. 
+     * By default only 'name' and 'description' fields are supported.
+     * After fields convertion it can be used in @see BxBaseModGeneralModule::serviceEntityAdd
+     * @param $aFieldsProfile fields in soem universal format.
+     * @return array which is ready to use for particular module
+     */ 
+    public function servicePrepareFields ($aFieldsProfile)
+    {        
+        bx_alert($this->getName(), 'prepare_fields', 0, 0, array('fields_orig' => $aFieldsProfile, 'fields_result' => &$aFieldsProfile));
+
+        return $aFieldsProfile;
+    }
+
+    protected function _servicePrepareFields ($aFieldsProfile, $aFieldsDefault, $aMap)
+    {
+        $aFieldsOrig = $aFieldsProfile;
+
+        bx_import('BxDolPrivacy');
+        $aFieldsDefault2 = array(
+            'allow_view_to' => BX_DOL_PG_ALL,
+        );
+        $aFieldsProfile = array_merge($aFieldsDefault2, $aFieldsDefault, $aFieldsProfile);
+
+        foreach ($aMap as $k => $v) {
+            if (isset($aFieldsProfile[$v]))
+                $aFieldsProfile[$k] = $aFieldsProfile[$v];
+            if ($k != $v && !isset($aMap[$v]))
+                unset($aFieldsProfile[$v]);
+        }
+        
+        bx_alert($this->getName(), 'prepare_fields', 0, 0, array('fields_orig' => $aFieldsOrig, 'fields_result' => &$aFieldsProfile));
+
+        return $aFieldsProfile;
+    }
+
+    public function serviceFormsHelper ()
+    {
+        return parent::serviceFormsHelper ();
+    }
+
+    public function serviceEntityCreate ($sDisplay = false)
+    {
+	    BxDolInformer::getInstance($this->_oTemplate)->setEnabled(false);
+
+	    return parent::serviceEntityCreate ($sDisplay);
+    }
+
+    public function serviceActAsProfile ()
+    {
+        return true;
+    }
+
+    public function serviceBrowseRecentProfiles ($bDisplayEmptyMsg = false)
+    {
+        return $this->_serviceBrowse ('recent', false, BX_DB_PADDING_DEF, $bDisplayEmptyMsg);
+    }
+
+    public function serviceBrowseActiveProfiles ($bDisplayEmptyMsg = false)
+    {
+        return $this->_serviceBrowse ('active', false, BX_DB_PADDING_DEF, $bDisplayEmptyMsg);
+    }
+
+    public function serviceBrowseTopProfiles ($bDisplayEmptyMsg = false)
+    {
+        return $this->_serviceBrowse ('top', false, BX_DB_PADDING_DEF, $bDisplayEmptyMsg);
+    }
+    
+	public function serviceBrowseOnlineProfiles ($bDisplayEmptyMsg = false)
+    {
+        return $this->_serviceBrowse ('online', false, BX_DB_PADDING_DEF, $bDisplayEmptyMsg);
+    }
+
+    public function serviceBrowseConnections ($iProfileId, $sObjectConnections = 'sys_profiles_friends', $sConnectionsType = 'content', $iMutual = false, $iDesignBox = BX_DB_PADDING_DEF, $iProfileId2 = 0)
+    {
+        return $this->_serviceBrowse (
+            'connections',
+            array(
+                'object' => $sObjectConnections,
+                'type' => $sConnectionsType,
+                'mutual' => $iMutual,
+                'profile' => (int)$iProfileId,
+                'profile2' => (int)$iProfileId2),
+            $iDesignBox
+        );
+    }
+
+    public function serviceBrowseConnectionsQuick ($iProfileId, $sObjectConnections = 'sys_profiles_friends', $sConnectionsType = 'content', $iMutual = false, $iProfileId2 = 0)
+    {
+        // get connections object
+        $oConnection = BxDolConnection::getObjectInstance($sObjectConnections);
+        if (!$oConnection)
+            return '';
+
+        // set some vars
+        $iLimit = empty($this->_oConfig->CNF['PARAM_NUM_CONNECTIONS_QUICK']) ? 4 : getParam($this->_oConfig->CNF['PARAM_NUM_CONNECTIONS_QUICK']);
+        if (!$iLimit)
+            $iLimit = 4;
+        $iStart = (int)bx_get('start');
+
+        // get connections array
+        bx_import('BxDolConnection');
+        $a = $oConnection->getConnectionsAsArray ($sConnectionsType, $iProfileId, $iProfileId2, $iMutual, (int)bx_get('start'), $iLimit + 1, BX_CONNECTIONS_ORDER_ADDED_DESC);
+        if (!$a)
+            return '';
+
+        return $this->_serviceBrowseQuick($a, $iStart, $iLimit);
+    }
+
+	public function serviceBrowseByAcl ($mixedLevelId, $iDesignBox = BX_DB_PADDING_DEF)
+    {
+        return $this->_serviceBrowse (
+            'acl',
+            array(
+                'level' => $mixedLevelId,
+			),
+            $iDesignBox,
+            true
+        );
+    }
+
+    public function serviceEntityEditCover ($iContentId = 0)
+    {
+        return $this->_serviceEntityForm ('editDataForm', $iContentId, $this->_oConfig->CNF['OBJECT_FORM_ENTRY_DISPLAY_EDIT_COVER']);
+    }
+
+	/**
+     * Entry comments
+     */
+    public function serviceEntityCommentsByProfile ($iProfileId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        if(empty($CNF['OBJECT_COMMENTS']))
+            return '';
+
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if(!$oProfile)
+            return '';
+
+        return $this->_entityComments($CNF['OBJECT_COMMENTS'], $oProfile->getContentId());
+    }
+
+	/**
+     * Entry social sharing block
+     */
+    public function serviceEntitySocialSharing ($iContentId = 0)
+    {
+        $mixedContent = $this->_getContent($iContentId);
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        $CNF = &$this->_oConfig->CNF;
+        return $this->_entitySocialSharing ($iContentId, array(
+            'id_timeline' => 0,
+        	'id_thumb' => !empty($CNF['FIELD_PICTURE']) && !empty($aContentInfo[$CNF['FIELD_PICTURE']]) ? $aContentInfo[$CNF['FIELD_PICTURE']] : '',
+        	'title' => !empty($aContentInfo[$CNF['FIELD_TITLE']]) ? $aContentInfo[$CNF['FIELD_TITLE']] : '',
+        	'object_storage' => !empty($CNF['OBJECT_STORAGE']) ? $CNF['OBJECT_STORAGE'] : '',
+            'object_transcoder' => false,
+        	'object_vote' => '',
+        	'object_favorite' => !empty($CNF['OBJECT_FAVORITES']) ? $CNF['OBJECT_FAVORITES'] : '',
+        	'object_feature' => !empty($CNF['OBJECT_FEATURED']) ? $CNF['OBJECT_FEATURED'] : '',
+        	'object_report' => !empty($CNF['OBJECT_REPORTS']) ? $CNF['OBJECT_REPORTS'] : '',
+        	'uri_view_entry' => !empty($CNF['URI_VIEW_ENTRY']) ? $CNF['URI_VIEW_ENTRY'] : '',
+        	'social_sharing' => false
+        ));
+    }
+
+    public function serviceProfileMembership ($iContentId = 0)
+    {
+    	$mixedContent = $this->_getContent($iContentId);
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+		return BxDolAcl::getInstance()->getProfileMembership($aContentInfo['profile_id']);
+    }
+
+    public function serviceProfileFriends ($iContentId = 0)
+    {
+        $mixedContent = $this->_getContent($iContentId);
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        bx_import('BxDolConnection');
+        $s = $this->serviceBrowseConnectionsQuick ($aContentInfo['profile_id'], 'sys_profiles_friends', BX_CONNECTIONS_CONTENT_TYPE_CONTENT, true);
+        if (!$s)
+            return MsgBox(_t('_sys_txt_empty'));
+        return $s;
+    }
+
+    public function serviceProfileSubscriptions ($iContentId = 0)
+    {
+        $mixedContent = $this->_getContent($iContentId);
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        bx_import('BxDolConnection');
+        $s = $this->serviceBrowseConnectionsQuick ($aContentInfo['profile_id'], 'sys_profiles_subscriptions', BX_CONNECTIONS_CONTENT_TYPE_CONTENT);
+        if (!$s)
+            return MsgBox(_t('_sys_txt_empty'));
+
+        return $s;
+    }
+
+    public function serviceProfileSubscribedMe ($iContentId = 0)
+    {
+        $mixedContent = $this->_getContent($iContentId);
+        if($mixedContent === false)
+            return false;
+
+        list($iContentId, $aContentInfo) = $mixedContent;
+
+        bx_import('BxDolConnection');
+        $s = $this->serviceBrowseConnectionsQuick ($aContentInfo['profile_id'], 'sys_profiles_subscriptions', BX_CONNECTIONS_CONTENT_TYPE_INITIATORS);
+        if (!$s)
+            return MsgBox(_t('_sys_txt_empty'));
+
+        return $s;
+    }
+
+    /**
+     * For internal usage only.
+     */
+    public function serviceDeleteEntityService ($iContentId, $bDeleteWithContent = false)
+    {
+        return parent::serviceDeleteEntity ($iContentId, 'deleteDataService');
+    }
+
+	/**
+     * Data for Notifications module
+     */
+    public function serviceGetNotificationsData()
+    {
+        $a = parent::serviceGetNotificationsData();
+
+        $sModule = $this->_aModule['name'];
+        
+        $a['handlers'][] = array('group' => $sModule . '_timeline_post_common', 'type' => 'insert', 'alert_unit' => $sModule, 'alert_action' => 'timeline_post_common', 'module_name' => $sModule, 'module_method' => 'get_notifications_timeline_post_common', 'module_class' => 'Module');
+
+        $a['alerts'][] = array('unit' => $sModule, 'action' => 'timeline_post_common');
+
+        return $a;
+    }
+
+    /**
+     * Notification about new member requst in the group
+     */
+    public function serviceGetNotificationsTimelinePostCommon($aEvent)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $iContentId = (int)$aEvent['object_id'];
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType((int)$iContentId, $this->getName());
+        if(!$oGroupProfile)
+            return array();
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return array();
+            
+        $aSubcontentInfo = BxDolService::call('bx_timeline', 'get_info', array((int)$aEvent['subobject_id'], false));
+        if(empty($aSubcontentInfo) || !is_array($aSubcontentInfo))
+            return array();
+
+        $sSubentryUrl = BxDolService::call('bx_timeline', 'get_link', array((int)$aEvent['subobject_id']));
+        $sSubentrySample = $aSubcontentInfo['title'];
+        if(empty($sSubentrySample))
+            $sSubentrySample = strmaxtextlen($aSubcontentInfo['description'], 20, '...');
+
+		return array(
+			'entry_sample' => $CNF['T']['txt_sample_single'],
+			'entry_url' => $oGroupProfile->getUrl(),
+			'entry_caption' => $oGroupProfile->getDisplayName(),
+			'entry_author' => $oGroupProfile->id(),
+			'subentry_sample' => $sSubentrySample,
+			'subentry_url' => $sSubentryUrl,
+			'lang_key' => $CNF['T']['txt_ntfs_timeline_post_common'],
+		);
+    }
+
+    public function serviceGetConnectionButtonsTitles($iProfileId, $sConnectionsObject = 'sys_profiles_friends')
+    {
+        if (!isLogged())
+            return array();
+
+        if (!($oConn = BxDolConnection::getObjectInstance($sConnectionsObject)))
+            return array();
+
+        $CNF = $this->_oConfig->CNF;
+
+        if ($oConn->isConnectedNotMutual(bx_get_logged_profile_id(), $iProfileId)) {
+            return array(
+                'add' => _t($CNF['T']['menu_item_title_befriend_sent']),
+                'remove' => _t($CNF['T']['menu_item_title_unfriend_cancel_request']),
+            );
+        } elseif ($oConn->isConnectedNotMutual($iProfileId, bx_get_logged_profile_id())) {
+            return array(
+                'add' => _t($CNF['T']['menu_item_title_befriend_confirm']),
+                'remove' => _t($CNF['T']['menu_item_title_unfriend_reject_request']),
+            );
+        } elseif ($oConn->isConnected($iProfileId, bx_get_logged_profile_id(), true)) {
+            return array(
+                'add' => '',
+                'remove' => _t($CNF['T']['menu_item_title_unfriend']),
+            );
+        } else {
+            return array(
+                'add' => _t($CNF['T']['menu_item_title_befriend']),
+                'remove' => '',
+            );
+        }
+    }
+    
+    // ====== PERMISSION METHODS
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedAdd ($isPerformAction = false)
+    {
+        $oAccount = BxDolAccount::getInstance();
+        if (!$oAccount || ($this->serviceActAsProfile() && $oAccount->isProfilesLimitReached()))
+            return _t('_sys_txt_access_denied');
+        return parent::checkAllowedAdd ($isPerformAction);
+    }   
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden. So make sure to make strict(===) checking.
+     */
+    public function checkAllowedView ($aDataEntry, $isPerformAction = false)
+    {
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->getName());
+        if ($oProfile && $oProfile->id() == $this->_iProfileId)
+            return CHECK_ACTION_RESULT_ALLOWED;
+        return parent::checkAllowedView ($aDataEntry, $isPerformAction);
+    }
+    
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedViewProfileImage ($aDataEntry, $isPerformAction = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        // check privacy
+        if (empty($CNF['OBJECT_PRIVACY_VIEW'])) 
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        $oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW']);
+        if ($oPrivacy && !$oPrivacy->check($aDataEntry[$CNF['FIELD_ID']]) && !$oPrivacy->isPartiallyVisible($aDataEntry[$CNF['FIELD_ALLOW_VIEW_TO']]))
+            return _t('_sys_access_denied_to_private_content');
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedViewCoverImage ($aDataEntry, $isPerformAction = false)
+    {
+        return $this->checkAllowedViewProfileImage($aDataEntry);
+    }
+    
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedPost ($aDataEntry, $isPerformAction = false)
+    {
+        return $this->checkAllowedView ($aDataEntry, $isPerformAction);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedEdit ($aDataEntry, $isPerformAction = false)
+    {
+        // moderator always has access
+        if ($this->_isModerator($isPerformAction))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // owner (checked by account! not as profile as ususal) always have access
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->_aModule['name']);
+        if (!$oProfile)
+            return _t('_sys_txt_error_occured');
+
+        if ($oProfile->getAccountId() == $this->_iAccountId)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * Check if user can change cover image
+     */
+    public function checkAllowedChangeCover ($aDataEntry, $isPerformAction = false)
+    {
+        // moderator always has access
+        if ($this->_isModerator($isPerformAction))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // owner (checked by account! not as profile as ususal) always have access
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->_aModule['name']);
+        if (!$oProfile)
+            return _t('_sys_txt_error_occured');
+
+        if ($oProfile->getAccountId() == $this->_iAccountId)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedDelete (&$aDataEntry, $isPerformAction = false)
+    {
+        // moderator always has access
+        if ($this->_isModerator($isPerformAction))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // check ACL and owner (checked by account! not as profile as ususal)
+        $aCheck = checkActionModule($this->_iProfileId, 'delete entry', $this->getName(), $isPerformAction);
+
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->_aModule['name'], $isPerformAction);
+        if (!$oProfile)
+            return _t('_sys_txt_error_occured');
+
+        if ($oProfile->getAccountId() == $this->_iAccountId && $aCheck[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED)
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedViewMoreMenu (&$aDataEntry, $isPerformAction = false)
+    {
+        $oMenu = BxTemplMenu::getObjectInstance($this->_oConfig->CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY_MORE']);
+        if (!$oMenu || !$oMenu->getCode())
+            return _t('_sys_txt_access_denied');
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+	/**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedCompose (&$aDataEntry, $isPerformAction = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$CNF['FIELD_ID']], $this->getName());
+        if($oProfile && $oProfile->id() == $this->_iProfileId)
+            return _t('_sys_txt_access_denied');
+
+        return $this->checkAllowedView ($aDataEntry, $isPerformAction);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedFriendAdd (&$aDataEntry, $isPerformAction = false)
+    {
+        return $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, 'sys_profiles_friends', true, false);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedFriendRemove (&$aDataEntry, $isPerformAction = false)
+    {
+        if (CHECK_ACTION_RESULT_ALLOWED === $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, 'sys_profiles_friends', false, true, true))
+            return CHECK_ACTION_RESULT_ALLOWED;
+        return $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, 'sys_profiles_friends', false, true, false);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedSubscribeAdd (&$aDataEntry, $isPerformAction = false)
+    {
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->checkAllowedView($aDataEntry)))
+            return $sMsg;
+        return $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, 'sys_profiles_subscriptions', false, false);
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedSubscribeRemove (&$aDataEntry, $isPerformAction = false)
+    {
+        return $this->_checkAllowedConnect ($aDataEntry, $isPerformAction, 'sys_profiles_subscriptions', false, true);
+    }
+
+    public function checkAllowedSubscriptionsView (&$aDataEntry, $isPerformAction = false)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$CNF['FIELD_ID']], $this->_aModule['name']);
+        if(!$oProfile || ($oProfile->id() != $this->_iProfileId && $this->_oDb->getParam($CNF['PARAM_PUBLIC_SBSN']) != 'on' && $this->_oDb->getParam($CNF['PARAM_PUBLIC_SBSD']) != 'on'))
+            return _t('_sys_txt_access_denied');
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    public function checkMyself($iContentId)
+    {
+		$iLogged = (int)bx_get_logged_profile_id();
+    	if(empty($iLogged))
+    		return false;
+
+    	$oProfile = BxDolProfile::getInstanceByContentAndType((int)$iContentId, $this->_oConfig->getName());
+    	if(!$oProfile)
+    		return false;
+
+		return $oProfile->id() == $iLogged;
+    }
+
+    // ====== PROTECTED METHODS
+
+    protected function _checkAllowedConnect (&$aDataEntry, $isPerformAction, $sObjConnection, $isMutual, $isInvertResult, $isSwap = false)
+    {
+        if (!$this->_iProfileId)
+            return _t('_sys_txt_access_denied');
+
+        $CNF = &$this->_oConfig->CNF;
+
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$CNF['FIELD_ID']], $this->_aModule['name']);
+        if (!$oProfile || $oProfile->id() == $this->_iProfileId)
+            return _t('_sys_txt_access_denied');
+
+        // check ACL
+        $aCheck = checkActionModule($this->_iProfileId, 'connect', 'system', $isPerformAction);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+
+        $oConn = BxDolConnection::getObjectInstance($sObjConnection);
+        if ($isSwap)
+            $isConnected = $oConn->isConnected($oProfile->id(), $this->_iProfileId, $isMutual);
+        else
+            $isConnected = $oConn->isConnected($this->_iProfileId, $oProfile->id(), $isMutual);
+
+        if ($isInvertResult)
+            $isConnected = !$isConnected;
+
+        return $isConnected ? _t('_sys_txt_access_denied') : CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    protected function _buildRssParams($sMode, $aArgs)
+    {
+        $aParams = array ();
+        $sMode = bx_process_input($sMode);
+        switch ($sMode) {
+            case 'connections':
+                $aParams = array(
+                    'object' => isset($aArgs[0]) ? $aArgs[0] : '',
+                    'type' => isset($aArgs[1]) ? $aArgs[1] : '',
+                    'profile' => isset($aArgs[2]) ? (int)$aArgs[2] : 0,
+                    'mutual' => isset($aArgs[3]) ? (int)$aArgs[3] : 0,
+                    'profile2' => isset($aArgs[4]) ? (int)$aArgs[4] : 0,
+                );
+                break;
+        }
+
+        return $aParams;
+    }
+
+    protected function _serviceBrowseQuick($aProfiles, $iStart = 0, $iLimit = 4)
+    {
+        // get paginate object
+        $oPaginate = new BxTemplPaginate(array(
+            'on_change_page' => "return !loadDynamicBlockAutoPaginate(this, '{start}', '{per_page}');",
+            'num' => count($aProfiles),
+            'per_page' => $iLimit,
+            'start' => $iStart,
+        ));
+
+        // remove last item from connection array, because we've got one more item for pagination calculations only
+        if (count($aProfiles) > $iLimit)
+            array_pop($aProfiles);
+
+        // get profiles HTML
+        $s = '';
+        foreach ($aProfiles as $iProfileId) {
+            $oProfile = BxDolProfile::getInstance($iProfileId);
+            if(!$oProfile)
+                continue;
+
+            $s .= $oProfile->getUnit();
+        }
+
+        // return profiles + paginate
+        return $s . (!$iStart && $oPaginate->getNum() <= $iLimit ?  '' : $oPaginate->getSimplePaginate());
+    }
+
+    protected function _entityComments($sObject, $iId = 0)
+    {
+        if(!$iId)
+            $iId = bx_process_input(bx_get('id'), BX_DATA_INT);
+
+        if(!$iId)
+            $iId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+
+        if(!$iId)
+            return false;
+
+        $oCmts = BxDolCmts::getObjectInstance($sObject, $iId);
+        if(!$oCmts || !$oCmts->isEnabled())
+            return false;
+
+        return $oCmts->getCommentsBlock(array(), array('in_designbox' => false));
+    }
+
+    protected function _getContent($iContentId = 0, $sFuncGetContent = 'getContentInfoById')
+    {
+        if(!$iContentId && bx_get('id') === false && bx_get('profile_id') !== false) {
+            $oProfile = BxDolProfile::getInstance((int)bx_get('profile_id'));
+            if($oProfile)
+                $iContentId = $oProfile->getContentId();
+        }
+
+        return parent::_getContent($iContentId, $sFuncGetContent);
+    }
+}
+
+/** @} */

+ 107 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/profile/classes/BxBaseModProfileSearchResult.php

@@ -0,0 +1,107 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseProfile Base classes for profile modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModProfileSearchResult extends BxBaseModGeneralSearchResult
+{    
+    public function __construct($sMode = '', $aParams = array())
+    {
+        parent::__construct($sMode, $aParams);
+        
+        $this->aUnitViews = array('gallery' => 'unit_with_cover.html', 'showcase' => 'unit_with_cover_showcase.html');
+        if (!empty($aParams['unit_views']))
+            $this->aUnitViews = $aParams['unit_views'];
+        if (!empty($aParams['unit_view']))
+            $this->sUnitViewDefault = $aParams['unit_view'];
+        
+        $this->sUnitTemplate = $this->aUnitViews[$this->sUnitViewDefault];
+        $this->sCenterContentUnitSelector = '.bx-base-pofile-unit';
+        $this->addContainerClass (array('bx-def-margin-sec-lefttopright-neg', 'bx-base-pofile-units-wrapper', 'bx-def-margin-sec-bottom-neg'));
+		if ('unit_with_cover_showcase.html' == $this->sUnitTemplate){
+			$this->bShowcaseView = true;
+			$this->removeContainerClass ('bx-def-margin-bottom-neg');			
+        }
+    }
+
+    protected function _setConnectionsConditions ($aParams)
+    {
+        $oConnection = isset($aParams['object']) ? BxDolConnection::getObjectInstance($aParams['object']) : false;
+        if (!$oConnection || !isset($aParams['profile']) || !(int)$aParams['profile'])
+            return false;
+
+        $sContentType = isset($aParams['type']) ? $aParams['type'] : BX_CONNECTIONS_CONTENT_TYPE_CONTENT;
+        $isMutual = isset($aParams['mutual']) ? $aParams['mutual'] : false;
+        $a = $oConnection->getConnectionsAsCondition ($sContentType, 'id', (int)$aParams['profile'], (int)$aParams['profile2'], $isMutual);
+
+        $this->aCurrent['restriction'] = array_merge($this->aCurrent['restriction'], $a['restriction']);
+        $this->aCurrent['join'] = array_merge($this->aCurrent['join'], $a['join']);
+
+        return true;
+    }
+
+	protected function _setAclConditions ($aParams)
+    {
+        $oAcl =  BxDolAcl::getInstance();
+        if(!$oAcl || empty($aParams['level']))
+            return false;
+
+		if(!is_array($aParams['level']))
+			$aParams['level'] = array($aParams['level']);
+
+		$this->aCurrent['title'] = array();
+		foreach($aParams['level'] as $iLevelId) {
+			$aInfo = $oAcl->getMembershipInfo($iLevelId);
+			if(empty($aInfo) || !is_array($aInfo))
+				continue;
+
+			$this->aCurrent['title'][] = _t($aInfo['name']);
+		}
+
+        $aCondition = $oAcl->getContentByLevelAsCondition('id', $aParams['level']);        
+        $this->aCurrent['restriction_sql'] = (!empty($this->aCurrent['restriction_sql']) ? $this->aCurrent['restriction_sql'] : '') . $aCondition['restriction_sql'];
+        $this->aCurrent['restriction'] = array_merge($this->aCurrent['restriction'], $aCondition['restriction']);
+        $this->aCurrent['join'] = array_merge($this->aCurrent['join'], $aCondition['join']);
+
+        return true;
+    }
+
+    protected function _setFavoriteConditions($sMode, $aParams, &$oProfileAuthor)
+    {
+        $CNF = &$this->oModule->_oConfig->CNF;
+
+        $oProfileAuthor = BxDolProfile::getInstance((int)$aParams['user']);
+        if(!$oProfileAuthor) 
+            return false;
+
+        $iProfileAuthor = $oProfileAuthor->id();
+        $oFavorite = $this->oModule->getObjectFavorite();
+        if(!$oFavorite->isPublic() && $iProfileAuthor != bx_get_logged_profile_id()) 
+            return false;
+
+        $aConditions = $oFavorite->getConditionsTrack($CNF['TABLE_ENTRIES'], 'id', $iProfileAuthor);
+        if(!empty($aConditions) && is_array($aConditions)) {
+            if(empty($this->aCurrent['restriction']) || !is_array($this->aCurrent['restriction']))
+                $this->aCurrent['restriction'] = array();
+            $this->aCurrent['restriction'] = array_merge($this->aCurrent['restriction'], $aConditions['restriction']);
+
+            if(empty($this->aCurrent['join']) || !is_array($this->aCurrent['join']))
+                $this->aCurrent['join'] = array();
+            $this->aCurrent['join'] = array_merge($this->aCurrent['join'], $aConditions['join']);
+        }
+
+        $this->sBrowseUrl = 'page.php?i=' . $CNF['URI_VIEW_FAVORITES'] . '&profile_id={profile_id}';
+        $this->aCurrent['title'] = _t($CNF['T']['txt_browse_favorites']);
+        $this->aCurrent['rss']['link'] = 'modules/?r=' . $this->oModule->_oConfig->getUri() . '/rss/' . $sMode . '/' . $iProfileAuthor;
+
+        return true;
+    }
+}
+
+/** @} */

+ 329 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/profile/classes/BxBaseModProfileTemplate.php

@@ -0,0 +1,329 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseProfile Base classes for profile modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/*
+ * Profile based modules representation.
+ */
+class BxBaseModProfileTemplate extends BxBaseModGeneralTemplate
+{
+    protected $_sUnitClass;
+    protected $_sUnitClassWithCover;
+    protected $_sUnitClassWoInfo;
+    protected $_sUnitClassShowCase;
+    
+    function __construct(&$oConfig, &$oDb)
+    {
+        parent::__construct($oConfig, $oDb);
+
+        $this->_sUnitClass = 'bx-base-pofile-unit';
+        $this->_sUnitClassWithCover = 'bx-base-pofile-unit-with-cover';
+        $this->_sUnitClassWoInfo = 'bx-base-pofile-unit-wo-info';
+        $this->_sUnitClassShowCase = 'bx-base-pofile-unit-with-cover bx-base-unit-showcase bx-base-pofile-unit-showcase';
+    }
+
+    /**
+     * Get profile unit
+     */
+    function unit ($aData, $isCheckPrivateContent = true, $sTemplateName = 'unit.html')
+    {
+        $aVars = $this->unitVars ($aData, $isCheckPrivateContent, $sTemplateName);
+
+        return $this->parseHtmlByName($sTemplateName, $aVars);
+    }
+
+    function unitVars ($aData, $isCheckPrivateContent = true, $sTemplateName = 'unit.html')
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $oModule = $this->getModule();
+        $iContentId = (int)$aData[$CNF['FIELD_ID']];
+
+        $bPublic = true;
+        if($isCheckPrivateContent && !empty($CNF['OBJECT_PRIVACY_VIEW'])) {
+            $oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW']);
+            if ($oPrivacy && !$oPrivacy->check($iContentId) && !$oPrivacy->isPartiallyVisible($aData[$CNF['FIELD_ALLOW_VIEW_TO']]))
+                $bPublic = false;
+        }
+
+        $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 title
+        $sTitle = bx_process_output($aData[$CNF['FIELD_NAME']]);
+
+        // get profile's url
+        $sUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $iContentId);
+
+        $sThumbUrl = $bPublicThumb ? $this->thumb($aData, false) : '';
+        $bThumbUrl = !empty($sThumbUrl);
+
+        $sCoverUrl = $bPublicCover ? $this->urlCoverUnit($aData, false) : '';
+        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.jpg');
+
+        $aTmplVarsMeta = array();
+        if(!empty($CNF['OBJECT_MENU_SNIPPET_META'])) {
+            $oMenuMeta = BxDolMenu::getObjectInstance($CNF['OBJECT_MENU_SNIPPET_META'], $this);
+            if($oMenuMeta) {
+                $oMenuMeta->setContentId($iContentId);
+                $oMenuMeta->setContentPublic($bPublic);
+                $aTmplVarsMeta = array(
+                    'meta' => $oMenuMeta->getCode()
+                );
+            }
+        }
+
+        // generate html
+        return array (
+        	'class' => $this->_getUnitClass($aData, $sTemplateName),
+            'id' => $iContentId,
+        	'public' => $bPublic,
+            'bx_if:show_thumb_image' => array(
+                'condition' => $bThumbUrl,
+                'content' => array(
+                    'thumb_url' => $sThumbUrl
+                )
+            ),
+            'bx_if:show_thumb_letter' => array(
+                'condition' => !$bThumbUrl,
+                'content' => array(
+                    'color' => implode(', ', BxDolTemplate::getColorCode($iProfile, 0.5)),
+                    'letter' => mb_strtoupper(mb_substr($sTitle, 0, 1))
+                )
+            ),
+            'thumb_url' => $bThumbUrl ? $sThumbUrl : $this->getImageUrl('no-picture-thumb.png'),
+            'cover_url' => $sCoverUrl,
+            '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,
+            'module_name' => _t($CNF['T']['txt_sample_single']),
+            'ts' => $aData[$CNF['FIELD_ADDED']],
+            'bx_if:meta' => array(
+                'condition' => !empty($aTmplVarsMeta),
+                'content' => $aTmplVarsMeta
+            )
+        );
+    }
+
+    /**
+     * Get profile cover
+     */
+    function setCover ($oPage, $aData, $sTemplateName = 'cover.html')
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if (CHECK_ACTION_RESULT_ALLOWED !== $this->getModule()->checkAllowedViewProfileImage($aData)) {
+            $CNF = &$this->_oConfig->CNF;
+            $aData[$CNF['FIELD_PICTURE']] = 0;
+        }
+
+        if (CHECK_ACTION_RESULT_ALLOWED !== $this->getModule()->checkAllowedViewCoverImage($aData)) {
+            $CNF = &$this->_oConfig->CNF;
+            $aData[$CNF['FIELD_COVER']] = 0;
+        }
+
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aData[$CNF['FIELD_ID']], $this->MODULE);
+        $iProfile = $oProfile->id();
+
+        $sUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aData[$CNF['FIELD_ID']]);
+        $sTitle = bx_process_output($aData[$CNF['FIELD_NAME']]);
+
+        $sUrlPicture = $this->urlPicture ($aData);
+        $sUrlPictureChange = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_EDIT_ENTRY'] . '&id=' . $aData[$CNF['FIELD_ID']]);
+
+        $sUrlAvatar = $this->urlAvatar($aData, false);
+        $bUrlAvatar = !empty($sUrlAvatar);
+
+        $sUrlCover = $this->urlCover ($aData, false);
+        if(!$sUrlCover && $oPage !== false && $oPage->isPageCover()) {
+            $aCover = $oPage->getPageCoverImage();
+			if(!empty($aCover))
+				$sUrlCover = BxDolCover::getCoverImageUrl($aCover);
+        }
+
+        if(!$sUrlCover)
+            $sUrlCover = $this->getImageUrl('cover.jpg');
+        
+        $sUrlCoverChange = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_EDIT_COVER'] . '&id=' . $aData[$CNF['FIELD_ID']]);
+
+        $sCoverPopup = '';
+        $sCoverPopupId = $this->MODULE . '-popup-cover';
+        if ($aData[$CNF['FIELD_COVER']]) {
+            $sCoverPopup = BxTemplFunctions::getInstance()->transBox($sCoverPopupId, $this->parseHtmlByName('image_popup.html', array (
+                'image_url' => $sUrlCover,
+                'bx_if:owner' => array (
+                    'condition' => CHECK_ACTION_RESULT_ALLOWED === $this->getModule()->checkAllowedChangeCover($aData),
+                    'content' => array (
+                        'change_image_url' => $sUrlCoverChange,
+                    ),
+                ),
+            )), true, true);
+        }
+
+        $sPicturePopup = '';
+        $sPicturePopupId = $this->MODULE . '-popup-picture';
+        if ($aData[$CNF['FIELD_PICTURE']]) {
+            $sPicturePopup = BxTemplFunctions::getInstance()->transBox($sPicturePopupId, $this->parseHtmlByName('image_popup.html', array (
+                'image_url' => $sUrlPicture,
+                'bx_if:owner' => array (
+                    'condition' => CHECK_ACTION_RESULT_ALLOWED === $this->getModule()->checkAllowedEdit($aData),
+                    'content' => array (
+                        'change_image_url' => $sUrlPictureChange,
+                    ),
+                ),
+            )), true, true);
+        }
+
+        $oMenu = BxTemplMenu::getObjectInstance($this->_oConfig->CNF['OBJECT_MENU_ACTIONS_VIEW_ENTRY']);
+
+        // generate html
+        $aVars = array (
+            'id' => $aData[$CNF['FIELD_ID']],
+            'content_url' => $sUrl,
+            'title' => $sTitle,
+            //'menu' => BxDolMenu::getObjectInstance($CNF['OBJECT_MENU_SUBMENU_VIEW_ENTRY_COVER'])->getCode(), // TODO: check if menu is used somewhere
+
+            'action_menu' => $oMenu ? $oMenu->getCode() : '',
+
+        	'bx_if:show_ava_image' => array(
+                'condition' => $bUrlAvatar,
+                'content' => array(
+                    'ava_url' => $sUrlAvatar
+                )
+            ),
+            'bx_if:show_ava_letter' => array(
+                'condition' => !$bUrlAvatar,
+                'content' => array(
+            		'color' => implode(', ', BxDolTemplate::getColorCode($iProfile, 0.5)),
+                    'letter' => mb_substr($sTitle, 0, 1)
+                )
+            ),
+            'picture_avatar_url' => $bUrlAvatar ? $sUrlAvatar : $this->getImageUrl('no-picture-preview.png'),
+            'picture_popup' => $sPicturePopup,
+            'picture_popup_id' => $sPicturePopupId,
+            'picture_url' => $sUrlPicture,
+            'picture_href' => !$aData[$CNF['FIELD_PICTURE']] && CHECK_ACTION_RESULT_ALLOWED === $this->getModule()->checkAllowedEdit($aData) ? $sUrlPictureChange : 'javascript:void(0);',
+
+            'cover_popup' => $sCoverPopup,
+            'cover_popup_id' => $sCoverPopupId,
+            'cover_url' => $sUrlCover,
+            'cover_href' => !$aData[$CNF['FIELD_COVER']] && CHECK_ACTION_RESULT_ALLOWED === $this->getModule()->checkAllowedChangeCover($aData) ? $sUrlCoverChange : 'javascript:void(0);',
+        );
+
+        BxDolCover::getInstance($this)->set($aVars, $sTemplateName);
+    }
+
+    /**
+     * Get profile picture thumb url
+     */
+    function thumb ($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 picture icon url
+     */
+    function icon ($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 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 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 = true)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        return $this->_image ($CNF['FIELD_COVER'], $CNF['OBJECT_IMAGES_TRANSCODER_COVER'], 'cover.jpg', $aData, $bSubstituteNoImage);
+    }
+
+	/**
+     * Get profile cover image url for browse unit
+     */
+    function urlCoverUnit ($aData, $bSubstituteNoImage = true)
+    {
+        $CNF = &$this->_oConfig->CNF;
+        return $this->_image ($CNF['FIELD_COVER'], $CNF['OBJECT_IMAGES_TRANSCODER_GALLERY'], 'cover.jpg', $aData, $bSubstituteNoImage);
+    }
+
+    /**
+     * 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]);
+        }
+        return $bSubstituteNoImage && !$sImageUrl ? $this->getImageUrl($sNoImage) : $sImageUrl;
+    }
+
+    protected function _getUnitClass($aData, $sTemplateName = 'unit.html')
+    {
+        $sResult = '';
+
+        switch($sTemplateName) {
+            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;
+        }
+
+        return $sResult;
+    }
+}
+
+/** @} */

+ 293 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/profile/template/css/main.css

@@ -0,0 +1,293 @@
+
+@import url(../../../general/template/css/main.css);
+
+/* browse unit */
+
+.bx-base-pofile-unit {
+    width:184px;
+    width:11.5rem;
+    float:left;
+}
+
+.bx-base-pofile-unit-wo-info {
+	position: relative;
+	display: block;
+}
+
+.bx-search-result-block {
+    text-align:center;
+}
+
+/* unit meta */
+.bx-base-general-unit-meta ul.bx-menu-custom li .bx-base-profile-unit-meta-item {
+	height: 1.625rem;
+	padding-left: 0.5rem;
+
+	line-height: 1.625rem;	
+}
+
+.bx-base-general-unit-meta ul.bx-menu-custom li:first-child .bx-base-profile-unit-meta-item {
+    padding-left: 0px;
+}
+
+/* browse unit with cover */
+
+.bx-base-pofile-unit-cover {
+    display:block;
+
+    height:7.5rem;
+
+    line-height:7.5rem;
+    text-align:center;
+
+    background-repeat:no-repeat;
+    background-position:center center;
+    -webkit-background-size:cover;
+    -moz-background-size:cover;
+    -o-background-size:cover;
+    background-size:cover;
+}
+
+img.bx-base-pofile-unit-thumb {
+	display: block;
+
+    background-color: #fff;
+}
+
+p.bx-base-pofile-unit-thumb,
+p.bx-base-pofile-unit-ava {
+	display: block;
+    margin: 0px;
+
+	line-height: 46px;
+    line-height: 2.9rem;
+
+	font-size: 20px;
+	font-size: 1.2rem;
+	font-weight: 700;
+	color: #d0d0d0;
+}
+p.bx-base-pofile-unit-ava {
+	line-height: 94px;
+    line-height: 5.9rem;
+
+	font-size: 48px;
+    font-size: 3.0rem;
+    font-weight: 700;
+}
+	
+.bx-base-pofile-unit-info,
+.bx-base-pofile-unit-title {
+    min-width:17.5rem;
+}
+
+.bx-base-pofile-unit-with-cover {
+    display:inline-block;
+    overflow:hidden;
+}
+
+.bx-base-pofile-unit-with-cover .bx-base-pofile-unit-thumb,
+.bx-base-pofile-unit-with-cover .bx-base-pofile-unit-ava {
+	display: inline-block;
+	vertical-align: middle;
+}
+
+.bx-layout-column-thin .bx-base-pofile-unit-with-cover {
+    margin-left:0;
+    margin-right:0;
+}
+
+.bx-base-pofile-unit-title {
+    text-align:center;
+    white-space:nowrap;
+    text-overflow:ellipsis;
+    overflow:hidden;
+}
+
+.bx-base-pofile-unit-info-members {
+    float:left;
+    line-height:1.5rem;
+    vertical-align:middle;
+}
+
+.bx-base-pofile-unit-info-btn {
+    float:right;
+}
+
+/* profile cover */
+
+.bx-base-pofile-menu {
+    text-align:center;
+}
+
+.bx-base-pofile-cover {
+    position:relative;
+    min-height:10rem;
+}
+
+a.bx-base-pofile-cover-image:hover,
+.bx-base-pofile-cover-image {
+    position:absolute;
+    width:100%;
+    height:100%;
+    text-decoration:none;
+
+    background-color: #eee;
+    background-repeat: no-repeat;
+    background-position: center center;
+    -webkit-background-size: cover;
+    -moz-background-size: cover;
+    -o-background-size: cover;
+    background-size: cover;
+}
+
+.bx-base-pofile-cover-info-container {
+    position:relative;
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+    min-height: 40vh;
+
+    cursor:pointer; 
+    cursor:hand;
+}
+
+.bx-base-pofile-cover-info {
+	width: 100%;
+    text-align:center;
+}
+.bx-base-pofile-cover-info .bx-def-ava,
+.bx-base-pofile-cover-info h1,
+.bx-base-pofile-cover-info .bx-base-pofile-cover-actions {
+    display:block;
+    text-align:center;
+}
+.bx-base-pofile-cover-info .bx-def-ava,
+.bx-base-pofile-cover-info .bx-base-pofile-cover-actions {
+    display:inline-block;
+}
+
+.bx-base-pofile-cover-actions ul li:last-child a {
+    margin-right:0;
+}
+
+.bx-base-pofile-cover-info img,
+.bx-base-pofile-cover-info .bx-base-pofile-unit-ava {
+    border:2px solid #fff;
+    border:0.15rem solid #fff;
+}
+
+.bx-base-pofile-cover-info h1.bx-def-unit-info {
+    padding:0;
+    text-shadow: 0px 0px 0.125rem #000;
+    line-height:normal;
+}
+
+.bx-base-pofile-popup-wrapper {
+    position:relative;
+}
+
+.bx-base-pofile-popup-btn {
+    position:absolute;
+    top:0;
+    right:0;
+}
+
+
+/* flexbox */
+.bx-base-pofile-units-wrapper {
+    display: -webkit-box;
+    display: -webkit-flex;
+    display: -ms-flexbox;
+    display: flex;
+
+    /* makes all items on a row equal height */
+    -webkit-box-align: stretch;
+    -webkit-align-items: stretch;
+    -ms-flex-align: stretch;
+    align-items: stretch;
+
+    /* wrap onto the next line rather than stretching or shrinking */
+    -webkit-flex-wrap: wrap;
+    -ms-flex-wrap: wrap;
+    flex-wrap: wrap;
+
+    /* tells flexbox to start items from the left */
+    -webkit-box-pack: start;
+    -ms-flex-pack: start;
+    plex-pack: start;
+    -webkit-justify-content: flex-start;
+    justify-content: flex-start;
+}
+
+.bx-base-pofile-units-wrapper .bx-base-pofile-unit-with-cover {
+    -webkit-box-flex: 0;
+
+    -webkit-flex: 0 0 100%;
+    -ms-flex: 0 0 100%;
+    flex: 0 0 100%;
+
+    overflow:hidden;
+}
+
+/* showcase view */
+.bx-base-unit-showcase-wrapper {
+    display:block
+}
+
+@media (min-width:533px) and (max-width:720px) {
+        .bx-layout-column-half .bx-base-pofile-units-wrapper .bx-base-pofile-unit-with-cover,
+        .bx-layout-column-thin .bx-base-pofile-units-wrapper .bx-base-pofile-unit-with-cover {
+        -webkit-flex: 0 0 49.8%;
+        -ms-flex: 0 0 49.8%;
+        flex: 0 0 49.8%;
+    }
+	
+	.bx-base-unit-showcase-wrapper .bx-base-pofile-unit-showcase
+	{
+		width:calc(50%);
+	}
+}
+
+@media (min-width:533px) and (max-width:1024px) {
+        .bx-layout-column-wide .bx-base-pofile-units-wrapper .bx-base-pofile-unit-with-cover,
+        #sys_search_results .bx-base-pofile-units-wrapper .bx-base-pofile-unit-with-cover,
+        .bx-layout-column-fullwidth .bx-base-pofile-units-wrapper .bx-base-pofile-unit-with-cover {
+        -webkit-flex: 0 0 49.8%;
+        -ms-flex: 0 0 49.8%;
+        flex: 0 0 49.8%;
+    }
+	
+	.bx-base-unit-showcase-wrapper .bx-base-pofile-unit-showcase
+	{
+		width:calc(50%);
+	}
+}
+
+@media (min-width:1024px) {
+    .bx-layout-column-wide .bx-base-pofile-units-wrapper .bx-base-pofile-unit-with-cover {
+        -webkit-flex: 0 0 49.8%;
+        -ms-flex: 0 0 49.8%;
+        flex: 0 0 49.8%;
+    }
+    #sys_search_results .bx-base-pofile-units-wrapper .bx-base-pofile-unit-with-cover,
+    .bx-layout-column-fullwidth .bx-base-pofile-units-wrapper .bx-base-pofile-unit-with-cover {
+        -webkit-flex: 0 0 33.25%;
+        -ms-flex: 0 0 33.25%;
+        flex: 0 0 33.25%;
+    }
+	
+	.bx-base-unit-showcase-wrapper .bx-base-pofile-unit-showcase
+	{
+		width:calc(33.3%);
+	}
+}
+
+
+/* view page */
+.bx-base-profile-entity-content,
+.bx-base-profile-entity-content p {
+    overflow: hidden;
+    text-overflow: ellipsis;
+}

+ 1 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/profile/template/unit_with_cover_showcase.html

@@ -0,0 +1 @@
+<bx_include_auto:unit_with_cover.html />

+ 28 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/text/classes/BxBaseModTextFormEntry.php

@@ -0,0 +1,28 @@
+<?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
+ *
+ * @{
+ */
+
+/**
+ * Create/Edit entry form
+ */
+class BxBaseModTextFormEntry extends BxBaseModGeneralFormEntry
+{
+    public function __construct($aInfo, $oTemplate = false)
+    {
+        parent::__construct($aInfo, $oTemplate);
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (isset($this->aInputs[$CNF['FIELD_ALLOW_VIEW_TO']]))
+            $this->aInputs[$CNF['FIELD_ALLOW_VIEW_TO']] = array_merge($this->aInputs[$CNF['FIELD_ALLOW_VIEW_TO']], BxDolPrivacy::getGroupChooser($CNF['OBJECT_PRIVACY_VIEW']));
+    }
+}
+
+/** @} */

+ 88 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/text/classes/BxBaseModTextPageEntry.php

@@ -0,0 +1,88 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseText Base classes for text modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/**
+ * Entry create/edit pages
+ */
+class BxBaseModTextPageEntry extends BxBaseModGeneralPageEntry
+{
+    public function __construct($aObject, $oTemplate = false)
+    {
+        parent::__construct($aObject, $oTemplate);
+
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if ($iContentId)
+            $this->_aContentInfo = $this->_oModule->_oDb->getContentInfoById($iContentId);
+
+        $sTitle = $sUrl = $sIcon = "";
+        if ($this->_aContentInfo && CHECK_ACTION_RESULT_ALLOWED === $this->_oModule->checkAllowedView($this->_aContentInfo)) {
+            $sTitle = isset($this->_aContentInfo[$CNF['FIELD_TITLE']]) ? $this->_aContentInfo[$CNF['FIELD_TITLE']] : strmaxtextlen($this->_aContentInfo[$CNF['FIELD_TEXT']], 20, '...');
+            $sUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $this->_aContentInfo[$CNF['FIELD_ID']]);
+            $sIcon = $CNF['ICON'];
+
+            $this->addMarkers($this->_aContentInfo); // every field can be used as marker
+        }
+
+        $this->addMarkers(array(
+            'title' => $sTitle,
+            'entry_link' => $sUrl,
+        ));
+
+        // select view entry submenu
+        $this->_setSubmenu(array(
+        	'title' => $sTitle,
+			'link' => $sUrl,
+            'icon' => $sIcon
+        ));
+    }
+
+    public function isActive()
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if($this->_aContentInfo[$CNF['FIELD_AUTHOR']] == bx_get_logged_profile_id() || $this->_oModule->_isModerator())
+            return true;
+
+        if(isset($CNF['FIELD_STATUS']) && $this->_aContentInfo[$CNF['FIELD_STATUS']] != 'active')
+            return false;
+
+        if(isset($CNF['FIELD_STATUS_ADMIN']) && $this->_aContentInfo[$CNF['FIELD_STATUS_ADMIN']] != 'active')
+            return false;
+
+        return true;
+    }
+
+    protected function _setSubmenu($aParams)
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+		$oMenuSubmenu = BxDolMenu::getObjectInstance('sys_site_submenu');
+		$oMenuSubmenu->setObjectSubmenu($CNF['OBJECT_MENU_SUBMENU_VIEW_ENTRY'], array_merge(array(
+			'title' => '',
+			'link' => '',
+			'icon' => '',
+		), $aParams));
+    }
+
+    protected function _getBlockService ($aBlock)
+    {
+        $a = parent::_getBlockService ($aBlock);
+        $sTest = '_view_entry_comments';
+        if (false !== strpos($aBlock['content'], 'entity_comments') && substr_compare($this->_sObject, $sTest, strlen($this->_sObject) - strlen($sTest), strlen($sTest)) === 0)
+            unset($a['title']);            
+        return $a;
+    }    
+
+}
+
+/** @} */

+ 134 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/text/classes/BxBaseModTextSearchResult.php

@@ -0,0 +1,134 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseText Base classes for text modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModTextSearchResult extends BxBaseModGeneralSearchResult
+{
+    protected $sUnitViewParamName = 'unit_view';
+
+    function __construct($sMode = '', $aParams = array())
+    {
+        parent::__construct($sMode, $aParams);
+        $this->aUnitViews = array('extended' => 'unit.html', 'gallery' => 'unit_gallery.html', 'full' => 'unit_full.html', 'showcase' => 'unit_showcase.html');
+		if (!empty($aParams['unit_views']))
+            $this->aUnitViews = $aParams['unit_views'];
+        if (!empty($aParams['unit_view']))
+            $this->sUnitViewDefault = $aParams['unit_view'];
+        $this->aGetParams = array($this->sUnitViewParamName);
+        $this->sUnitTemplate = $this->aUnitViews[$this->sUnitViewDefault];
+        if (isset($this->aUnitViews[bx_get($this->sUnitViewParamName)]))
+            $this->sUnitTemplate = $this->aUnitViews[bx_get($this->sUnitViewParamName)];
+		$this->addContainerClass ('bx-def-margin-bottom-neg');
+        if ('unit_gallery.html' == $this->sUnitTemplate){
+			$this->removeContainerClass ('bx-def-margin-bottom-neg');		
+            $this->addContainerClass (array('bx-def-margin-sec-lefttopright-neg', 'bx-base-text-unit-gallery-wrapper bx-def-margin-sec-bottom-neg'));
+		}
+		if ('unit_showcase.html' == $this->sUnitTemplate){
+			$this->bShowcaseView = true;
+		}
+    }
+
+    protected function processReplaceableMarkers($oProfileAuthor) 
+    {
+        if ($oProfileAuthor) {
+            $this->addMarkers($oProfileAuthor->getInfo()); // profile info is replaceable
+            $this->addMarkers(array('profile_id' => $oProfileAuthor->id())); // profile id is replaceable
+            $this->addMarkers(array('display_name' => $oProfileAuthor->getDisplayName())); // profile display name is replaceable
+        }
+
+        $this->sBrowseUrl = $this->_replaceMarkers($this->sBrowseUrl);
+        $this->aCurrent['title'] = $this->_replaceMarkers($this->aCurrent['title']);
+    }
+
+    protected function getCurrentOnclick($aAdditionalParams = array(), $bReplacePagesParams = true) 
+    {
+        // always add UnitView as additional param
+        $sUnitView = bx_process_input(bx_get($this->sUnitViewParamName));
+        if ($sUnitView && isset($this->aUnitViews[$sUnitView]))
+            $aAdditionalParams = array_merge(array($this->sUnitViewParamName => $sUnitView), $aAdditionalParams);
+
+        return parent::getCurrentOnclick($aAdditionalParams, $bReplacePagesParams);
+    }
+
+    protected function _updateCurrentForAuthor($sMode, $aParams, &$oProfileAuthor)
+    {
+        $CNF = &$this->oModule->_oConfig->CNF;
+
+        $oProfileAuthor = BxDolProfile::getInstance((int)$aParams['author']);
+        if (!$oProfileAuthor) 
+            return false;
+
+        $iProfileAuthor = $oProfileAuthor->id();
+        $this->aCurrent['restriction']['author']['value'] = $iProfileAuthor;
+
+        if(!empty($aParams['except']))
+        	$this->aCurrent['restriction']['except']['value'] = is_array($aParams['except']) ? $aParams['except'] : array($aParams['except']); 
+
+        if(!empty($aParams['per_page']))
+        	$this->aCurrent['paginate']['perPage'] = is_numeric($aParams['per_page']) ? (int)$aParams['per_page'] : (int)getParam($aParams['per_page']);
+
+        $this->sBrowseUrl = 'page.php?i=' . $CNF['URI_AUTHOR_ENTRIES'] . '&profile_id={profile_id}';
+        $this->aCurrent['title'] = _t($CNF['T']['txt_all_entries_by_author']);
+        $this->aCurrent['rss']['link'] = 'modules/?r=' . $this->oModule->_oConfig->getUri() . '/rss/' . $sMode . '/' . $iProfileAuthor;
+
+        return true;
+    }
+
+    protected function _updateCurrentForFavorite($sMode, $aParams, &$oProfileAuthor)
+    {
+        $CNF = &$this->oModule->_oConfig->CNF;
+        
+        $sSystem = '';
+        if(!empty($aParams['system'])) {
+            $sSystem = $aParams['system'];
+            unset($aParams['system']);
+        }
+
+        $oProfileAuthor = BxDolProfile::getInstance((int)$aParams['user']);
+        if(!$oProfileAuthor) 
+            return false;
+
+        $iProfileAuthor = $oProfileAuthor->id();
+        $oFavorite = $this->oModule->getObjectFavorite($sSystem);
+        if(!$oFavorite->isPublic() && $iProfileAuthor != bx_get_logged_profile_id()) 
+            return false;
+
+        $aConditions = $oFavorite->getConditionsTrack($this->aCurrent['table'], 'id', $iProfileAuthor);
+        if(!empty($aConditions) && is_array($aConditions)) {
+            if(empty($this->aCurrent['restriction']) || !is_array($this->aCurrent['restriction']))
+                $this->aCurrent['restriction'] = array();
+            $this->aCurrent['restriction'] = array_merge($this->aCurrent['restriction'], $aConditions['restriction']);
+
+            if(empty($this->aCurrent['join']) || !is_array($this->aCurrent['join']))
+                $this->aCurrent['join'] = array();
+            $this->aCurrent['join'] = array_merge($this->aCurrent['join'], $aConditions['join']);
+        }
+
+        $this->sBrowseUrl = 'page.php?i=' . $CNF['URI_AUTHOR_ENTRIES'] . '&profile_id={profile_id}';
+        $this->aCurrent['title'] = _t($CNF['T']['txt_all_entries_by_author']);
+        $this->aCurrent['rss']['link'] = 'modules/?r=' . $this->oModule->_oConfig->getUri() . '/rss/' . $sMode . '/' . $iProfileAuthor;
+
+        return true;
+    }
+
+    function _getPseud ()
+    {
+        return array(
+            'id' => 'id',
+            'title' => 'title',
+            'text' => 'text',
+            'added' => 'added',
+            'author' => 'author',
+            'photo' => 'photo',
+        );
+    }
+}
+
+/** @} */

+ 246 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/text/classes/BxBaseModTextTemplate.php

@@ -0,0 +1,246 @@
+<?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';
+    }
+
+    function unit ($aData, $isCheckPrivateContent = true, $sTemplateName = 'unit.html', $aParams = array())
+    {
+    	$sResult = $this->checkPrivacy ($aData, $isCheckPrivateContent, $this->getModule(), $sTemplateName);
+    	if($sResult)
+            return $sResult;
+        $aParams['template_name'] = $sTemplateName;
+		return $this->parseHtmlByName($sTemplateName, $this->getUnit($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::getInstance($iProfileId);
+        if (!$oProfile) 
+            $oProfile = BxDolProfileUndefined::getInstance();
+
+        if (!$oProfile)
+            return '';
+
+        $sName = $oProfile->getDisplayName();
+        $sAddon = $sFuncAuthorAddon ? $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) : '',
+            'bx_if:addon' => array (
+                'condition' => (bool)$sAddon,
+                'content' => array (
+                    'content' => $sAddon,
+                ),
+            ),
+        );
+        return $this->parseHtmlByName($sTemplateName, $aVars);
+    }
+
+    function getAuthorDesc ($aData)
+    {
+        return '';
+    }
+
+    function getAuthorAddon ($aData, $oProfile)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+        $sUrl = 'page.php?i=' . $CNF['URI_AUTHOR_ENTRIES'] . '&profile_id=' . $oProfile->id();
+        $sUrl = BxDolPermalinks::getInstance()->permalink($sUrl);
+        return _t($CNF['T']['txt_all_entries_by'], $sUrl, $oProfile->getDisplayName(), $this->getModule()->_oDb->getEntriesNumByAuthor($oProfile->id()));
+    }
+
+    public function entryBreadcrumb($aContentInfo, $aTmplVarsItems = array())
+    {
+    	$CNF = &BxDolModule::getInstance($this->MODULE)->_oConfig->CNF;
+
+    	return $this->parseHtmlByName('breadcrumb.html', array(
+    		'url_home' => BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink($CNF['URL_HOME']),
+    		'icon_home' => $CNF['ICON'],
+    		'bx_repeat:items' => $aTmplVarsItems
+    	));
+    }
+
+    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 = &BxDolModule::getInstance($this->MODULE)->_oConfig->CNF;
+
+        // get thumb url
+        list($sPhotoThumb, $sPhotoGallery) = $this->getUnitThumbAndGallery($aData);
+
+        // get entry url
+        $sUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aData[$CNF['FIELD_ID']]);
+
+        $oProfile = BxDolProfile::getInstance($aData[$CNF['FIELD_AUTHOR']]);
+        if (!$oProfile) 
+            $oProfile = BxDolProfileUndefined::getInstance();
+
+        $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);
+        }
+
+        $sTitle = bx_process_output($sTitle);
+
+        $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()
+                );
+            }
+        }
+
+        // generate html
+        return array (
+            'class' => $this->_getUnitClass($aData,(isset($aParams['template_name']) ? $aParams['template_name'] : '')),
+            'id' => $aData[$CNF['FIELD_ID']],
+            'content_url' => $sUrl,
+            'title' => $sTitle,
+            '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 : '',
+                    'strecher' => str_repeat('&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ', 40),
+                ),
+            ),
+            'bx_if:no_thumb' => array (
+                'condition' => !$sPhotoThumb,
+                'content' => array (
+                    'content_url' => $sUrl,
+                    'summary_plain' => $sSummaryPlain,
+                    'strecher' => mb_strlen($sSummaryPlain) > 240 ? '' : str_repeat('&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ', round((240 - mb_strlen($sSummaryPlain)) / 6)),
+                ),
+            ),
+        );
+    }
+
+    protected function getTitle($aData)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        return isset($aData[$CNF['FIELD_TITLE']]) ? $aData[$CNF['FIELD_TITLE']] : '';
+    }
+
+    protected function getText($aData)
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+
+        return isset($aData[$CNF['FIELD_TEXT']]) ? $aData[$CNF['FIELD_TEXT']] : '';
+    }
+
+    protected function getSummary($aData, $sTitle = '', $sText = '', $sUrl = '')
+    {
+        $CNF = &$this->getModule()->_oConfig->CNF;
+        if(empty($CNF['PARAM_CHARS_SUMMARY']))
+            return '';
+
+        // get summary
+        $sLinkMore = ' <a title="' . bx_html_attribute(_t('_sys_read_more', $sTitle)) . '" href="' . $sUrl . '"><i class="sys-icon ellipsis-h"></i></a>';
+        return  strmaxtextlen($sText, (int)getParam($CNF['PARAM_CHARS_SUMMARY']), $sLinkMore);
+    }
+    
+    protected function _getUnitClass($aData, $sTemplateName = 'unit.html')
+    {
+        $sResult = '';
+
+        switch($sTemplateName) {
+            case 'unit_showcase.html':
+                $sResult = $this->_sUnitClassShowCase;
+                break;
+        }
+
+        return $sResult;
+    }
+}
+
+/** @} */

+ 230 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/text/template/css/main.css

@@ -0,0 +1,230 @@
+@import url(../../../general/template/css/main.css);
+
+/* Breadcrumb */
+div.bx-base-text-breadcrumb  {
+	white-space: nowrap;
+	overflow: hidden;
+}
+div.bx-base-text-breadcrumb a,
+div.bx-base-text-breadcrumb i {
+	display: inline-block;
+	vertical-align: top;
+
+	height: 2.2rem;
+	line-height: 2.2rem;	
+}
+.bx-base-text-breadcrumb-home i.sys-icon {
+	font-size: 2.0rem;
+}
+
+/* unit */
+.bx-base-text-unit h2,
+.bx-base-text-unit h1 {
+    margin:0;
+}
+
+.bx-base-text-unit .sys-icon {
+    vertical-align:middle;
+}
+
+.bx-base-text-unit-cnt {
+	width: 100%;
+}
+
+.bx-base-text-unit-no-thumb,
+.bx-base-text-unit-thumb {
+    display:block;
+    overflow:hidden;    
+}
+
+.bx-base-text-unit-title,
+.bx-base-text-unit-summary {
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+.bx-base-text-unit-summary p {
+    margin:0;
+}
+
+.bx-base-text-unit-cover {
+	position: relative;
+}
+.bx-base-text-unit-text {
+	width: 100%;
+	overflow: hidden;
+    text-overflow: ellipsis;
+}
+.bx-base-text-unit-ext .bx-base-text-unit-text {
+	text-align: left;
+}
+
+.bx-base-text-unit-cover img {
+	position: relative;
+	display: block;
+
+	width: 100%;
+	height: 300px;
+    object-fit: cover;
+}
+
+.bx-base-text-unit-info {
+    white-space:nowrap;
+    overflow:hidden;
+    text-overflow:ellipsis;
+}
+
+.bx-base-text-unit-date {
+    white-space: nowrap;
+}
+
+.bx-base-text-unit-gallery .bx-base-text-unit-no-thumb,
+.bx-base-text-unit-gallery .bx-base-text-unit-thumb {
+    float:none;
+    line-height:1.25em;
+    background-size:cover;
+    background-repeat:no-repeat;
+    background-position:center center;
+    display:table;
+    width:100%;
+}
+
+.bx-base-text-unit-gallery h2 {
+	margin-bottom: 0;
+	max-height: 3.75em;
+    line-height: 1.25em;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    overflow: hidden;
+}
+
+.bx-base-text-wrapper {
+    margin-bottom:0;
+}
+
+.bx-base-text-unit-ext .bx-base-text-unit-thumb {
+    float:left;
+}
+
+.bx-base-text-unit-strecher {
+    visibility: hidden;
+}
+
+
+/* flexbox */
+
+.bx-base-text-unit-gallery-wrapper {
+    display: -webkit-box;
+    display: -webkit-flex;
+    display: -ms-flexbox;
+    display: flex;
+
+    /* makes all items on a row equal height */
+    -webkit-box-align: stretch;
+    -webkit-align-items: stretch;
+    -ms-flex-align: stretch;
+    align-items: stretch;
+
+    /* wrap onto the next line rather than stretching or shrinking */
+    -webkit-flex-wrap: wrap;
+    -ms-flex-wrap: wrap;
+    flex-wrap: wrap;
+
+    /* tells flexbox to start items from the left */
+    -webkit-box-pack: start;
+    -ms-flex-pack: start;
+    flex-pack: start;
+    -webkit-justify-content: flex-start;
+    justify-content: flex-start;
+}
+
+.bx-base-text-unit-gallery-wrapper .bx-base-text-unit-gallery {
+    display: -webkit-box;
+    display: -webkit-flex;
+    display: -ms-flexbox;
+    display: flex;
+
+    -webkit-box-flex: 0;
+
+    -webkit-flex: 0 0 100%;
+    -ms-flex: 0 0 100%;
+    flex: 0 0 100%;
+
+    overflow:hidden;
+}
+
+@media (min-width:533px) and (max-width:720px) {
+    .bx-layout-column-half .bx-base-text-unit-gallery-wrapper .bx-base-text-unit-gallery,
+    .bx-layout-column-thin .bx-base-text-unit-gallery-wrapper .bx-base-text-unit-gallery {
+        -webkit-flex: 0 0 49.8%;
+        -ms-flex: 0 0 49.8%;
+        flex: 0 0 49.8%;
+    }
+	
+	.bx-base-unit-showcase-wrapper .bx-base-text-unit-showcase
+	{
+	   width:calc(50%);
+	}
+}
+
+@media (min-width:533px) and (max-width:1024px) {
+    .bx-layout-column-wide .bx-base-text-unit-gallery-wrapper .bx-base-text-unit-gallery,
+    #sys_search_results .bx-base-text-unit-gallery-wrapper .bx-base-text-unit-gallery,
+    .bx-layout-column-fullwidth .bx-base-text-unit-gallery-wrapper .bx-base-text-unit-gallery {
+        -webkit-flex: 0 0 49.8%;
+        -ms-flex: 0 0 49.8%;
+        flex: 0 0 49.8%;
+    }
+	
+	.bx-base-unit-showcase-wrapper .bx-base-text-unit-showcase
+	{
+	   width:calc(50%);
+	}
+}
+
+@media (min-width:1024px) {
+    .bx-layout-column-wide .bx-base-text-unit-gallery-wrapper .bx-base-text-unit-gallery {
+        -webkit-flex: 0 0 49.8%;
+        -ms-flex: 0 0 49.8%;
+        flex: 0 0 49.8%;
+    }
+    #sys_search_results .bx-base-text-unit-gallery-wrapper .bx-base-text-unit-gallery,
+    .bx-layout-column-fullwidth .bx-base-text-unit-gallery-wrapper .bx-base-text-unit-gallery {
+        -webkit-flex: 0 0 33.25%;
+        -ms-flex: 0 0 33.25%;
+        flex: 0 0 33.25%;
+    }
+	
+	.bx-base-unit-showcase-wrapper .bx-base-text-unit-showcase
+	{
+	   width:calc(50%);
+	}
+}
+
+/* view page */
+.bx-base-text-entity-content,
+.bx-base-text-entity-content p,
+.bx-base-text-entity-content h1,
+.bx-base-text-entity-content h2,
+.bx-base-text-entity-content h3 {
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+.bx-base-text-entity-content p:first-child,
+.bx-base-text-entity-content h2:first-child,
+.bx-base-text-entity-content h1:first-child {
+    margin-top:0;
+}
+
+.bx-base-text-entity-content p:last-child,
+.bx-base-text-entity-content h2:last-child,
+.bx-base-text-entity-content h1:last-child {
+    margin-bottom:0;
+}
+
+.bx-base-text-entity-actions-all .bx-base-text-entity-share,
+.bx-base-text-entity-actions-all .bx-menu-btns-hor {
+    display:inline-block;
+    vertical-align:top;
+}

+ 25 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/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 bx-def-padding-sec">
+	<div class="bx-base-text-unit-cnt bx-def-box bx-def-round-corners bx-def-color-bg-box bx-def-box-sizing">
+		<div class="bx-base-text-unit-text bx-def-padding-leftright bx-def-padding-sec-topbottom bx-def-font-align-center bx-def-box-sizing">
+			<h2><a href="__content_url__">__title__</a></h2>
+		    <div class="bx-base-text-unit-info">
+                <bx_if:meta><div class="bx-base-general-unit-meta">__meta__</div></bx_if:meta>
+            </div>
+		</div>
+	    <bx_if:thumb>
+	        <a href="__content_url__" title="__summary_attr__">
+	            <div class="bx-base-text-unit-thumb bx-def-gal-div-size bx-def-round-corners bx-def-font-middle" style="background-image:url(__gallery_url__)">
+	                <div class="bx-base-text-unit-strecher">__strecher__</div>
+	            </div>
+	        </a>
+	    </bx_if:thumb>
+	    <bx_if:no_thumb>
+	        <div class="bx-def-gal-img-column">
+	            <div class="bx-base-text-unit-no-thumb bx-def-gal-div-size bx-def-font-middle bx-def-font-grayed bx-def-padding bx-def-box-sizing">
+	                __summary_plain__
+	                <div class="bx-base-text-unit-strecher">__strecher__</div>
+	            </div>
+	        </div>
+	    </bx_if:no_thumb>
+	</div>
+</div>

+ 1 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/modules/base/text/template/unit_showcase.html

@@ -0,0 +1 @@
+<bx_include_base:../modules/base/text/template/unit_gallery.html />

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 6 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/plugins_public/headroom.min.js


+ 92 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/plugins_public/tinymce-mention/css/autocomplete.css

@@ -0,0 +1,92 @@
+.rte-autocomplete{
+    position: absolute;
+    top: 0px;
+    left: 0px;
+    display: block;
+    z-index: 1000;
+    float: left;
+    min-width: 160px;
+    padding: 5px 0;
+    margin: 2px 0 0;
+    list-style: none;
+    background-color: #fff;
+    border: 1px solid #ccc;
+    border: 1px solid rgba(0,0,0,0.2);
+    -webkit-border-radius: 6px;
+    -moz-border-radius: 6px;
+    border-radius: 6px;
+    -webkit-box-shadow: 0 5px 10px rgba(0,0,0,0.2);
+    -moz-box-shadow: 0 5px 10px rgba(0,0,0,0.2);
+    box-shadow: 0 5px 10px rgba(0,0,0,0.2);
+    -webkit-background-clip: padding-box;
+    -moz-background-clip: padding;
+    background-clip: padding-box;
+    font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
+    font-size: 14px;
+}
+
+.rte-autocomplete:before {
+    content: '';
+    display: inline-block;
+    border-left: 7px solid transparent;
+    border-right: 7px solid transparent;
+    border-bottom: 7px solid #ccc;
+    border-bottom-color: rgba(0, 0, 0, 0.2);
+    position: absolute;
+    top: -7px;
+    left: 9px;
+}
+
+.rte-autocomplete:after {
+    content: '';
+    display: inline-block;
+    border-left: 6px solid transparent;
+    border-right: 6px solid transparent;
+    border-bottom: 6px solid white;
+    position: absolute;
+    top: -6px;
+    left: 10px;
+}
+
+.rte-autocomplete > li.loading {
+	background: url("https://www.ajaxload.info/cache/FF/FF/FF/00/00/00/1-0.gif") center no-repeat;
+	height: 16px;
+}
+
+.rte-autocomplete > li > a {
+    display: block;
+    padding: 3px 20px;
+    clear: both;
+    font-weight: normal;
+    line-height: 20px;
+    color: #333;
+    white-space: nowrap;
+    text-decoration: none;
+}
+
+.rte-autocomplete >li > a:hover, .rte-autocomplete > li > a:focus, .rte-autocomplete:hover > a, .rte-autocomplete:focus > a {
+    color: #fff;
+    text-decoration: none;
+    background-color: #0081c2;
+    background-image: -moz-linear-gradient(top,#08c,#0077b3);
+    background-image: -webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));
+    background-image: -webkit-linear-gradient(top,#08c,#0077b3);
+    background-image: -o-linear-gradient(top,#08c,#0077b3);
+    background-image: linear-gradient(to bottom,#08c,#0077b3);
+    background-repeat: repeat-x;
+    filter: progid:DXImageTransform.Microsoft;
+}
+
+.rte-autocomplete >.active > a, .rte-autocomplete > .active > a:hover, .rte-autocomplete > .active > a:focus {
+    color: #fff;
+    text-decoration: none;
+    background-color: #0081c2;
+    background-image: -moz-linear-gradient(top,#08c,#0077b3);
+    background-image: -webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));
+    background-image: -webkit-linear-gradient(top,#08c,#0077b3);
+    background-image: -o-linear-gradient(top,#08c,#0077b3);
+    background-image: linear-gradient(to bottom,#08c,#0077b3);
+    background-repeat: repeat-x;
+    outline: 0;
+    filter: progid:DXImageTransform.Microsoft;
+}

+ 3 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/plugins_public/tinymce-mention/css/rte-content.css

@@ -0,0 +1,3 @@
+span#autocomplete {
+    font-weight: bold;
+}

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/plugins_public/tinymce-mention/plugin.min.js


+ 44 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/splash.php

@@ -0,0 +1,44 @@
+<?php
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+require_once('./inc/header.inc.php');
+require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+
+bx_import('BxDolLanguages');
+
+function getPageMainCode()
+{
+    $oTemplate = BxDolTemplate::getInstance();    
+
+    $bEnabled = getParam('sys_site_splash_enabled');
+    if(!$bEnabled)
+    	$oTemplate->displayPageNotFound();
+
+    $oPermalink = BxDolPermalinks::getInstance();
+    $oTemplate->addJs(array('skrollr/skrollr.min.js'));
+    $s = $oTemplate->parseHtmlByContent(getParam('sys_site_splash_code'), array(
+    	'join_link' => BX_DOL_URL_ROOT . $oPermalink->permalink('page.php?i=create-account'),
+    	'login_link' => BX_DOL_URL_ROOT . $oPermalink->permalink('page.php?i=login'),
+    	'login_form' => BxDolService::call('system', 'login_form', array(), 'TemplServiceLogin')
+    )); 
+
+    if (getParam('add_to_mobile_homepage') == 'on')
+        $s .= BxDolService::call('system', 'add_to_mobile_homepage', array(), 'TemplServices');
+        
+    return $s;
+}
+
+check_logged();
+
+$oTemplate = BxDolTemplate::getInstance();
+$oTemplate->setPageNameIndex(BX_PAGE_DEFAULT);
+$oTemplate->setPageContent ('page_main_code', getPageMainCode());
+$oTemplate->getPageCode();
+
+/** @} */

+ 331 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/studio/classes/BxDolStudioNavigationQuery.php

@@ -0,0 +1,331 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaStudio UNA Studio
+ * @{
+ */
+
+class BxDolStudioNavigationQuery extends BxDolDb
+{
+    function __construct()
+    {
+        parent::__construct();
+    }
+
+    function getMenus($aParams, &$aItems, $bReturnCount = true)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sSelectClause = $sJoinClause = $sWhereClause = $sGroupClause = $sOrderClause = $sLimitClause = "";
+
+        if(!isset($aParams['order']) || empty($aParams['order']))
+           $sOrderClause = "ORDER BY `tm`.`id` ASC";
+
+        switch($aParams['type']) {
+            case 'by_id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                	'id' => $aParams['value']
+                );
+
+                $sWhereClause = " AND `tm`.`id`=:id ";
+                break;
+
+            case 'by_object':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                	'object' => $aParams['value']
+                );
+
+                $sWhereClause = " AND `tm`.`object`=:object ";
+                break;
+
+            case 'by_set_name':
+            	$aMethod['params'][1] = array(
+                	'set_name' => $aParams['value']
+                );
+
+                $sWhereClause = " AND `tm`.`set_name`=:set_name ";
+                break;
+
+            case 'counter_by_modules':
+                $aMethod['name'] = 'getPairs';
+                $aMethod['params'][1] = 'module';
+                $aMethod['params'][2] = 'counter';
+                $sSelectClause = ", COUNT(*) AS `counter`";
+                $sGroupClause = "GROUP BY `tm`.`module`";
+                break;
+
+            case 'all':
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT " . ($bReturnCount ? "SQL_CALC_FOUND_ROWS" : "") . "
+                `tm`.`id` AS `id`,
+                `tm`.`object` AS `object`,
+                `tm`.`title` AS `title`,
+                `tm`.`set_name` AS `set_name`,
+                `tm`.`module` AS `module`,
+                `tm`.`template_id` AS `template_id`,
+                `tm`.`deletable` AS `deletable`,
+                `tm`.`active` AS `active`,
+                `tm`.`override_class_name` AS `override_class_name`,
+                `tm`.`override_class_file` AS `override_class_file`" . $sSelectClause . "
+            FROM `sys_objects_menu` AS `tm` " . $sJoinClause . "
+            WHERE 1 " . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause . " " . $sLimitClause;
+        $aItems = call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+
+        if(!$bReturnCount)
+            return !empty($aItems);
+
+        return (int)$this->getOne("SELECT FOUND_ROWS()");
+    }
+
+    function isSetExists($sName)
+    {
+        $sSql = $this->prepare("SELECT COUNT(*) FROM `sys_menu_sets` WHERE `set_name`=? LIMIT 1", $sName);
+        return (int)$this->getOne($sSql) == 1;
+    }
+
+    function getSets($aParams, &$aItems, $bReturnCount = true)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sSelectClause = $sJoinClause = $sWhereClause = $sGroupClause = $sOrderClause = $sLimitClause = "";
+
+        if(!isset($aParams['order']) || empty($aParams['order']))
+           $sOrderClause = "ORDER BY `tms`.`title` ASC";
+
+        switch($aParams['type']) {
+            case 'by_name':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                	'set_name' => $aParams['value']
+                );
+
+                $sSelectClause = ", COUNT(`tmi`.`id`) AS `items_count`";
+                $sJoinClause = "LEFT JOIN `sys_menu_items` AS `tmi` ON `tms`.`set_name`=`tmi`.`set_name` AND `tmi`.`active`='1'";
+                $sWhereClause = " AND `tms`.`set_name`=:set_name ";
+                $sGroupClause = "GROUP BY `tms`.`set_name`";
+                break;
+
+            case 'by_module':
+            	$aMethod['params'][1] = array(
+                	'module' => $aParams['value']
+                );
+
+                $sWhereClause = " AND `tms`.`module`=:module ";
+                break;
+
+            case 'counter_by_modules':
+                $aMethod['name'] = 'getPairs';
+                $aMethod['params'][1] = 'module';
+                $aMethod['params'][2] = 'counter';
+                $sSelectClause = ", COUNT(*) AS `counter`";
+                $sGroupClause = "GROUP BY `tms`.`module`";
+                break;
+
+            case 'dump_by_name':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                	'set_name' => $aParams['value']
+                );
+
+                $sWhereClause = " AND `tms`.`set_name`=:set_name ";
+                break;
+
+            case 'all':
+                if(isset($aParams['except'])) {
+                	$aMethod['params'][1] = array(
+	                	'set_name' => $aParams['except']
+	                );
+
+                    $sWhereClause = " AND `tms`.`set_name`<>:set_name ";
+                }
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT " . ($bReturnCount ? "SQL_CALC_FOUND_ROWS" : "") . "
+                `tms`.`set_name` AS `name`,
+                `tms`.`set_name` AS `set_name`,
+                `tms`.`module` AS `module`,
+                `tms`.`title` AS `title`,
+                `tms`.`deletable` AS `deletable`" . $sSelectClause . "
+            FROM `sys_menu_sets` AS `tms` " . $sJoinClause . "
+            WHERE 1 " . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause . " " . $sLimitClause;
+        $aItems = call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+
+        if(!$bReturnCount)
+            return !empty($aItems);
+
+        return (int)$this->getOne("SELECT FOUND_ROWS()");
+    }
+
+    function addSet($aFields)
+    {
+        $sSql = "INSERT INTO `sys_menu_sets` SET `" . implode("`=?, `", array_keys($aFields)) . "`=?";
+        $sSql = call_user_func_array(array($this, 'prepare'), array_merge(array($sSql), array_values($aFields)));
+        return (int)$this->query($sSql) > 0;
+    }
+
+    function getTemplates($aParams, &$aItems, $bReturnCount = true)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sSelectClause = $sJoinClause = $sWhereClause = $sGroupClause = $sOrderClause = $sLimitClause = "";
+
+        if(!isset($aParams['order']) || empty($aParams['order']))
+           $sOrderClause = "ORDER BY `tmt`.`title` ASC";
+
+        switch($aParams['type']) {
+        	case 'all_visible_key_id':
+        		$aMethod['name'] = 'getAllWithKey';
+        		$aMethod['params'][1] = 'id';
+
+        		$sWhereClause .= "AND `tmt`.`visible`='1'";
+                break;
+
+            case 'all':
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT " . ($bReturnCount ? "SQL_CALC_FOUND_ROWS" : "") . "
+                `tmt`.`id` AS `id`,
+                `tmt`.`template` AS `template`,
+                `tmt`.`title` AS `title`,
+                `tmt`.`visible` AS `visible`" . $sSelectClause . "
+            FROM `sys_menu_templates` AS `tmt` " . $sJoinClause . "
+            WHERE 1 " . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause . " " . $sLimitClause;
+        $aItems = call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+
+        if(!$bReturnCount)
+            return !empty($aItems);
+
+        return (int)$this->getOne("SELECT FOUND_ROWS()");
+    }
+
+    function getItems($aParams, &$aItems, $bReturnCount = true)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sSelectClause = $sJoinClause = $sWhereClause = $sGroupClause = $sOrderClause = $sLimitClause = "";
+
+        if(!isset($aParams['order']) || empty($aParams['order']))
+           $sOrderClause = "ORDER BY `tmi`.`order` ASC";
+
+        switch($aParams['type']) {
+            case 'by_id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                	'id' => $aParams['value']
+                );
+
+                $sWhereClause = " AND `tmi`.`id`=:id ";
+                break;
+
+            case 'by_set_name':
+            	$aMethod['params'][1] = array(
+                	'set_name' => $aParams['value']
+                );
+
+                $sWhereClause = " AND `tmi`.`set_name`=:set_name ";
+                break;
+
+            case 'counter_by_sets':
+                $aMethod['name'] = 'getPairs';
+                $aMethod['params'][1] = 'set_name';
+                $aMethod['params'][2] = 'counter';
+                $sSelectClause = ", COUNT(*) AS `counter`";
+                $sGroupClause = "GROUP BY `tmi`.`set_name`";
+                break;
+
+            case 'counter_by_modules':
+                $aMethod['name'] = 'getPairs';
+                $aMethod['params'][1] = 'module';
+                $aMethod['params'][2] = 'counter';
+                $sSelectClause = ", COUNT(*) AS `counter`";
+                $sGroupClause = "GROUP BY `tmi`.`module`";
+                break;
+
+            case 'all':
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT " . ($bReturnCount ? "SQL_CALC_FOUND_ROWS" : "") . "
+                `tmi`.`id` AS `id`,
+                `tmi`.`set_name` AS `set_name`,
+                `tmi`.`module` AS `module`,
+                `tmi`.`name` AS `name`,
+                `tmi`.`title_system` AS `title_system`,
+                `tmi`.`title` AS `title`,
+                `tmi`.`link` AS `link`,
+                `tmi`.`onclick` AS `onclick`,
+                `tmi`.`target` AS `target`,
+                `tmi`.`icon` AS `icon`,
+                `tmi`.`addon` AS `addon`,
+                `tmi`.`submenu_object` AS `submenu_object`,
+                `tmi`.`submenu_popup` AS `submenu_popup`,
+                `tmi`.`visible_for_levels` AS `visible_for_levels`,
+                `tmi`.`active` AS `active`,
+                `tmi`.`copyable` AS `copyable`,
+                `tmi`.`editable` AS `editable`,
+                `tmi`.`order` AS `order`" . $sSelectClause . "
+            FROM `sys_menu_items` AS `tmi` " . $sJoinClause . "
+            WHERE 1 " . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause . " " . $sLimitClause;
+        $aItems = call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+
+        if(!$bReturnCount)
+            return !empty($aItems);
+
+        return (int)$this->getOne("SELECT FOUND_ROWS()");
+    }
+
+    function deleteItemsBy($aParams)
+    {
+        $sWhereClause = $sLimitClause = "";
+
+        switch($aParams['type']) {
+            case 'by_id':
+            	$aMethod['params'][1] = array(
+                	'id' => $aParams['value']
+                );
+
+                $sWhereClause = " AND `tmi`.`id`=:id ";
+                break;
+
+            case 'by_set_name':
+            	$aMethod['params'][1] = array(
+                	'set_name' => $aParams['value']
+                );
+
+                $sWhereClause = " AND `tmi`.`set_name`=:set_name ";
+                break;
+
+            case 'all':
+                break;
+        }
+
+        $sSql = "DELETE FROM `tmi` USING `sys_menu_items` AS `tmi` WHERE 1 " . $sWhereClause . " " . $sLimitClause;
+        return (int)$this->query($sSql) > 0;
+    }
+
+    function addItem($aItem)
+    {
+        $sSql = "INSERT INTO `sys_menu_items` SET `" . implode("`=?, `", array_keys($aItem)) . "`=?";
+        $sSql = call_user_func_array(array($this, 'prepare'), array_merge(array($sSql), array_values($aItem)));
+        return (int)$this->query($sSql) > 0 ? $this->lastId() : 0;
+    }
+
+    function updateItem($iId, $aFields)
+    {
+        $sSql = "UPDATE `sys_menu_items` SET `" . implode("`=?, `", array_keys($aFields)) . "`=?  WHERE `id`=?";
+        $sSql = call_user_func_array(array($this, 'prepare'), array_merge(array($sSql), array_values($aFields), array($iId)));
+        return $this->query($sSql);
+    }
+
+    function getItemOrderMax($sSetName)
+    {
+        $sSql = $this->prepare("SELECT MAX(`order`) FROM `sys_menu_items` WHERE `set_name`=? LIMIT 1", $sSetName);
+        return (int)$this->getOne($sSql);
+    }
+}
+
+/** @} */

+ 652 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/studio/classes/BxDolStudioToolsAudit.php

@@ -0,0 +1,652 @@
+<?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    UnaStudio UNA Studio
+ * @{
+ */
+
+define('BX_DOL_AUDIT_FAIL', 'fail');
+define('BX_DOL_AUDIT_WARN', 'warn');
+define('BX_DOL_AUDIT_UNDEF', 'undef');
+define('BX_DOL_AUDIT_OK', 'ok');
+
+class BxDolStudioToolsAudit extends BxDol
+{
+    protected $aType2ClassCSS;
+    protected $aType2Title;
+
+    protected $sMinPhpVer;
+    protected $aPhpSettings;
+    protected $iPhpErrorReporting;
+
+    protected $sMinMysqlVer;
+    protected $aMysqlOptimizationSettings;
+
+    protected $aOptimizationSettings;
+
+    protected $aRequiredApacheModules;
+
+    function __construct()
+    {
+        parent::__construct();
+
+        $this->aType2ClassCSS = array (
+            BX_DOL_AUDIT_FAIL => 'fail',
+            BX_DOL_AUDIT_WARN => 'warn',
+            BX_DOL_AUDIT_UNDEF => 'undef',
+            BX_DOL_AUDIT_OK => 'ok',
+        );
+
+        $this->aType2Title = array (
+            BX_DOL_AUDIT_FAIL => _t('_sys_audit_title_fail'),
+            BX_DOL_AUDIT_WARN => _t('_sys_audit_title_warn'),
+            BX_DOL_AUDIT_UNDEF => _t('_sys_audit_title_undef'),
+            BX_DOL_AUDIT_OK => _t('_sys_audit_title_ok'),
+        );
+
+        $this->sMinPhpVer = '5.5.0';
+        $this->aPhpSettings = array (
+            'allow_url_fopen' => array('op' => '=', 'val' => true, 'type' => 'bool'),
+            'allow_url_include' => array('op' => '=', 'val' => false, 'type' => 'bool'),
+            'magic_quotes_gpc' => array('op' => '=', 'val' => false, 'type' => 'bool', 'warn' => 1),
+            'memory_limit' => array('op' => '>=', 'val' => 128*1024*1024, 'type' => 'bytes', 'unlimited' => -1),
+            'post_max_size' => array('op' => '>=', 'val' => 2*1024*1024, 'type' => 'bytes', 'warn' => 1),
+            'upload_max_filesize' => array('op' => '>=', 'val' => 2*1024*1024, 'type' => 'bytes', 'warn' => 1),
+            'register_globals' => array('op' => '=', 'val' => false, 'type' => 'bool'),
+            'safe_mode' => array('op' => '=', 'val' => false, 'type' => 'bool'),
+            'short_open_tag' => array('op' => '=', 'val' => true, 'type' => 'bool'),
+            'disable_functions' => array('op' => 'without', 'val' => 'shell_exec,eval,assert,phpinfo,getenv,ini_set,mail,fsockopen,chmod,parse_ini_file'),
+            'php module: curl' => array('op' => 'module', 'val' => 'curl'),
+            'php module: gd' => array('op' => 'module', 'val' => 'gd'),
+            'php module: mbstring' => array('op' => 'module', 'val' => 'mbstring'),
+            'php module: json' => array('op' => 'module', 'val' => 'json'),
+            'php module: fileinfo' => array('op' => 'module', 'val' => 'fileinfo'),
+            'php module: zip' => array('op' => 'module', 'val' => 'zip'),
+            'php module: openssl' => array('op' => 'module', 'val' => 'openssl'),
+            'php module: exif' => array('op' => 'module', 'val' => 'exif'),
+        );
+
+        $this->sMinMysqlVer = '5.5.3';
+        $this->aMysqlOptimizationSettings = array (
+            'key_buffer_size' => array('op' => '>=', 'val' => 128*1024, 'type' => 'bytes'),
+            'query_cache_limit' => array('op' => '>=', 'val' => 1000000),
+            'query_cache_size' => array('op' => '>=', 'val' => 16*1024*1024, 'type' => 'bytes'),
+            'query_cache_type' => array('op' => 'strcasecmp', 'val' => 'on'),
+            'max_heap_table_size' => array('op' => '>=', 'val' => 16*1024*1024, 'type' => 'bytes'),
+            'tmp_table_size' => array('op' => '>=', 'val' => 16*1024*1024, 'type' => 'bytes'),
+            'thread_cache_size ' => array('op' => '>', 'val' => 0),
+        );
+
+        $this->aOptimizationSettings = array (
+            'DB cache' => array('enabled' => 'sys_db_cache_enable', 'cache_engine' => 'sys_db_cache_engine', 'check_accel' => true),
+            'Pages cache' => array('enabled' => 'sys_page_cache_enable', 'cache_engine' => 'sys_page_cache_engine', 'check_accel' => true),
+            'Page blocks cache' => array('enabled' => 'sys_pb_cache_enable', 'cache_engine' => 'sys_pb_cache_engine', 'check_accel' => true),
+            'Templates Cache' => array('enabled' => 'sys_template_cache_enable', 'cache_engine' => 'sys_template_cache_engine', 'check_accel' => true),
+            'CSS files cache' => array('enabled' => 'sys_template_cache_css_enable', 'cache_engine' => '', 'check_accel' => false),
+            'JS files cache' => array('enabled' => 'sys_template_cache_js_enable', 'cache_engine' => '', 'check_accel' => false),
+            'Compression for CSS/JS cache' => array('enabled' => 'sys_template_cache_compress_enable', 'cache_engine' => '', 'check_accel' => false),
+        );
+
+        $this->aRequiredApacheModules = array (
+            'rewrite_module' => 'mod_rewrite',
+        );
+
+        if (isset($_GET['action'])) {
+            $sOutput = null;
+            switch ($_GET['action']) {
+                case 'audit_send_test_email':
+                    $sOutput = $this->sendTestEmail();
+                    break;
+                case 'phpinfo':
+                    ob_start();
+                    phpinfo();
+                    $sOutput = ob_get_clean();
+                    break;
+                case 'phpinfo_popup':
+                    $sUrlSelf = bx_js_string($_SERVER['PHP_SELF'], BX_ESCAPE_STR_APOS);
+                    $sUrlSelf = bx_append_url_params($sUrlSelf, array('action' => 'phpinfo'));
+                    $sOutput = '<iframe width="640" height="480" src="' . $sUrlSelf . '"></iframe>';
+                    break;
+            }
+            if ($sOutput) {
+                header('Content-type: text/html; charset=utf-8');
+                echo $sOutput;
+                exit;
+            }
+        }
+    }
+
+    public function generate()
+    {
+        ob_start();
+
+        $this->setErrorReporting();
+
+        $this->generateStyles();
+        $this->generateJs();
+
+        $this->requirements();
+
+        if (!defined('BX_DOL_INSTALL'))
+            $this->siteSetup();
+
+        $this->optimization();
+
+        $this->manualCheck();
+
+        $this->restoreErrorReporting();
+
+        return ob_get_clean();
+    }
+
+    public function generateStyles($bReturn = false)
+    {
+    	ob_start();
+        ?>
+<style>
+    .ok {
+        color:green;
+    }
+    .fail {
+        color:red;
+    }
+    .warn {
+        color:orange;
+    }
+    .undef {
+        color:gray;
+    }
+</style>
+        <?php
+        $sStyles = ob_get_clean();
+
+        if($bReturn)
+			return $sStyles;
+
+		echo $sStyles;
+    }
+
+    public function generateJs()
+    {
+        $sUrlSelf = bx_js_string($_SERVER['PHP_SELF'], BX_ESCAPE_STR_APOS);
+        ?>
+        <script language="javascript">
+            function bx_sys_adm_audit_test_email()
+            {
+            	bx_prompt('<?php echo _t('_Email'); ?>', '<?php echo class_exists('BxDolDb') && BxDolDb::getInstance() ? BxDolDb::getInstance()->getParam('site_email') : ''; ?>', function(oPopup) {
+        			var sEmail = oPopup.getValue();
+                    if (null == sEmail || ('string' == (typeof sEmail) && !sEmail.length))
+                        return;
+
+                    $('#bx-sys-adm-audit-test-email').html('Sending...');
+                    $.post('<?php echo bx_append_url_params($sUrlSelf, array('action' => 'audit_send_test_email')); ?>&email=' + sEmail, function(data) {
+                        $('#bx-sys-adm-audit-test-email').html(data);
+                    });
+				});
+
+            	return false;
+            }
+
+            function bx_sys_adm_audit_phpinfo()
+            {
+                $(window).dolPopupAjax({url: '<?php echo bx_append_url_params($sUrlSelf, array('action' => 'phpinfo_popup')); ?>'});
+            }
+        </script>
+        <?php
+    }
+
+    /**
+     * Check minimal requirements
+     * @param $sType - BX_DOL_AUDIT_FAIL, BX_DOL_AUDIT_WARN, BX_DOL_AUDIT_UNDEF or BX_DOL_AUDIT_OK
+     * @param $sFunc - requirementsPHP, requirementsMySQL or requirementsWebServer
+     * @return array of FAIL, WARN, UNDEF or OK items, if array is empty then no specified items are found
+     */
+    public function checkRequirements($sType = BX_DOL_AUDIT_FAIL, $sFunc = 'requirementsPHP')
+    {
+        $this->setErrorReporting();
+
+        $aRet = array ();        
+        $aMessages = array ();  
+
+        $this->$sFunc(false, $aMessages);
+
+        foreach ($aMessages as $sName => $r)
+            if ($sType == $r['type'])
+                $aRet[] = "$sName = " . $this->format_output($r['params']['real_val'], isset($this->aPhpSettings[$sName]) ? $this->aPhpSettings[$sName] : '') . " - " . $this->getMsgHTML($sName, $r);        
+
+        $this->restoreErrorReporting();
+
+        return $aRet;
+    }
+
+    public function typeToTitle ($sType) 
+    {
+        return $this->aType2Title[$sType];
+    }
+
+    protected function requirements()
+    {
+        echo '<h1>' . _t('_sys_audit_header_requirements') . '</h1>';
+        $this->requirementsPHP();
+        if (!defined('BX_DOL_INSTALL'))
+            $this->requirementsMySQL();
+        $this->requirementsWebServer();
+        $this->requirementsOS();
+        $this->requirementsHardware();
+    }
+
+    protected function requirementsPHP($bEcho = true, &$aOutputMessages = null)
+    {
+        $a = unserialize(file_get_contents("http://www.php.net/releases/index.php?serialize=1"));
+        $sLatestPhpVersion = $a[5]['version'];
+
+        if (version_compare(phpversion(), "5.4", ">=") == 1)
+            unset($this->aPhpSettings['short_open_tag']);
+
+        $aMessages = array ();
+
+        $sPhpVer = PHP_VERSION;
+        if (empty($sLatestPhpVersion))
+            $aVer = array('type' => BX_DOL_AUDIT_UNDEF, 'msg' => _t('_sys_audit_msg_value_checking_failed'), 'params' => array ('real_val' => $sPhpVer));
+        elseif (version_compare($sPhpVer, $this->sMinPhpVer, '<'))
+            $aVer = array('type' => BX_DOL_AUDIT_FAIL, 'msg' => _t('_sys_audit_msg_version_is_incompatible', $this->sMinPhpVer), 'params' => array ('real_val' => $sPhpVer));
+        elseif (version_compare($sPhpVer, '5.3.0', '>=') && version_compare($sPhpVer, '5.4.0', '<'))
+            $aVer = array('type' => BX_DOL_AUDIT_WARN, 'msg' => _t('_sys_audit_msg_version_is_outdated', $sLatestPhpVersion), 'params' => array ('real_val' => $sPhpVer));
+        else
+            $aVer = array('type' => BX_DOL_AUDIT_OK, 'params' => array ('real_val' => $sPhpVer));
+
+        $aMessages[_t('_sys_audit_version')] = $aVer;
+
+        foreach ($this->aPhpSettings as $sName => $r) {
+            $a = $this->checkPhpSetting($sName, $r);
+            if ($a['res'])
+                $aMessages[$sName] = array('type' => BX_DOL_AUDIT_OK, 'params' => $a);
+            elseif (isset($r['warn']) && $r['warn'])
+                $aMessages[$sName] = array('type' => BX_DOL_AUDIT_WARN, 'msg' => _t('_sys_audit_msg_should_be', $r['op'], $this->format_output($r['val'], $r)), 'params' => $a);
+            else
+                $aMessages[$sName] = array('type' => BX_DOL_AUDIT_FAIL, 'msg' => _t('_sys_audit_msg_must_be', $r['op'], $this->format_output($r['val'], $r)), 'params' => $a);
+        }
+
+        if (null !== $aOutputMessages)
+            $aOutputMessages = $aMessages;
+
+        if ($bEcho) {
+            $s = '';
+            foreach ($aMessages as $sName => $r) {
+                $s .= $this->getBlock($sName, $this->format_output($r['params']['real_val'], isset($this->aPhpSettings[$sName]) ? $this->aPhpSettings[$sName] : ''), $this->getMsgHTML($sName, $r));
+            }
+            echo $this->getSection('PHP', '', $s);
+        }
+    }
+
+    protected function requirementsMySQL($bEcho = true, &$aOutputMessages = null)
+    {
+        $sMysqlVer = BxDolDb::getInstance()->getServerInfo();
+        if (preg_match ('/^(\d+)\.(\d+)\.(\d+)/', $sMysqlVer, $m)) {
+            $sMysqlVer = "{$m[1]}.{$m[2]}.{$m[3]}";
+            if (version_compare($sMysqlVer, $this->sMinMysqlVer, '<'))
+                $aMessage = array('type' => BX_DOL_AUDIT_FAIL, 'msg' => _t('_sys_audit_msg_version_is_incompatible', $this->sMinMysqlVer));
+            else
+                $aMessage = array('type' => BX_DOL_AUDIT_OK);
+        } else {
+            $aMessage = array('type' => BX_DOL_AUDIT_UNDEF, 'msg' => _t('_sys_audit_msg_value_checking_failed'));
+        }
+
+        if (null !== $aOutputMessages)
+            $aOutputMessages['mysql'] = array($aMessage);
+
+        if ($bEcho) {
+            $s = $this->getBlock(_t('_sys_audit_version'), $sMysqlVer, $this->getMsgHTML(_t('_sys_audit_version'), $aMessage));
+            echo $this->getSection('MySQL', '', $s);
+        }
+    }
+
+    protected function requirementsWebServer($bEcho = true, &$aOutputMessages = null)
+    {
+        $aMessages = array();
+        foreach ($this->aRequiredApacheModules as $sName => $sNameCompiledName)
+            $aMessages[$sName] = $this->checkApacheModule($sName, $sNameCompiledName);
+
+        if (null !== $aOutputMessages)
+            $aOutputMessages = $aMessages;
+
+        if ($bEcho) {
+            $s = '';
+            foreach ($aMessages as $sName => $r) {
+                $s .= $this->getBlock($sName, '', $this->getBlock('', '', $this->getMsgHTML($sName, $r), false));
+            }
+            echo $this->getSection(_t('_sys_audit_section_webserver'), $_SERVER['SERVER_SOFTWARE'], $s);
+        }
+    }
+
+    protected function requirementsOS()
+    {
+        $s = $this->getBlock(php_uname());
+        echo $this->getSection(_t('_sys_audit_section_os'), '', $s);
+    }
+
+    protected function requirementsHardware()
+    {
+        $s = $this->getBlock(_t('_sys_audit_msg_hardware_requirements'));
+        echo $this->getSection(_t('_sys_audit_section_hardware'), '', $s);
+    }
+
+    protected function siteSetup()
+    {
+        $sFfmpegPath = defined('BX_SYSTEM_FFMPEG') ? BX_SYSTEM_FFMPEG : BX_DIRECTORY_PATH_PLUGINS . 'ffmpeg/ffmpeg.exe';
+
+        $sEmailToCkeckMailSending = class_exists('BxDolDb') && BxDolDb::getInstance() ? BxDolDb::getInstance()->getParam('site_email') : '';
+
+        $oUpgrader = new BxDolUpgrader();
+        if (!($sLatestVer = $oUpgrader->getLatestVersionNumber()))
+            $sLatestVer = 'undefined';
+
+        $sVer = bx_get_ver();
+        $aMessage = array('type' => BX_DOL_AUDIT_OK);
+        if (!version_compare($sVer, $sLatestVer, '>='))
+            $aMessage = array('type' => BX_DOL_AUDIT_WARN, 'msg' => _t('_sys_audit_msg_version_is_outdated', $sLatestVer));
+
+        $s = $this->getBlock(_t('_sys_audit_version_script'), $sVer, $this->getMsgHTML(_t('_sys_audit_version_script'), $aMessage));
+
+        $s .= $this->getBlock(_t('_sys_audit_permissions'), '', _t('_sys_audit_msg_permissions'));
+
+        $s .= $this->getBlock('ffmpeg', '', _t('_sys_audit_msg_ffmpeg', `{$sFfmpegPath} 2>&1`));
+
+        $s .= $this->getBlock(_t('_sys_audit_mail_sending'), '', _t('_sys_audit_msg_mail_sending'));
+
+        $s .= $this->getBlock(_t('_sys_audit_cron_jobs'), '', _t('_sys_audit_msg_cron_jobs', `crontab -l 2>&1`));
+
+        $iCronTime = getParam('sys_cron_time');
+        $s .= $this->getBlock(_t('_sys_audit_cron_jobs_exec_time'), '', !empty($iCronTime) ? bx_time_js($iCronTime, BX_FORMAT_DATE_TIME, true) : _t('_None'));
+
+        echo '<h1>' . _t('_sys_audit_header_site_setup') . '</h1>';
+        echo "<ul>$s</ul>";
+    }
+
+    protected function optimization()
+    {
+        echo '<h1>' . _t('_sys_audit_header_site_optimization') . '</h1>';
+
+        $this->optimizationPhp();
+
+        if (!defined('BX_DOL_INSTALL'))
+            $this->optimizationMySQL();
+
+        $this->optimizationWebServer();
+
+        if (!defined('BX_DOL_INSTALL'))
+            $this->optimizationScript();
+    }
+
+    protected function optimizationPhp()
+    {
+        $s = '';
+        $aMessage = array();
+
+        $sAccel = $this->getPhpAccelerator();
+        if (!$sAccel)
+            $aMessage = array('type' => BX_DOL_AUDIT_WARN, 'msg' => _t('_sys_audit_msg_php_accelerator_missing'));
+        else
+            $aMessage = array('type' => BX_DOL_AUDIT_OK);
+        $s .= $this->getBlock(_t('_sys_audit_php_accelerator'), $sAccel, $this->getMsgHTML(_t('_sys_audit_php_accelerator'), $aMessage));
+
+        $sSapi = php_sapi_name();
+        if (0 === strcasecmp('cgi', $sSapi))
+            $aMessage = array('type' => BX_DOL_AUDIT_WARN, 'msg' => _t('_sys_audit_msg_php_setup_inefficient'));
+        else
+            $aMessage = array('type' => BX_DOL_AUDIT_OK);
+        $s .= $this->getBlock(_t('_sys_audit_php_setup'), $sSapi, $this->getMsgHTML(_t('_sys_audit_php_setup'), $aMessage));
+
+        echo $this->getSection('PHP', '', $s);
+    }
+
+    protected function optimizationMySQL()
+    {
+        $s = '';
+        $oDb = BxDolDb::getInstance();
+
+        foreach ($this->aMysqlOptimizationSettings as $sName => $r) {
+            $a = $this->checkMysqlSetting($sName, $r, $oDb);
+            $aMessage = array('type' => BX_DOL_AUDIT_OK);
+            if (!$a['res'])
+                $aMessage = array('type' => BX_DOL_AUDIT_FAIL, 'msg' => _t('_sys_audit_msg_must_be', $r['op'], $this->format_output($r['val'], $r)));
+            $s .= $this->getBlock($sName, $this->format_output($a['real_val'], $r), $this->getMsgHTML($sName, $aMessage));
+        }
+
+        echo $this->getSection('MySQL', '', $s);
+    }
+
+    protected function optimizationWebServer()
+    {
+        $s = '';
+
+        $sName = 'expires_module';
+        $sApacheModuleChack = _t('_sys_audit_msg_optimization_apache_module', $sName, $this->checkApacheModule($sName));
+        $s .= $this->getBlock(_t('_sys_audit_userside_caching'), '', _t('_sys_audit_msg_userside_caching', $this->getUrlForGooglePageSpeed('LeverageBrowserCaching'), $sApacheModuleChack));
+
+        $sName = 'deflate_module';
+        $sApacheModuleChack = _t('_sys_audit_msg_optimization_apache_module', $sName, $this->checkApacheModule($sName));
+        $s .= $this->getBlock(_t('_sys_audit_serverside_compression'), '', _t('_sys_audit_msg_serverside_compression', $sApacheModuleChack));
+
+        echo $this->getSection(_t('_sys_audit_section_webserver'), '', $s);
+    }
+
+    protected function optimizationScript()
+    {
+        $s = '';
+        foreach ($this->aOptimizationSettings as $sName => $a) {
+
+            $sVal = ('always_on' == $a['enabled'] || getParam($a['enabled'])) ? 'On' : 'Off';
+            if ($a['cache_engine'])
+                $sVal .= _t('_sys_audit_msg_x_based_cache_engine', getParam($a['cache_engine']));
+
+            if ('always_on' != $a['enabled'] && !getParam($a['enabled']))
+                $aMessage = array('type' => BX_DOL_AUDIT_FAIL, 'msg' => _t('_sys_audit_msg_optimization_fail'));
+            elseif ($a['check_accel'] && !$this->getPhpAccelerator() && 'File' == getParam($a['cache_engine']))
+                $aMessage = array('type' => BX_DOL_AUDIT_WARN, 'msg' => _t('_sys_audit_msg_optimization_warn'));
+            else
+                $aMessage = array('type' => BX_DOL_AUDIT_OK);
+
+            $s .= $this->getBlock($sName, $sVal, $this->getMsgHTML($sName, $aMessage));
+        }
+
+        echo $this->getSection('UNA', '', $s);
+    }
+
+    protected function manualCheck()
+    {
+        echo '<a name="manual_audit"></a>';
+        echo '<h1>' . _t('_sys_audit_header_manual_audit') . '</h1>';
+        echo _t('_sys_audit_msg_manual_audit');
+    }
+
+    protected function checkPhpSetting($sName, $a)
+    {
+        $mixedVal = ini_get($sName);
+        $mixedVal = $this->format_input ($mixedVal, $a);
+
+        switch ($a['op']) {
+            case 'without':
+                $aFuncsDisabled = explode(',', $mixedVal);
+                $aFuncsMustBeEnabled = explode(',', $a['val']);
+                $a = array_intersect($aFuncsDisabled, $aFuncsMustBeEnabled);
+                $bResult = !$a;
+                break;
+            case 'module':
+                $bResult = extension_loaded($a['val']);
+                $mixedVal = $bResult ? $a['val'] : '';
+                break;
+            case '>':
+                $bResult = (isset($a['unlimited']) && $mixedVal == $a['unlimited']) ? true : ($mixedVal > $a['val']);
+                break;
+            case '>=':
+                $bResult = (isset($a['unlimited']) && $mixedVal == $a['unlimited']) ? true : ($mixedVal >= $a['val']);
+                break;
+            case '=':
+            default:
+                $bResult = ($mixedVal == $a['val']);
+        }
+        return array ('res' => $bResult, 'real_val' => $mixedVal);
+    }
+
+    protected function checkMysqlSetting($sName, $a, $oDb)
+    {
+        $mixedVal = $oDb->getOption($sName);
+        $mixedVal = $this->format_input ($mixedVal, $a);
+
+        switch ($a['op']) {
+            case '>':
+                $bResult = ($mixedVal > $a['val']);
+                break;
+            case '>=':
+                $bResult = ($mixedVal >= $a['val']);
+                break;
+            case 'strcasecmp':
+                $bResult = 0 === strcasecmp($mixedVal, $a['val']);
+                break;
+            case '=':
+            default:
+                $bResult = ($mixedVal == $a['val']);
+        }
+        return array ('res' => $bResult, 'real_val' => $mixedVal);
+    }
+
+    protected function format_output ($mixedVal, $a)
+    {
+        if (isset($a['type']) && 'bool' == $a['type'])
+            return $mixedVal ? 'On' : 'Off';
+        else
+            return $mixedVal;
+    }
+
+    protected function format_input ($mixedVal, $a)
+    {
+        if (isset($a['type']) && 'bytes' == $a['type'])
+            return $this->format_bytes ($mixedVal);
+        else
+            return $mixedVal;
+    }
+
+    protected function format_bytes($val)
+    {
+        return return_bytes($val);
+    }
+
+    protected function checkApacheModule ($sModule, $sNameCompiledName = '')
+    {
+        $a = array (
+            'deflate_module' => 'mod_deflate',
+            'expires_module' => 'mod_expires',
+        );
+        if (!$sNameCompiledName && isset($a[$sModule]))
+            $sNameCompiledName = $a[$sModule];
+
+        if (function_exists('apache_get_modules')) {
+
+            $aModules = apache_get_modules();
+            $ret = in_array($sNameCompiledName, $aModules);
+
+        } else {
+
+            $sApachectlPath = trim(`which apachectl`);
+            if (!$sApachectlPath)
+                $sApachectlPath = trim(`which apache2ctl`);
+            if (!$sApachectlPath)
+                $sApachectlPath = trim(`which /usr/local/apache/bin/apachectl`);
+            if (!$sApachectlPath)
+                $sApachectlPath = trim(`which /usr/local/apache/bin/apache2ctl`);
+            if (!$sApachectlPath) {
+                return array('type' => BX_DOL_AUDIT_UNDEF, 'msg' => _t('_sys_audit_msg_apache_module_undef', $sModule));
+            }
+            $ret = (boolean)`$sApachectlPath -M 2>&1 | grep $sModule`;
+            if (!$ret)
+                $ret = (boolean)`$sApachectlPath -l 2>&1 | grep $sNameCompiledName`;
+        }
+
+        $aMessage = array('type' => BX_DOL_AUDIT_OK);
+        if (!$ret)
+            $aMessage = array('type' => BX_DOL_AUDIT_FAIL, 'msg' => _t('_sys_audit_msg_apache_module_fail', $sModule));
+        
+        return $aMessage;
+    }
+
+    protected function getPhpAccelerator ()
+    {
+        $aAccelerators = array (
+            'APC' => array('op' => 'module', 'val' => 'apc'),
+            'XCache' => array('op' => 'module', 'val' => 'xcache'),
+        	'ZendOPcache' => array('op' => 'module', 'val' => 'Zend OPcache'),
+        );
+        foreach ($aAccelerators as $sName => $r) {
+            $a = $this->checkPhpSetting($sName, $r);
+            if ($a['res'])
+                return $sName;
+        }
+        return false;
+    }
+
+    protected function getUrlForGooglePageSpeed ($sRule)
+    {
+        $sUrl = urlencode(BX_DOL_URL_ROOT);
+        return 'http://pagespeed.googlelabs.com/#url=' . $sUrl . '&mobile=false&rule=' . $sRule;
+    }
+
+    protected function sendTestEmail ()
+    {
+        $sEmailToCkeckMailSending = isset($_GET['email']) ? $_GET['email'] : getParam('site_email');
+        $mixedRet = sendMail($sEmailToCkeckMailSending, 'Audit Test Email', 'Sample text for testing<br /><u><b>Sample text for testing</b></u>', '', array(), BX_EMAIL_SYSTEM);
+        if (!$mixedRet) {
+            $aMessage = array('type' => BX_DOL_AUDIT_FAIL, 'msg' => _t('_sys_audit_msg_mail_send_failed'));
+            return $this->getBlock('', '', $this->getMsgHTML(_t('_sys_audit_mail_sending'), $aMessage), false);
+        } else {
+            return _t('_sys_audit_msg_mail_sent', $sEmailToCkeckMailSending);
+        }
+    }
+
+    protected function setErrorReporting ()
+    {
+        if (version_compare(phpversion(), "5.3.0", ">=") == 1)
+            $this->iPhpErrorReporting = error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING & ~E_DEPRECATED & ~E_STRICT);
+        else
+            $this->iPhpErrorReporting = error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
+    }
+
+    protected function restoreErrorReporting ()
+    {
+        error_reporting($this->iPhpErrorReporting);
+    }
+
+    protected function getSection($sTitle, $sTitleAddon, $sContent)
+    {
+        $s = '<b>' . $sTitle . '</b>: ' . $sTitleAddon;
+        $s .= '<ul>';
+        $s .= $sContent;
+        $s .= '</ul>';
+        return $s;
+    }
+
+    protected function getBlock($sName, $sValue = '', $sMsg = '', $bWrapAsListItem = true)
+    {
+        $s = $bWrapAsListItem ? '<li>'  : '';
+        if ($sName !== '')
+            $s .= "$sName ";
+        if ($sValue !== '')
+            $s .= " = $sValue ";
+        if ($sMsg)
+                $s .= ($s ? " - " : '') . $sMsg;
+        return $s . ($bWrapAsListItem ? '</li>' : '') . "\n";
+    }
+
+    protected function getMsgHTML($sName, $a)
+    {
+        $s = '';
+        $s .= '<b class="' . $this->aType2ClassCSS[$a['type']]. '">' . $this->aType2Title[$a['type']]. '</b> ';
+        if (isset($a['msg']))
+            $s .= '(' . $a['msg'] . ')';
+        return $s;
+    }
+}
+
+/** @} */

+ 57 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/studio/js/navigation_import.js

@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaStudio UNA Studio
+ * @{
+ */
+function BxDolStudioNavigationImport(oOptions) {
+	this._iSearchTimeoutId = false;
+	this.sActionsUrl = oOptions.sActionUrl;
+	this.sPageUrl = oOptions.sPageUrl;
+	this.sObjNameGrid = oOptions.sObjNameGrid;
+    this.sObjName = oOptions.sObjName == undefined ? 'oBxDolStudioNavigationImport' : oOptions.sObjName;
+    this.sAnimationEffect = oOptions.sAnimationEffect == undefined ? 'fade' : oOptions.sAnimationEffect;
+    this.iAnimationSpeed = oOptions.iAnimationSpeed == undefined ? 'slow' : oOptions.iAnimationSpeed;
+    this.sParamsDivider = oOptions.sParamsDivider == undefined ? '#-#' : oOptions.sParamsDivider;
+
+    this.sTextSearchInput = oOptions.sTextSearchInput == undefined ? '' : oOptions.sTextSearchInput;
+}
+
+BxDolStudioNavigationImport.prototype.onChangeFilter = function() {
+	var $this = this;
+
+	var sValueSet = $('#bx-grid-set-' + this.sObjNameGrid).val();
+
+	var sValueModule = $('#bx-grid-module-' + this.sObjNameGrid).val();
+
+	var sValueSearch = $('#bx-grid-search-' + this.sObjNameGrid).val();
+	if(sValueSearch == this.sTextSearchInput)
+		sValueSearch = '';
+
+	clearTimeout($this._iSearchTimeoutId);
+    $this._iSearchTimeoutId = setTimeout(function () {
+    	if(glGrids[$this.sObjNameGrid])
+    		glGrids[$this.sObjNameGrid].setFilter(sValueSet + $this.sParamsDivider + sValueModule + $this.sParamsDivider + sValueSearch, true);
+    }, 500);
+};
+
+BxDolStudioNavigationImport.prototype.onImport = function(oData) {
+	$.each(oData.disable, function (iKey, iValue){
+		$("[bx_grid_action_single = 'import'][bx_grid_action_data = '" + iValue + "']").addClass('bx-btn-disabled');
+	});
+	glGrids.sys_studio_nav_items.processJson({grid: oData.parent_grid, blink: oData.parent_blink}, 'import', false);
+};
+
+BxDolStudioNavigationImport.prototype.done = function(oData) {
+	var $this = this;
+
+	$('.bx-popup-applied:visible').dolPopupHide({
+		removeOnClose: true, 
+		onHide: function() {
+			if(glGrids[$this.sObjNameGrid] != undefined)
+				glGrids[$this.sObjNameGrid] = null;
+		}
+	})
+};
+/** @} */

+ 458 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/studio/js/store.js

@@ -0,0 +1,458 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaStudio UNA Studio
+ * @{
+ */
+function BxDolStudioStore(oOptions) {
+	this.sActionsUrl = oOptions.sActionUrl;
+    this.sObjName = oOptions.sObjName == undefined ? 'oBxDolStudioStore' : oOptions.sObjName;
+    this.sAnimationEffect = oOptions.sAnimationEffect == undefined ? 'fade' : oOptions.sAnimationEffect;
+    this.iAnimationSpeed = oOptions.iAnimationSpeed == undefined ? 'slow' : oOptions.iAnimationSpeed;
+
+    this.sIdPageContent = 'bx-std-pc-content';
+    this.sIdPopupProduct = 'bx-std-str-popup-product';
+}
+
+BxDolStudioStore.prototype.addToCart = function(iVendor, iProduct, oButton) {
+	var oDate = new Date();
+	var $this = this;
+	bx_loading(this.sIdPageContent, true);
+
+	$.get(
+		this.sActionsUrl,
+		{
+			str_action: 'add-to-cart',
+			str_vendor: iVendor,
+			str_item: iProduct,
+			_t:oDate.getTime()
+		},
+		function(oData) {
+			bx_loading($this.sIdPageContent, false);
+
+			$this.showNotification(oData.message);
+
+			if(parseInt(oData.code) == 0) {
+				var oCounter = $('#bx-std-pmi-checkout .bx-std-pmen-item-counter span');
+				oCounter.html(parseInt(oCounter.html()) + 1);
+
+				if(parseInt(oCounter.html()) > 0)
+					oCounter.parent('.bx-std-pmen-item-counter').show();
+
+				$(oButton).hide().next('.bx-std-pc-checkout,.bx-std-pva-checkout').show();
+			}
+		},
+		'json'
+	);
+};
+
+BxDolStudioStore.prototype.deleteFromCart = function(iVendor, iProduct, oButton) {
+	var oDate = new Date();
+	var $this = this;
+	bx_loading(this.sIdPageContent, true);
+
+	$.get(
+		this.sActionsUrl,
+		{
+			str_action: 'delete-from-cart',
+			str_vendor: iVendor,
+			str_item: iProduct,
+			_t:oDate.getTime()
+		},
+		function(oData) {
+			bx_loading($this.sIdPageContent, false);
+
+			if(oData.message.length > 0)
+				$this.showNotification(oData.message);
+
+			var iCode = parseInt(oData.code);
+			var oCounter = $('#bx-std-pmi-checkout .bx-std-pmen-item-counter span');
+			if(iCode == 0 && iProduct != 0) {
+				var oProduct = $('#bx-std-product-' + iProduct);
+				if(oProduct.siblings('.bx-std-product').length == 0)
+					oProduct.parents('.bx-std-block:first').bx_anim('hide', 'fade', 'fast', function() {
+						$(this).remove();
+					});
+				else
+					oProduct.bx_anim('hide', 'fade', 'fast', function() {
+						$(this).remove();
+					});
+
+				oCounter.html(parseInt(oCounter.html()) - 1);
+			}
+			else if(iCode == 0 && iProduct == 0) {
+				var oBlock = $(oButton).parents('.bx-std-block:first');
+				oCounter.html(parseInt(oCounter.html()) - oBlock.find('.bx-std-product').length);
+
+				oBlock.bx_anim('hide', 'fade', 'fast', function() {
+					$(this).remove();
+				});
+			}
+
+			if(parseInt(oCounter.html()) <= 0)
+				oCounter.parent('.bx-std-pmen-item-counter').hide();
+		},
+		'json'
+	);
+};
+
+BxDolStudioStore.prototype.deleteAllFromCart = function(iVendor, oButton) {
+	this.deleteFromCart(iVendor, 0, oButton);
+};
+
+BxDolStudioStore.prototype.checkoutCart = function(iVendor, oButton) {
+	var oDate = new Date();
+	var $this = this;
+	bx_loading(this.sIdPageContent, true);
+
+	$.get(
+		this.sActionsUrl,
+		{
+			str_action: 'checkout-cart',
+			str_vendor: iVendor,
+			_t:oDate.getTime()
+		},
+		function(oData) {
+			bx_loading($this.sIdPageContent, false);
+
+			if(oData.message.length > 0)
+				$this.showNotification(oData.message);
+
+			if(parseInt(oData.code) == 0 && oData.redirect.length > 0)
+				document.location=oData.redirect;
+		},
+		'json'
+	);
+};
+
+BxDolStudioStore.prototype.subscribe = function(iVendor, iProduct, oButton) {
+	var oDate = new Date();
+	var $this = this;
+	bx_loading(this.sIdPageContent, true);
+
+	$.get(
+		this.sActionsUrl,
+		{
+			str_action: 'subscribe',
+			str_vendor: iVendor,
+			str_item: iProduct,
+			_t:oDate.getTime()
+		},
+		function(oData) {
+			bx_loading($this.sIdPageContent, false);
+
+			if(oData.message.length > 0)
+				$this.showNotification(oData.message);
+
+			if(parseInt(oData.code) == 0 && oData.redirect.length > 0)
+				document.location=oData.redirect;
+		},
+		'json'
+	);
+};
+
+BxDolStudioStore.prototype.getFile = function(iFileId, oButton) {
+	this._getFile('get-file', iFileId, '', oButton);
+};
+
+BxDolStudioStore.prototype.getUpdate = function(sModuleName, oButton) {
+	this._getFile('get-update', sModuleName, '', oButton);
+};
+
+BxDolStudioStore.prototype.getUpdateAndInstall = function(sModuleName, oButton) {
+	var $this = this;
+
+	var onResult = function(oData, oButton) {
+		if(oData.code != 0) {
+			$this._onGetFile(oData, oButton);
+			return;
+		}
+
+		bx_loading($this.sIdPageContent, false);
+
+		$(oButton).parents('.bx-std-product:first').hide();
+	};
+
+	this._getFile('get-update-and-install', sModuleName, onResult, oButton);
+};
+
+BxDolStudioStore.prototype._getFile = function(sAction, mixedId, onResult, oButton) {
+	var oDate = new Date();
+	var $this = this;
+
+	bx_loading_btn(oButton, true);
+	$(oButton).addClass('bx-btn-disabled');
+
+	$.get(
+		this.sActionsUrl,
+		{
+			str_action: sAction,
+			str_id: mixedId,
+			_t:oDate.getTime()
+		},
+		function(oData) {
+			if(typeof onResult == 'function')
+				onResult(oData, oButton);
+			else
+				$this._onGetFile(oData, oButton);
+		},
+		'json'
+	);
+};
+
+BxDolStudioStore.prototype._onGetFile = function(oData, oButton) {
+	bx_loading_btn(oButton, false);
+
+	switch(parseInt(oData.code)) {
+		case 1:
+			$(oButton).removeClass('bx-btn-disabled');
+			break;
+		case 2:
+			$(oButton).val(_t('_adm_btn_queued_submit'));
+			break;
+	}
+
+	if(oData.message)
+		this.showNotification(oData.message);
+};
+
+BxDolStudioStore.prototype.info = function(sModuleName, oLink) {
+	var oDate = new Date();
+	var $this = this;
+	bx_loading(this.sIdPageContent, true);
+
+	$.get(
+		this.sActionsUrl,
+		{
+			str_action: 'get-product',
+			str_id: sModuleName,
+			_t:oDate.getTime()
+		},
+		function(oData) {
+			bx_loading($this.sIdPageContent, false);
+
+			var sId = $this.sIdPopupProduct;
+			if(oData.code == 0 && oData.popup.length > 0) {
+		        $('#' + sId).remove();
+				$(oData.popup).appendTo('body').bxTime();
+				$('#' + sId).dolPopup({
+					onShow: function() {
+						$this.initScreenshots(oData.screenshots);
+					}
+				});
+			}
+			else
+				$this.showNotification(oData.message);
+		},
+		'json'
+	);
+};
+
+BxDolStudioStore.prototype.initScreenshots = function(iCount) {
+	var iWidth = 202;
+	var iPadding = 20;
+	var iWidthOuter = iWidth + iPadding;
+	var bBusy = false;
+
+	$(".bx-std-pv-screenshots a[rel=group]").fancybox({
+		transitionIn: 'elastic',
+		transitionOut: 'elastic',
+		speedIn: 600,
+		speedOut: 200
+	});
+	if(iCount <= 2)
+		return;
+
+	$(".bx-std-pvs-left").bind('click', function() {
+		if(bBusy || parseInt($(".bx-std-pvs-cnt").css('left')) >= 0)
+			return;
+
+		bBusy = true;
+		$(".bx-std-pvs-cnt").animate({left: '+=' + iWidthOuter}, 500, function() {
+			bBusy = false;
+		});
+	});
+	$(".bx-std-pvs-right").bind('click', function() {
+		if(bBusy || parseInt($(".bx-std-pvs-cnt").css('left')) <= 460 - $(".bx-std-pvs-cnt").width())
+			return;
+
+		bBusy = true;
+		$(".bx-std-pvs-cnt").animate({left: '-=' + iWidthOuter}, 500, function() {
+			bBusy = false;
+		});
+	});
+
+	$(".bx-std-pv-screenshots").hover(function() {
+		$(".bx-std-pvs-left, .bx-std-pvs-right").bx_anim('show', 'fade', 'fast');
+	}, function() {
+		$(".bx-std-pvs-left, .bx-std-pvs-right").bx_anim('hide', 'fade', 'fast');;
+	});
+};
+
+BxDolStudioStore.prototype.install = function(sValue, oInput) {
+	var $this = this;
+	var onSuccess = function(oData) {
+		$(oInput).parent('.bx-std-pc-buttons:first').hide(0, function() {
+			$(this).siblings('.bx-std-pcb-installed:hidden').show(0);
+		});
+	};
+
+	return this.perform('install', sValue, onSuccess, oInput);
+};
+
+BxDolStudioStore.prototype.update = function(sValue, oInput) {
+	var $this = this;
+	var onSuccess = function(oData) {
+		$(oInput).parents('.bx-std-product:first').hide();
+	};
+
+	return this.perform('update', sValue, onSuccess, oInput);
+};
+
+BxDolStudioStore.prototype.remove = function(sValue, oInput) {
+	var $this = this;
+
+	bx_confirm('', function() {
+		$this.perform('delete', sValue, function(oData) {
+			switch(parseInt(oData.code)) {
+				case 0:
+					$(oInput).parents('.bx-std-product:first').hide();
+					break;
+
+				case 2:
+					$(oInput).parent('.bx-std-pc-buttons:first').hide(0, function() {
+						$(this).siblings('.bx-std-pcb-queued:hidden').show(0);
+					});
+					break;
+			}
+	    }, oInput);
+	});
+
+    return false; 
+};
+
+BxDolStudioStore.prototype.perform = function(sType, sValue, onSuccess, oInput) {
+	var oDate = new Date();
+	var $this = this;
+
+	if(!sValue)
+        return false;
+
+	bx_loading_btn(oInput, true);
+
+    $.post(
+    	this.sActionsUrl,
+    	{
+    		str_action: sType,
+    		str_value: sValue,
+    		_t:oDate.getTime()
+    	},
+    	function (oData) {
+    		bx_loading_btn(oInput, false);
+
+    		if(oData.message.length > 0)
+    			$this.showNotification(oData.message);
+
+    		switch(parseInt(oData.code)) {
+	    		case 0:
+	    		case 2:
+	    			if(typeof onSuccess == 'function')
+	    				onSuccess(oData);
+	    			break;
+    		}
+    	},
+    	'json'
+    );
+};
+
+/**
+ * Is needed if AJAX is used to change (reload) pages. 
+ */
+BxDolStudioStore.prototype.changePage = function(sType) {
+	var oDate = new Date();
+	var $this = this;
+
+	$.get(
+		this.sActionsUrl,
+		{
+			str_action: 'get-products-by-type',
+			str_value: sType,
+			_t:oDate.getTime()
+		},
+		function(oData) {
+			if(oData.code != 0) {
+				$this.showNotification(oData.message);
+				return;
+			}
+
+			$('#bx-std-pc-menu > .bx-std-pmi-active').removeClass('bx-std-pmi-active');
+			$('#bx-std-pmi-' + sType).addClass('bx-std-pmi-active');
+
+			$('#' + $this.sIdPageContent).bx_anim('hide', $this.sAnimationEffect, $this.iAnimationSpeed, function() {
+				$(this).html(oData.content).bx_anim('show', $this.sAnimationEffect, $this.iAnimationSpeed);
+			});
+		},
+		'json'
+	);
+
+	return true;
+};
+
+BxDolStudioStore.prototype.changePagePaginate = function(oButton, sType, mixedValue, iStart, iPerPage) {
+	var oDate = new Date();
+	var $this = this;
+
+	$.get(
+		this.sActionsUrl,
+		{
+			str_action: 'get-products-by-page',
+			str_type: sType,
+			str_value: mixedValue,
+			str_start: iStart,
+			str_per_page: iPerPage,
+			_t:oDate.getTime()
+		},
+		function(oData) {
+			if(oData.code != 0) {
+				$this.showNotification(oData.message);
+				return;
+			}
+
+			$(oButton).parents('.bx-std-block-content:first').bx_anim('hide', $this.sAnimationEffect, $this.iAnimationSpeed, function() {
+				$(this).html(oData.content).bx_anim('show', $this.sAnimationEffect, $this.iAnimationSpeed);
+			});
+		},
+		'json'
+	);
+
+	return true;
+};
+
+BxDolStudioStore.prototype.showNotification = function(sContent) {
+	$(sContent).appendTo('body').dolPopup({
+    	position: 'fixed',
+    	left: '1rem',
+    	top: 'auto',
+    	bottom: '1rem',
+    	fog: false,
+    	removeOnClose: true
+    }); 
+};
+
+/**
+ * It isn't used anywhere for now.
+ */
+BxDolStudioStore.prototype.showPopup = function(sId, sContent, mixedPointer) {
+    $('#' + sId).remove();
+    $('<div id="' + sId + '" style="display: none;"></div>').appendTo('body').html(sContent);
+
+    var oParams = {};
+    if(mixedPointer)
+    	oParams.pointer = {
+    		el:$(mixedPointer)
+    	};
+
+    $('#' + sId).dolPopup(oParams);
+};
+/** @} */

+ 4 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/studio/template/_footer.html

@@ -0,0 +1,4 @@
+       __system_injection_footer__
+       <bx_injection:injection_footer />
+    </body>
+</html>

+ 69 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/studio/template/_header.html

@@ -0,0 +1,69 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
+    <head>
+        <bx_injection:injection_head_begin />
+        
+        <meta http-equiv="Content-Type" content="text/html; charset=__page_charset__" />
+        <title>__page_header__</title>
+
+        __meta_info__
+	    __page_robots__
+
+        <bx_include_css_styles />
+        <bx_include_css_system />
+        <bx_include_css />
+
+        __dol_images__
+        __dol_lang__
+        __dol_options__
+
+        <bx_include_js_system />
+        <bx_include_js />
+        <script defer type="text/javascript">
+            var sUrlRoot = '<bx_url_root />';
+            var sUrlStudio = '<bx_url_studio />';
+
+            var bUseSvgLoading = true;
+            if(!bUseSvgLoading) {
+	            var aSpinnerOpts = {
+	                lines: 7, // The number of lines to draw
+	                length: 0, // The length of each line
+	                width: 8, // The line thickness
+	                radius: 7, // The radius of the inner circle
+	                corners: 1, // Corner roundness (0..1)
+	                rotate: 0, // The rotation offset
+	                direction: 1, // 1: clockwise, -1: counterclockwise
+	                color: '#165ea1', // #rgb or #rrggbb or array of colors
+	                speed: 1, // Rounds per second
+	                trail: 60, // Afterglow percentage
+	                shadow: false, // Whether to render a shadow
+	                hwaccel: false, // Whether to use hardware acceleration
+	                className: 'bx-sys-spinner', // The CSS class to assign to the spinner
+	                zIndex: 2e9, // The z-index (defaults to 2000000000)
+	                top: '50%', // Top position relative to parent in px
+	                left: '50%' // Left position relative to parent in px
+	            };
+	            var aSpinnerSmallOpts = $.extend({}, aSpinnerOpts, {
+	                lines: 6,
+	                width: 6,
+	                radius: 4,
+	                color: '#333',
+	                top: '50%',
+	                left: 'calc(100% - 20px)'
+	            }); 
+            }
+
+			$(document).ready(function () {
+				bx_time('__lang__');
+
+                bx_activate_anim_icons('#3e8685');
+			});
+        </script>
+        __extra_js__
+
+        __system_injection_head__
+        <bx_injection:injection_head />
+    </head>
+    <body class="bx-def-font bx-def-color-bg-page bx-std-bg<bx_injection:injection_body_style />" <bx_injection:injection_body /> >
+        __system_injection_header__
+        <bx_injection:injection_header />

+ 69 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/studio/template/scripts/BxBaseStudioFunctions.php

@@ -0,0 +1,69 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaView UNA Studio Representation classes
+ * @ingroup     UnaStudio
+ * @{
+ */
+
+class BxBaseStudioFunctions extends BxBaseFunctions implements iBxDolSingleton
+{
+    function __construct($oTemplate = false)
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct($oTemplate ? $oTemplate : BxDolStudioTemplate::getInstance());
+    }
+
+    public static function getInstance()
+    {
+        if (!isset($GLOBALS['bxDolClasses']['BxBaseStudioFunctions']))
+            $GLOBALS['bxDolClasses']['BxBaseStudioFunctions'] = new BxTemplStudioFunctions();
+
+        return $GLOBALS['bxDolClasses']['BxBaseStudioFunctions'];
+    }
+
+    function getLoginForm()
+    {
+        $oTemplate = BxDolStudioTemplate::getInstance();
+
+        $sUrlRelocate = bx_get('relocate');
+        if (empty($sUrlRelocate) || basename($sUrlRelocate) == 'index.php')
+            $sUrlRelocate = '';
+
+        $sHtml = $oTemplate->parseHtmlByName('login_form.html', array (
+            'role' => BX_DOL_ROLE_ADMIN,
+            'csrf_token' => BxDolForm::genCsrfToken(true),
+            'relocate_url' => bx_html_attribute($sUrlRelocate),
+            'action_url' => BX_DOL_URL_ROOT . 'member.php',
+            'forgot_password_url' => BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=forgot-password'),
+        ));
+        $sHtml = $oTemplate->parseHtmlByName('login.html', array (
+            'form' => $this->transBox('bx-std-login-form-box', $sHtml, true),
+        ));
+
+        $oTemplate->setPageNameIndex(BX_PAGE_CLEAR);
+        $oTemplate->setPageParams(array(
+           'css_name' => array('forms.css', 'login.css'),
+           'js_name' => array('jquery-ui/jquery.ui.position.min.js', 'jquery.form.min.js', 'jquery.dolPopup.js', 'login.js'),
+           'header' => _t('_adm_page_cpt_login'),
+        ));
+        $oTemplate->setPageContent ('page_main_code', $sHtml);
+        $oTemplate->getPageCode();
+    }
+
+    protected function getInjHeadLiveUpdates() 
+    {
+        return '';
+    }
+
+    protected function getInjFooterPopupMenus() 
+    {
+        return '';
+    }
+}
+
+/** @} */

+ 172 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/studio/template/scripts/BxBaseStudioNavigationImport.php

@@ -0,0 +1,172 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaView UNA Studio Representation classes
+ * @ingroup     UnaStudio
+ * @{
+ */
+
+class BxBaseStudioNavigationImport extends BxDolStudioNavigationImport
+{
+    function __construct($aOptions, $oTemplate = false)
+    {
+        parent::__construct($aOptions, $oTemplate);
+
+        $this->_aOptions['actions_single']['import']['attr']['title'] = _t('_adm_nav_btn_items_import');
+    }
+
+    public function performActionImport()
+    {
+    	bx_import('BxDolStudioUtils');
+
+        $iAffected = 0;
+        $aIds = bx_get('ids');
+        if(!$aIds || !is_array($aIds)) {
+            echoJson(array());
+            exit;
+        }
+
+        $oLanguagesUtils = BxDolStudioLanguagesUtils::getInstance();
+
+        $aIdsAffected = array ();
+        foreach($aIds as $iId) {
+            $aItem = array();
+            $iItem = $this->oDb->getItems(array('type' => 'by_id', 'value' => (int)$iId), $aItem);
+            if($iItem != 1 || empty($aItem))
+                continue;
+
+            $mixedIcon = 0;
+            if(is_numeric($aItem['icon']) && (int)$aItem['icon'] != 0) {
+                $oStorage = BxDolStorage::getObjectInstance(BX_DOL_STORAGE_OBJ_IMAGES);
+
+                if(($mixedIcon = $oStorage->storeFileFromStorage(array('id' => (int)$aItem['icon']), false, 0)) === false) {
+                    echoJson(array('msg' => _t('_adm_nav_err_items_icon_copy') . $oStorage->getErrorString()));
+                    return;
+                }
+
+                $oStorage->afterUploadCleanup($mixedIcon, 0);
+            }
+
+            $sPostfix = time();
+
+            $iIdImported = (int)$aItem['id'];
+            unset($aItem['id']);
+            
+            $aItem['set_name'] = $this->sSet;
+            $aItem['module'] = BX_DOL_STUDIO_MODULE_CUSTOM;
+
+            $sKeyTitleSystem = '';
+            if(!empty($aItem['title_system'])) {
+                $sKeyTitleSystem = $aItem['title_system'];
+
+                $aItem['title_system'] .= '_' . $sPostfix;
+            }
+
+            $sKeyTitle = '';
+            if(!empty($aItem['title'])) {
+                $sKeyTitle = $aItem['title'];
+
+                $aItem['title'] .= '_' . $sPostfix;
+            }
+
+            $aItem['icon'] = $mixedIcon != 0 ? $mixedIcon : '';
+            $aItem['active'] = 1;
+            $aItem['order'] = $this->oDb->getItemOrderMax($this->sSet) + 1;
+
+            if(($iIdAdded = (int)$this->oDb->addItem($aItem)) == 0)
+                continue;
+
+            if(!empty($sKeyTitleSystem))
+                $oLanguagesUtils->addLanguageString($aItem['title_system'], _t($sKeyTitleSystem));
+
+            if(!empty($sKeyTitle))
+                $oLanguagesUtils->addLanguageString($aItem['title'], _t($sKeyTitle));
+
+            $aIdsImported[] = $iIdImported;
+            $aIdsAdded[] = $iIdAdded;
+            $iAffected++;
+        }
+
+        $aResult = array('msg' => _t('_adm_nav_err_items_import'));
+        if($iAffected) {
+            $oGrid = BxDolGrid::getObjectInstance('sys_studio_nav_items');
+            $oGrid->resetQueryParams();
+            if($oGrid !== false)
+                $aResult = array(
+                    'parent_grid' => $oGrid->getCode(false),
+                    'parent_blink' => $aIdsAdded,
+                    'disable' => $aIdsImported,
+                    'eval' => $this->getJsObject() . '.onImport(oData)'
+                );
+        }
+        echoJson($aResult);
+    }
+
+    function getJsObject()
+    {
+        return 'oBxDolStudioNavigationImport';
+    }
+
+    function getCode($isDisplayHeader = true)
+    {
+        return $this->_oTemplate->parseHtmlByName('nav_import.html', array(
+            'content' => parent::getCode($isDisplayHeader),
+            'js_object' => $this->getJsObject(),
+            'grid_object' => $this->_sObject,
+            'params_divider' => $this->sParamsDivider
+        ));
+    }
+
+    protected function _getCellIcon ($mixedValue, $sKey, $aField, $aRow)
+    {
+        $mixedValue = $this->_oTemplate->getIcon($mixedValue, array('class' => 'bx-nav-item-icon bx-def-border'));
+        return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellModule($mixedValue, $sKey, $aField, $aRow)
+    {
+        $mixedValue = $this->_limitMaxLength($this->getModuleTitle($aRow['module']), $sKey, $aField, $aRow, $this->_isDisplayPopupOnTextOverflow);
+        return parent::_getCellDefault($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getActionDone ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        $a['attr']['onclick'] = "javascript: " . $this->getJsObject() . ".done();";
+        return  parent::_getActionDefault($sType, $sKey, $a, false, $isDisabled, $aRow);
+    }
+
+    protected function _getFilterControls ()
+    {
+        parent::_getFilterControls();
+
+        $sJsObject = $this->getJsObject();
+        $oForm = new BxTemplStudioFormView(array());
+
+        $aInputSets = array(
+            'type' => 'select',
+            'name' => 'set',
+            'attrs' => array(
+                'id' => 'bx-grid-set-' . $this->_sObject,
+                'onChange' => 'javascript:$(this).off(\'keyup focusout\'); javascript:' . $sJsObject . '.onChangeFilter()',
+                'onBlur' => 'javascript:' . $sJsObject . '.onChangeFilter()',
+            ),
+            'value' => '',
+            'values' => array()
+        );
+
+        $aSets = $aCounter = array();
+        $this->oDb->getSets(array('type' => 'all', 'except' => $this->sSet), $aSets, false);
+        $this->oDb->getItems(array('type' => 'counter_by_sets'), $aCounter, false);
+        foreach($aSets as $aSet)
+            $aInputSets['values'][$aSet['name']] = _t($aSet['title']) . " (" . (isset($aCounter[$aSet['name']]) ? $aCounter[$aSet['name']] : "0") . ")";
+
+        asort($aInputSets['values']);
+        $aInputSets['values'] = array_merge(array('' => _t('_adm_nav_txt_select_set')), $aInputSets['values']);
+
+        return $oForm->genRow($aInputSets) . $this->getModulesSelectAll('getItems') . $this->getSearchInput();
+    }
+}
+
+/** @} */

+ 4 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/_footer.html

@@ -0,0 +1,4 @@
+        __system_injection_footer__
+       <bx_injection:injection_footer />
+    </body>
+</html>

+ 39 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/_header.html

@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html lang="__lang__">
+<head>
+    <bx_injection:injection_head_begin />
+
+    <meta http-equiv="Content-Type" content="text/html; charset=__page_charset__" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
+    <title>__page_header__</title>
+    <base href="<bx_url_root />" />
+
+    __meta_info__
+    __page_robots__
+
+    <meta http-equiv="Content-Style-Type" content="text/css" />
+
+    <bx_include_css_styles />
+    <bx_include_css_system />
+    <bx_include_css />
+
+    __dol_images__
+    __dol_lang__
+    __dol_options__
+
+    <bx_include_js_system />
+    <bx_include_js />
+   
+    <bx_include_auto:_header_js.html />
+    <bx_include_auto:_header_js_spinner.html />
+    <bx_include_auto:_header_js_toolbar.html />
+    <bx_include_auto:_header_js_custom.html />
+
+    __extra_js__
+
+    __system_injection_head__
+    <bx_injection:injection_head />
+</head>
+<body class="bx-def-font bx-def-color-bg-page bx-def-image-bg-page" dir="__lang_direction__" <bx_injection:injection_body /> >
+    __system_injection_header__
+    <bx_injection:injection_header />

+ 773 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/css/common.css

@@ -0,0 +1,773 @@
+
+* {
+    -webkit-font-smoothing: antialiased;
+    -webkit-text-size-adjust: 100%;
+    -moz-text-size-adjust: 100%;
+    -ms-text-size-adjust: 100%;
+    text-size-adjust: 100%;
+}
+
+a {
+    text-decoration:none;
+    outline:none;
+    color:#3e8685; /* 26.13.13: prev val #387877; */
+}
+
+a:hover {
+    text-decoration:none;
+}
+
+a > img {
+    border:none;
+}
+
+html {
+    height:100%;
+}
+body {
+	min-height: 100%;
+    min-height: 100vh;
+    padding: 0;
+    margin: 0;
+}
+
+blockquote {
+    /* NOTE: this style was copied to editor.css */
+    border-left: 2px solid #ccc;
+    padding-left: 10px;
+}
+
+.bx-main {
+	position: relative;
+	display: -webkit-flex;
+	display: flex;
+	-webkit-flex-direction: column;
+	flex-direction: column;
+
+	min-height: 100%;
+	min-height: 100vh;
+}
+
+.bx-clear {
+    clear:both;
+    float:none;
+    display:block;
+    width: 0px;
+    height: 0px;
+    font-size:1px;
+}
+
+.bx-clearfix:before,
+.bx-clearfix:after {
+    content:"";
+    display:table;
+}
+.bx-clearfix:after {
+    clear:both;
+}
+
+.bx-anchor-invisible,
+.bx-anchor {
+    padding-top:60px;
+    margin-top:-60px;
+    -webkit-background-clip:content-box;
+    background-clip:content-box;
+}
+.bx-anchor-invisible {
+    display:block;
+    height:0px;
+    width:0px;
+    overflow:hidden;
+}
+
+.bx-bullet:before {
+    content: "\00A0\00b7\00A0";
+
+    color: #999999;
+}
+.bx-bullet-narrow:before {
+    content: "\00b7";
+
+    color: #999999;
+}
+
+.sys-icon-a {
+    width:1.5rem;
+    height:1.5rem;
+}
+
+.bx-search-site {
+    width:20rem;
+    margin-left:auto;
+    margin-right:auto;
+}
+
+div.mce-fullscreen {
+    z-index: 1200;
+}
+
+/* --------------- design boxes */
+
+.bx-db-container {
+    position:relative;
+}
+.bx-db-header {
+    display: -webkit-box;
+    display: -webkit-flex;
+    display: -ms-flexbox;
+    display: flex;
+
+    -webkit-box-align: stretch;
+    -webkit-align-items: stretch;
+    -ms-flex-align: stretch;
+    align-items: stretch;
+
+    -webkit-flex-wrap: nowrap;
+    -ms-flex-wrap: nowrap;
+    flex-wrap: nowrap;
+}
+.bx-db-title {
+    -webkit-flex: 1 0 auto;
+    -ms-flex: 1 0 auto;
+    flex: 1 0 auto;
+
+    line-height:40px;
+}
+
+.bx-db-menu {
+	-webkit-flex: 0 1 auto;
+    -ms-flex: 0 1 auto;
+    flex: 0 1 auto;
+    
+    line-height:40px;
+}
+
+.bx-db-menu-tab {
+    float: left;
+    margin-left:5px;
+}
+
+.bx-db-menu-tab-active {
+    text-decoration:underline;
+}
+
+.bx-db-menu-tab-btn a {
+    text-decoration:none;
+}
+
+.bx-db-menu .sys-icon-a {
+    vertical-align:middle;
+}
+
+/* --------------- popup boxes */
+
+html.bx-popup-lock, body.bx-popup-lock {
+    overflow: hidden;
+}
+
+body > .bx-popup-wrapper {
+    max-height:100%;
+    overflow-x:hidden;
+    overflow-y:auto;
+    /* -webkit-overflow-scrolling:touch; */
+}
+
+.bx-popup-transitions {
+    -webkit-transition: opacity 0.2s ease-in-out, -webkit-transform 0.2s ease-in-out;
+    transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
+}
+
+.bx-popup-inactive {
+    -webkit-opacity: 0;
+    opacity: 0;
+
+    -webkit-transform: scale(0.75);
+    transform: scale(0.75);
+}
+
+.bx-popup-active {
+    max-height:100%;
+    overflow-y:auto;
+
+    -webkit-opacity: 1;
+    opacity: 1;
+
+    -webkit-transform: scale(1);
+    transform: scale(1);
+
+    -webkit-transition: opacity 0.2s ease-in-out, -webkit-transform 0.2s ease-in-out;
+    transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
+}
+
+.bx-popup-width {
+	width: 500px;
+}
+
+.bx-popup-border {
+	border:1px solid #383d66;
+}
+
+.bx-popup-color-bg {
+    background-color:#fff;
+}
+
+.bx-popup {    
+    overflow:hidden;
+
+	-moz-box-shadow:0px 0px 0px 1px rgba(255, 255, 255, 0.15), 0px 10px 25px 0px rgba(33, 33, 33, 0.5);
+	-webkit-box-shadow:0px 0px 0px 1px rgba(255, 255, 255, 0.15), 0px 10px 25px 0px rgba(33, 33, 33, 0.5);
+    box-shadow:0px 0px 0px 1px rgba(255, 255, 255, 0.15), 0px 10px 25px 0px rgba(33, 33, 33, 0.5);
+}
+
+.bx-popup-header {
+    position: relative;
+}
+.bx-popup-header-color-bg {
+	background-color: #3b8686;
+}
+
+.bx-popup-header .bx-db-menu-tab-btn {
+    padding-top:16px;
+}
+
+.bx-popup-header h2 {
+    margin:0;
+    text-align:center;
+    color:#FFFFFF;
+}
+
+.bx-popup-box-pointer {
+    display:none;
+    position:relative;
+    top:10px;
+    width:13px;
+    height:7px;
+    overflow:hidden;
+}
+
+.bx-popup-box-pointer > i {
+    display:block;
+    position:absolute;
+    top:-5px;
+    width:0px;
+    height:0px;
+    border-width:6px;
+    border-style:solid;
+    border-top-color:transparent;
+    border-right-color:transparent;
+    border-left-color:transparent;
+}
+
+.bx-popup-image-container {
+    position: relative;
+}
+
+.bx-popup-image-btn {
+    position: absolute;
+    right: 0;
+    top: 0;
+}
+
+.bx-popup-image-wrapper img {
+    display: block;
+}
+
+.bx-popup-element-close:hover,
+.bx-popup-element-close {
+    position:absolute;
+    top:0;
+    right:0;
+    text-decoration:none;
+    line-height:1.8rem;
+}
+
+.bx-popup-slide-wrapper {
+	width:100%;
+
+	background-color: rgba(255, 255, 255, 0.9);
+}
+
+.bx-popup-slide-wrapper li {
+    text-align:right;
+}
+
+.bx-popup-image-wrapper .bx-popup-close-wrapper,
+.bx-popup-image-wrapper .bx-popup-element-close {
+    display:none;
+}
+
+#bx-popup-alert,
+#bx-popup-confirm,
+#bx-popup-prompt {
+	max-width: 500px;
+	max-width: 31.25rem;
+}
+
+.popup_alert_actions .bx-btn,
+.popup_confirm_actions .bx-btn,
+.popup_prompt_actions .bx-btn {
+    float: none;
+    display: inline-block;
+}
+
+/* --------------- full screen popup */
+.bx-popup-full-screen.bx-popup-trans-wrapper,
+.bx-popup-full-screen.bx-popup-box-wrapper,
+.bx-popup-full-screen .bx-popup-trans,
+.bx-popup-full-screen .bx-popup-box {
+    position:fixed !important;
+    top:0 !important;
+    left:0 !important;
+    bottom:0 !important;
+    right:0 !important;
+    width:100%;
+    width:100vw;
+    height:100%;
+    height:100vh;
+    overflow:auto;
+}
+
+.bx-popup-full-screen .bx-popup-trans > div,
+.bx-popup-full-screen .bx-popup-box > div {
+    border-radius:0;
+    -moz-border-radius:0;
+    -webkit-border-radius:0;
+}
+
+.bx-popup-full-screen.bx-popup-trans-wrapper .bx-popup-border,
+.bx-popup-full-screen.bx-popup-box-wrapper .bx-popup-border {
+    border:none;
+}
+
+.bx-popup-full-screen.bx-popup-trans-wrapper .bx-popup,
+.bx-popup-full-screen.bx-popup-box-wrapper .bx-popup {
+    -moz-box-shadow:none;
+    -webkit-box-shadow:none;
+    box-shadow:none;
+}
+
+.bx-popup-full-screen .bx-popup-width {
+    width:100%;
+}
+
+.bx-popup-full-screen .bx-popup .bx-popup-close-wrapper {
+	display:block !important;
+}
+
+/* --------------- notification boxes */
+.bx-popup-inline-holder {
+	padding-top: 70px;
+   	padding-top: 4.375rem;
+}
+.bx-popup-inline {
+	width: 100%;
+}
+.bx-popup-inline .bx-popup {
+	position: relative;
+
+	width: 100%;
+	height: 68px;
+    height: 4.25rem;
+
+    border-width: 1px 0px 1px 0px;
+	border-color: #000000;
+	border-radius: 0px;
+
+	background-color: rgba(85, 85, 85, .9);
+
+	color: #ffffff;
+}
+.bx-popup-inline .bx-popup .bx-popup-content {
+	position: relative;
+	display: flex;
+
+	height: 100%;
+
+	padding: 0px 60px;
+	padding: 0px 3.75rem;
+
+	align-items: center; 
+}
+.bx-popup-inline .bx-popup .bx-popup-content > * {
+	max-height: 100%; 
+}
+.bx-popup-inline .bx-popup .bx-popup-actions {
+	position: absolute;
+	display: flex;
+
+	height: 100%;
+	top: 0px;
+
+	align-items: center;
+}
+.bx-popup-inline .bx-popup .bx-pa-left {
+	left: 0px;
+}
+.bx-popup-inline .bx-popup .bx-pa-right {
+	right: 0px;
+}
+.bx-popup-inline .bx-popup .bx-popup-actions a {
+	text-decoration: none;
+	font-size: 28px;
+	font-size: 1.75rem;
+	color: #9c9c9c;
+}
+
+/* --------------- chain notifications */
+div.bx-popup-chain {
+    position: relative;
+    max-width: 22.5rem;
+}
+div.bx-popup-chain-item {
+    position: relative;
+
+    width: 100%;
+
+    border-color: rgba(0, 0, 0, 0.2);
+    background-color: #ffffff;
+
+    box-shadow: 1px 1px 5px 0px rgba(0, 0, 0, 0.2);
+}
+div.bx-popup-chain-item-cnt {
+	padding: 1.25rem;
+}
+div.bx-popup-chain-item-actions {
+    position: absolute;
+    top: 0px;
+}
+div.bx-popup-cia-left {
+    left: 0px;
+}
+div.bx-popup-cia-right {
+    right: 0px;
+}
+div.bx-popup-chain-item-actions a {
+    line-height: initial;
+    text-decoration: none;
+}
+div.bx-popup-chain-item-actions .sys-icon {
+	display: inline-block;
+	width: 1.5rem;
+	height: 1.5rem;
+	line-height: 1.5rem;
+	text-align: center;
+}
+
+/* --------------- message box */
+.bx-msg-box-container {
+    padding:0px 5px;
+    text-align:center;
+}
+
+.bx-msg-box {
+    display:inline-block; 
+    margin-right:auto;
+    margin-left:auto;
+
+    text-align:center;
+    font-size:1rem;
+    padding:10px;
+    text-transform:uppercase;
+}
+
+/* --------------- loading indicator */
+
+.bx-loading-ajax {
+    position:absolute;
+    width:100px;
+    height:100px;
+    margin-left:auto;
+    margin-right:auto;
+
+    background-color: transparent;
+
+	border-radius:10px;
+	-moz-border-radius:10px;
+	-webkit-border-radius:10px;
+
+    text-align:center;
+    color:#CCCCCC;
+}
+.bx-loading-ajax.static {
+	position: static;
+}
+.bx-loading-ajax svg {
+	width: 50px;
+	height: 50px;
+	padding: 25px;  
+}
+
+.bx-loading-ajax-btn {
+	position: relative;
+    display: inline-block;
+
+    vertical-align: top;
+
+    margin-left: 5px;
+}
+
+@-moz-document url-prefix() {
+	button.bx-btn .bx-loading-ajax-btn {
+		top: -1px;
+	}
+}
+
+button.bx-btn:focus,
+button.bx-btn:focus .bx-loading-ajax-btn {
+    outline: 0;	
+}
+
+
+/* --------------- SVG loading indicator */
+svg .outer, 
+svg .inner{
+	-webkit-transform-origin: 50% 50%;
+   	-moz-transform-origin: 50% 50%;
+    transform-origin: 50% 50%;
+}
+svg .outer{
+	-webkit-animation: clockwise 5s infinite linear;
+	-moz-animation: clockwise 5s infinite linear;
+    animation: clockwise 5s infinite linear;
+    
+
+}
+svg .inner{
+	-webkit-animation: counter-clockwise 2s infinite linear;
+	-moz-animation: counter-clockwise 2s infinite linear;
+    animation: counter-clockwise 2s infinite linear;
+    
+}
+svg .colored .inner .p1 {
+	fill:#2AB191;
+}
+svg .colored .inner .p2,
+svg .colored .outer .p1 {
+	fill:#4990E2;
+}
+svg .colored .outer .p2 {
+	fill:#EE6666;
+}
+svg .gray .inner .p1,
+svg .gray .inner .p2,
+svg .gray .outer .p1,
+svg .gray .outer .p2 {
+	fill:#4A4A4A;
+}
+
+@-webkit-keyframes counter-clockwise {
+    0% { -webkit-transform: rotate(0deg); }
+    100% { -webkit-transform: rotate(360deg); }
+}
+@-webkit-keyframes clockwise {
+    0% { -webkit-transform: rotate(360deg); }
+    100% { -webkit-transform: rotate(0deg); }
+}	
+@-moz-keyframes counter-clockwise {
+    0% { -moz-transform: rotate(0deg); }
+    100% { -moz-transform: rotate(360deg); }
+}
+@-moz-keyframes clockwise {
+    0% { -moz-transform: rotate(360deg); }
+    100% { -moz-transform: rotate(0deg); }
+}
+
+@keyframes counter-clockwise {
+    0% { transform: rotate(0deg); }
+    100% { transform: rotate(360deg); }
+}
+@keyframes clockwise {
+    0% { transform: rotate(360deg); }
+    100% { transform: rotate(0deg); }
+}
+	
+/* --------------- button */
+
+.bx-btn {
+    position:relative;
+	display:block;	
+    float:left;
+
+    -moz-box-sizing: border-box;
+    -ms-box-sizing: border-box;
+    box-sizing: border-box;
+
+    height:36px;
+    height:2.2rem;
+    margin:0;
+    padding:0px 12px 0px 12px;
+
+    background-color: #6caa8a;
+
+    border:none;
+	border-radius:3px;
+	-moz-border-radius:3px;
+	-webkit-border-radius:3px;	
+
+    -webkit-transition: background-color 0.2s ease-in-out;
+    -moz-transition: background-color 0.2s ease-in-out;
+    -o-transition: background-color 0.2s ease-in-out;
+    transition: background-color 0.2s ease-in-out;
+
+    line-height:36px;
+    line-height:2.2rem;
+    text-decoration:none;
+    cursor: pointer;
+    font-size:1rem;
+    color:#fff;
+}
+.bx-btn-height {
+	height: 36px;
+    height: 2.2rem;
+    line-height: 36px;
+    line-height: 2.2rem;
+}
+.bx-btn-border {
+	border: none;
+}
+.bx-btn:active,
+.bx-btn:hover {	
+    background-color: #79bd9a;
+    text-decoration:none;
+}
+button.bx-btn,
+input.bx-btn {
+	padding-top:0px;
+	padding-bottom:0px;    
+}
+
+.bx-btn img {
+    width:1.2em;
+    height:1.2em;
+    vertical-align:middle;
+    background-size:1.2em 1.2em;
+    background-position:center center;
+}
+.bx-btn u {
+    margin-left: 10px;
+    text-decoration: none;
+}
+.bx-btn u:first-child {
+	margin-left: 0px;
+}
+.bx-btn .sys-icon {
+    font-size:1.2em;
+}
+.bx-btn .sys-icon-bigger {
+    font-size:1.4em;
+}
+
+.bx-btn.bx-btn-small {
+    height:24px; 
+    height:1.6rem; 
+    padding-left:0px 8px 0px 8px;
+
+    line-height:24px;
+    line-height:1.5rem;
+    font-size:0.9rem;
+}
+
+.bx-btn-small-height {
+	height: 24px; 
+    height: 1.6rem;
+    line-height: 24px;
+    line-height: 1.5rem;
+}
+
+.bx-btn-small-border {
+	border: none;
+}
+
+.bx-btn.bx-btn-primary {
+    background-color: #357877;
+}
+.bx-btn.bx-btn-primary:hover,
+.bx-btn.bx-btn-primary:active { 
+    background-color: #3a8686;
+}
+
+.bx-btn.bx-btn-disabled:active,
+.bx-btn.bx-btn-disabled:hover,
+.bx-btn.bx-btn-disabled,
+.bx-btn.bx-btn-small.bx-btn-disabled:active,
+.bx-btn.bx-btn-small.bx-btn-disabled:hover,
+.bx-btn.bx-btn-small.bx-btn-disabled,
+.bx-btn.bx-btn-primary.bx-btn-disabled:active,
+.bx-btn.bx-btn-primary.bx-btn-disabled:hover,
+.bx-btn.bx-btn-primary.bx-btn-disabled {
+    background-color: #ddd;
+}
+
+.bx-btn.bx-btn-symbol {
+    font-family: "Times New Roman", Georgia, Serif;
+    font-size:1.3rem;
+    line-height:32px;
+}
+.bx-btn.bx-btn-symbol:hover {
+    text-decoration:none;
+}
+.bx-btn.bx-btn-symbol-small {
+    font-size:1rem;
+}
+
+.bx-btn-group .bx-btn {
+    margin-left:0px;
+    margin-right:0px;
+}
+.bx-btn-group .bx-btn:not(:first-child):after {
+    content:"";
+    position:absolute;
+    display:block;
+    top:0px;
+    left:-0.05em;
+    width:0.1em;
+    height:100%;
+    background-color:rgba(255,255,255,0.1);
+}
+.bx-btn-group .bx-btn + .bx-btn:not(:last-child) {
+	border-radius:0px;
+	-moz-border-radius:0px;
+	-webkit-border-radius:0px;    
+}
+.bx-btn-group .bx-btn:first-child {
+    -webkit-border-top-right-radius: 0px;
+    -webkit-border-bottom-right-radius: 0px;
+    -moz-border-radius-topright: 0px;
+    -moz-border-radius-bottomright: 0px;
+    border-top-right-radius: 0px;
+    border-bottom-right-radius: 0px;
+}
+.bx-btn-group .bx-btn:last-child {
+    -webkit-border-top-left-radius: 0px;
+    -webkit-border-bottom-left-radius: 0px;
+    -moz-border-radius-topleft: 0px;
+    -moz-border-radius-bottomleft: 0px;
+    border-top-left-radius: 0px;
+    border-bottom-left-radius: 0px;
+}
+
+/* --------------- button at the end of cut string */
+
+.bx-str-limit {
+    height:10px;
+    width:14px;    
+    margin-left:3px;
+
+	border-radius:3px;
+	-moz-border-radius:3px;
+	-webkit-border-radius:3px;
+    border-collapse:separate;
+
+	-moz-box-shadow:0px 0px 1px rgba(92, 92, 92, 0.8);
+	-webkit-box-shadow:0px 0px 1px rgba(92, 92, 92, 0.8);
+    box-shadow:0px 0px 1px rgba(92, 92, 92, 0.8);
+
+    cursor:pointer;
+}
+
+/* --------------- categories */
+
+.bx-category-list-item {
+	line-height: 24px;
+	line-height: 1.5rem;
+}

+ 622 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/css/default.less

@@ -0,0 +1,622 @@
+@import '_mixins.less';
+
+.bx-def-page-width {
+    width:@bx-page-width;
+}
+
+.bx-def-box-sizing {
+    .box-sizing(border-box);
+}
+
+/* margins */
+.bx-def-centered {
+	margin: 0px auto;
+}
+
+.bx-def-margin { 
+    .margin(@bx-margin);
+}
+.bx-def-margin-neg { 
+    .margin(-@bx-margin);
+}
+.bx-def-margin-lefttopright {
+    .margin-left(@bx-margin);
+    .margin-top(@bx-margin);
+    .margin-right(@bx-margin);
+}
+.bx-def-margin-rightbottomleft {
+    .margin-right(@bx-margin);
+    .margin-bottom(@bx-margin);
+    .margin-left(@bx-margin);
+}
+.bx-def-margin-leftright {
+    .margin-left(@bx-margin);
+    .margin-right(@bx-margin);
+}
+.bx-def-margin-leftright-neg {
+    .margin-left(-@bx-margin);
+    .margin-right(-@bx-margin);
+}
+.bx-def-margin-topbottom {
+    .margin-top(@bx-margin);
+    .margin-bottom(@bx-margin);
+}
+.bx-def-margin-topbottom-neg {
+    .margin-top(-@bx-margin);
+    .margin-bottom(-@bx-margin);
+}
+.bx-def-margin-top {
+    .margin-top(@bx-margin);
+}
+.bx-def-margin-top-auto {
+    .margin-top(@bx-margin);
+    &:first-child {
+        .margin-top(0px);
+    }
+}
+.bx-def-margin-right {
+    .margin-right(@bx-margin);
+}
+.bx-def-margin-bottom {
+    .margin-bottom(@bx-margin);
+}
+.bx-def-margin-bottom-neg {
+    .margin-bottom(-@bx-margin);
+}
+.bx-def-margin-left {
+    .margin-left(@bx-margin);
+}
+.bx-def-margin-left-auto {
+    .margin-left(@bx-margin);
+    &:first-child {
+        .margin-left(0);
+    }
+}
+.bx-def-margin-right-auto {
+    .margin-right(@bx-margin);
+    &:last-child {
+        .margin-right(0);
+    }
+}
+
+.bx-def-margin-sec {
+    .margin(@bx-margin-sec);
+}
+.bx-def-margin-sec-neg {
+    .margin(-@bx-margin-sec);
+}
+.bx-def-margin-sec-lefttopright {
+	.margin-left(@bx-margin-sec);
+    .margin-top(@bx-margin-sec);
+    .margin-right(@bx-margin-sec);
+}
+.bx-def-margin-sec-lefttopright-neg {
+	.margin-left(-@bx-margin-sec);
+    .margin-top(-@bx-margin-sec);
+    .margin-right(-@bx-margin-sec);
+}
+.bx-def-margin-sec-bottom-neg {
+    .margin-bottom(-@bx-margin-sec);
+}
+.bx-def-margin-sec-rightbottomleft {
+    .margin-right(@bx-margin-sec);
+    .margin-bottom(@bx-margin-sec);
+    .margin-left(@bx-margin-sec);
+}
+.bx-def-margin-sec-leftright {
+    .margin-left(@bx-margin-sec);
+    .margin-right(@bx-margin-sec);
+}
+.bx-def-margin-sec-leftright-neg {
+    .margin-left(-@bx-margin-sec);
+    .margin-right(-@bx-margin-sec);
+}
+.bx-def-margin-sec-topbottom {
+    .margin-top(@bx-margin-sec);
+    .margin-bottom(@bx-margin-sec);
+}
+.bx-def-margin-sec-top {
+    .margin-top(@bx-margin-sec);
+}
+.bx-def-margin-sec-top-auto {
+    .margin-top(@bx-margin-sec);
+    &:first-child {
+        .margin-top(0);
+    }
+}
+.bx-def-margin-sec-right {
+    .margin-right(@bx-margin-sec);
+}
+.bx-def-margin-sec-bottom {
+    .margin-bottom(@bx-margin-sec);
+}
+.bx-def-margin-sec-left {
+    .margin-left(@bx-margin-sec);
+}
+.bx-def-margin-sec-left-auto {
+    .margin-left(@bx-margin-sec);
+    &:first-child {
+        .margin-left(0);
+    }
+}
+.bx-def-margin-sec-right-auto {
+    .margin-left(@bx-margin-sec);
+    &:last-child {
+        .margin-left(0);
+    }
+}
+
+.bx-def-margin-thd {
+    .margin(@bx-margin-thd);
+}
+.bx-def-margin-thd-neg {
+    .margin(-@bx-margin-thd);
+}
+.bx-def-margin-thd-rightbottomleft {
+    .margin-right(@bx-margin-thd);
+    .margin-bottom(@bx-margin-thd);
+    .margin-left(@bx-margin-thd);
+}
+.bx-def-margin-thd-leftright {
+    .margin-left(@bx-margin-thd);
+    .margin-right(@bx-margin-thd);
+}
+.bx-def-margin-thd-leftright-neg {
+	.margin-left(-@bx-margin-thd);
+    .margin-right(-@bx-margin-thd);
+}
+.bx-def-margin-thd-topbottom {
+    .margin-top(@bx-margin-thd);
+    .margin-bottom(@bx-margin-thd);
+}
+.bx-def-margin-thd-top {
+    .margin-top(@bx-margin-thd);
+}
+.bx-def-margin-thd-top-auto {
+    .margin-top(@bx-margin-thd);
+    &:first-child {
+        .margin-top(0);
+    }
+}
+.bx-def-margin-thd-right {
+    .margin-right(@bx-margin-thd);
+}
+.bx-def-margin-thd-bottom {
+    .margin-bottom(@bx-margin-thd);
+}
+.bx-def-margin-thd-left {
+    .margin-left(@bx-margin-thd);
+}
+.bx-def-margin-thd-left-auto {
+    .margin-left(@bx-margin-thd);
+    &:first-child {
+        .margin-left(0);
+    }
+}
+
+/* paddings */
+.bx-def-padding {
+    .padding(@bx-padding);
+}
+.bx-def-padding-lefttopright {
+    .padding-left(@bx-padding);
+    .padding-top(@bx-padding);
+    .padding-right(@bx-padding);
+}
+.bx-def-padding-rightbottomleft {
+    .padding-right(@bx-padding);
+    .padding-bottom(@bx-padding);
+    .padding-left(@bx-padding);
+}
+.bx-def-padding-leftright {
+    .padding-left(@bx-padding);
+    .padding-right(@bx-padding);
+}
+.bx-def-padding-topbottom {
+    .padding-top(@bx-padding);
+    .padding-bottom(@bx-padding);
+}
+.bx-def-padding-top {
+    .padding-top(@bx-padding);
+}
+.bx-def-padding-right {
+    .padding-right(@bx-padding);
+}
+.bx-def-padding-bottom {
+    .padding-bottom(@bx-padding);
+}
+.bx-def-padding-left {
+    .padding-left(@bx-padding);
+}
+
+.bx-def-padding-sec {
+    .padding(@bx-padding-sec);
+}
+.bx-def-padding-sec-rightbottomleft {
+    .padding-right(@bx-padding-sec);
+    .padding-bottom(@bx-padding-sec);
+    .padding-left(@bx-padding-sec);
+}
+.bx-def-padding-sec-topbottom {
+    .padding-top(@bx-padding-sec);
+    .padding-bottom(@bx-padding-sec);
+}
+.bx-def-padding-sec-leftright {
+    .padding-left(@bx-padding-sec);
+    .padding-right(@bx-padding-sec);
+}
+.bx-def-padding-sec-top {
+    .padding-top(@bx-padding-sec);
+}
+.bx-def-padding-sec-right {
+    .padding-right(@bx-padding-sec);
+}
+.bx-def-padding-sec-bottom {
+    .padding-bottom(@bx-padding-sec);
+}
+.bx-def-padding-sec-left {
+    .padding-left(@bx-padding-sec);
+}
+
+.bx-def-padding-thd {
+    .padding(@bx-padding-thd);
+}
+.bx-def-padding-thd-rightbottomleft {
+	.padding-right(@bx-padding-thd);
+	.padding-bottom(@bx-padding-thd);
+    .padding-left(@bx-padding-thd);
+}
+.bx-def-padding-thd-topbottom {
+    .padding-top(@bx-padding-thd);
+    .padding-bottom(@bx-padding-thd);
+}
+.bx-def-padding-thd-leftright {
+    .padding-left(@bx-padding-thd);
+    .padding-right(@bx-padding-thd);
+}
+.bx-def-padding-thd-top {
+    .padding-top(@bx-padding-thd);
+}
+.bx-def-padding-thd-right {
+    .padding-right(@bx-padding-thd);
+}
+.bx-def-padding-thd-bottom {
+    .padding-bottom(@bx-padding-thd);
+}
+.bx-def-padding-thd-left {
+    .padding-left(@bx-padding-thd);
+}
+
+/* colors */
+.bx-def-color-bg-page {
+    .background-color (@bx-color-page);
+}
+.bx-def-color-bg-block {
+    .background-color (@bx-color-block);
+}
+.bx-def-color-bg-box {
+    .background-color (@bx-color-box);
+}
+.bx-def-color-bg-box-hover:hover {
+    .background-color (@bx-color-box-hover);
+}
+.bx-def-color-bg-sec {
+    .background-color (@bx-color-sec);
+}
+
+.bx-def-color-bg-hl,
+.bx-def-color-bg-hl-even:nth-child(even),
+.bx-def-color-bg-hl-hover:hover {
+    .background-color (@bx-color-hl);
+}
+
+.bx-def-color-bg-active {
+    .background-color (@bx-color-active);
+}
+
+/* hr */
+.bx-def-hr,
+.bx-def-hr-auto {
+    margin-left:0;
+    margin-right:0;
+    padding:0;
+    border:0px;
+    border-top:@bx-border-width @bx-border-type @bx-border-color;
+}
+.bx-def-hr-auto:first-child {
+	display: none;
+}
+
+/* border for common layout elements likedesign boxes, pads, menu items, etc */
+.bx-def-border {
+    border:@bx-border-width @bx-border-type @bx-border-color;
+}
+.bx-def-border-rightbottomleft {
+    border-right:@bx-border-width @bx-border-type @bx-border-color;
+    border-bottom:@bx-border-width @bx-border-type @bx-border-color;
+    border-left:@bx-border-width @bx-border-type @bx-border-color;
+}
+.bx-def-border-top {
+    border-top:@bx-border-width @bx-border-type @bx-border-color;
+}
+.bx-def-border-top-auto {
+    border-top:@bx-border-width @bx-border-type @bx-border-color;
+    &:first-child {
+        border-top:none;
+    }
+}
+.bx-def-border-right {
+    border-right:@bx-border-width @bx-border-type @bx-border-color;
+}
+.bx-def-border-bottom {
+    border-bottom:@bx-border-width @bx-border-type @bx-border-color;
+}
+.bx-def-border-left {
+    border-left:@bx-border-width @bx-border-type @bx-border-color;
+}
+.bx-def-border-right {
+    border-right:@bx-border-width @bx-border-type @bx-border-color;
+}
+
+/* shadow */
+.bx-def-shadow {
+    .drop-shadow (0px, 1px, 3px, 0px, rgba(51, 51, 51, 0.1));
+}
+
+/* round corders */
+.bx-def-round-corners {
+    .radius(@bx-round-corners-radius);
+}
+
+/* blur */
+.bx-def-blur {
+    .blur(2px);
+}
+
+/* fonts */
+
+/* NOTE: bx-def-font, h1, h2, h3 styles were copied to editor.css */
+
+.bx-def-font {
+    font-family:@bx-font-family;
+    font-weight:400; /* 26.13.13: prev val 300 */
+    .font-size(@bx-font-size-default);
+    color:@bx-font-color-default;
+}
+
+.bx-def-font-inputs {
+    font-family:@bx-font-family;
+    font-weight:400; /* 27.13.13: prev val 300 */
+    .font-size(@bx-font-size-default);
+    /* TODO: Remove 'color' instruction later. It affects the font color in Buttons which is usually differs from text color in inputs. */
+    /*color:@bx-font-color-default; */ 
+}
+
+.bx-def-font-color,
+.bx-def-font-color:hover {
+	color:@bx-font-color-default;
+}
+
+.bx-def-font-grayed {
+    color:@bx-font-color-grayed;
+}
+
+.bx-def-font-contrasted:after,
+.bx-def-font-contrasted:before,
+.bx-def-font-contrasted {
+    color:@bx-font-color-contrasted;
+}
+
+.bx-def-font-small {
+    .font-size(@bx-font-size-small);
+}
+
+.bx-def-font-middle {
+    .font-size(@bx-font-size-middle);
+}
+
+.bx-def-font-large {
+    .font-size(@bx-font-size-large);
+}
+
+.bx-def-font-light {
+	font-weight: 300;
+}
+
+.bx-def-font-normal {
+	font-weight: 400;
+}
+
+.bx-def-font-semibold {
+	font-weight: 600;
+}
+
+.bx-def-font-bold {
+	font-weight: 700;
+}
+
+.bx-def-font-extrabold {
+	font-weight: 800;
+}
+
+.bx-def-font-align-center {
+	text-align: center;
+}
+
+h1,
+.bx-def-font-h1 {
+    .font-size(@bx-font-size-h1);
+    font-weight:700;
+}
+
+h2,
+.bx-def-font-h2 {
+    .font-size(@bx-font-size-h2);
+    font-weight:700;
+}
+
+h3,
+.bx-def-font-h3 {
+    .font-size(@bx-font-size-h3);
+    font-weight:500;
+}
+
+/* image/thumbnail/gallery default styles */
+
+.bx-def-ava,
+.bx-def-thumb,
+.bx-def-icon {
+    .radius(80px);
+    overflow:hidden;
+}
+
+
+.bx-def-ava-size {
+    .width(@bx-size-avatar);
+    .height(@bx-size-avatar);
+    .line-height(@bx-size-avatar);
+}
+.bx-def-ava-size-max-height {
+    .max-height(@bx-size-avatar);
+}
+.bx-def-ava-size-min-height {
+    .min-height(@bx-size-avatar);
+}
+.bx-def-ava-size-left-indent {
+    .margin-left(@bx-size-avatar + @bx-margin);
+}
+
+
+.bx-def-thumb-size {
+    .width(@bx-size-thumb);
+    .height(@bx-size-thumb);
+    .line-height(@bx-size-thumb);
+}
+.bx-def-thumb-size-max-height {
+    .max-height(@bx-size-thumb);
+}
+.bx-def-thumb-size-min-height {
+    .min-height(@bx-size-thumb);
+}
+.bx-def-thumb-size-left-indent {
+	.margin-left(@bx-size-thumb + @bx-margin);
+}
+
+.bx-def-icon-size {
+    .width(@bx-size-icon);
+    .height(@bx-size-icon);
+}
+.bx-def-icon-size-max-height {
+    .max-height(@bx-size-icon);
+}
+.bx-def-icon-size-min-height {
+    .min-height(@bx-size-icon);
+}
+.bx-def-icon-size-left-indent {
+    .margin-left(@bx-size-icon + @bx-margin);
+}
+
+
+.bx-def-unit {
+    position:relative;
+    white-space:nowrap;
+}
+.bx-def-unit .bx-def-unit-info,
+.bx-def-unit .bx-def-unit-img,
+.bx-def-unit img {
+    display:inline;
+    display:inline-block;
+    vertical-align:middle;
+    .line-height(19px);
+}
+.bx-def-unit.absolute .bx-def-unit-img {
+    position: absolute;
+    top: 0px;
+    left: 0px;
+}
+.bx-def-unit.absolute .bx-def-unit-info {
+    display: -webkit-flex;
+    display: flex;
+    -webkit-align-items: center;
+    align-items: center;
+}
+.bx-def-unit.absolute .bx-def-unit-info-cnt {
+    width: 100%;
+}
+.bx-def-unit-info {
+    .box-sizing(border-box);
+
+    text-overflow:ellipsis;
+    overflow:hidden;
+}
+.bx-def-unit.absolute .bx-def-unit-line {
+    text-overflow:ellipsis;
+    overflow:hidden;
+}
+.bx-def-unit-alert-middle,
+.bx-def-unit-alert {
+    display:block;
+    position:absolute;
+    right:0;
+    bottom:0;
+    .min-width(@bx-font-size-small + 5);
+    .height(@bx-font-size-small + 5);
+    .line-height(@bx-font-size-small + 5);
+    .radius(@bx-font-size-small);
+    .border (1px, solid, #fff);
+    .font-size(@bx-font-size-small);
+    text-align:center;
+    color:white;
+}
+.bx-def-unit-alert-middle {
+    .min-width(@bx-font-size-middle + 2);
+    .height(@bx-font-size-middle + 2);
+    .line-height(@bx-font-size-middle + 2);
+    .radius(@bx-font-size-middle);
+    .font-size(@bx-font-size-middle);
+    .padding(3px);
+}
+
+
+.bx-def-gal-img-column {
+    .column-width-raw(100%);
+}
+
+.bx-def-gal-img-size {
+    .width(@bx-size-gallery-img-width);
+    .height(@bx-size-gallery-img-height);
+}
+
+.bx-def-gal-div-size {
+    width:100%;
+    .height(@bx-size-gallery-img-height);
+}
+.bx-def-gal-unit-column {
+    .column-width-raw(100%);
+}
+.bx-def-gal-unit-size {
+    display:inline-block;
+    width:100%; 
+    height:auto;
+    vertical-align:top;
+    overflow:hidden;
+    .box-sizing();
+}
+
+/* z-index */
+
+.bx-def-z-index-front {
+    z-index:100;
+}
+
+.bx-def-z-index-nav {
+    z-index:400;
+}
+
+.bx-def-z-index-overlay {
+    z-index:700;
+}
+
+.bx-def-z-index-modal {
+    z-index:1000;
+}

+ 156 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/css/media-phone.css

@@ -0,0 +1,156 @@
+@media (max-width:720px) {
+
+html {
+    font-size:85%;
+}
+
+.bx-def-media-phone-hide {
+    display:none;
+}
+.bx-def-media-phone-show-block {
+    display:block;
+}
+
+
+.bx-def-constraint-all *,
+.bx-def-constraint-list ol, .bx-def-constraint-list ul,
+.bx-def-constraint-pre pre,
+.bx-def-constraint-object object,
+.bx-def-constraint-iframe iframe,
+.bx-def-constraint-img img {
+    -moz-box-sizing: border-box;
+    -ms-box-sizing: border-box;
+    -webkit-box-sizing: border-box;
+    box-sizing: border-box;
+
+    max-width:100% !important;
+    overflow:hidden !important;
+}
+.bx-def-constraint-img img {
+    height:auto !important;
+}
+.bx-def-constraint-pre pre {
+    overflow:scroll !important;
+}
+
+
+/* default.css */ 
+
+.bx-def-page-width {
+    width:auto;
+}
+
+.bx-page-wrapper.bx-def-padding-sec-leftright {
+	padding-left: 0px;
+	padding-right: 0px;
+}
+
+#bx-content-container.bx-def-padding-sec-left {
+	padding-left: 0px;
+}
+
+#bx-content-container.bx-def-padding-sec-right {
+	padding-right: 0px;
+}
+
+#bx-content-container .bx-db-container.bx-def-border-block {
+	border-left-width: 0px;
+	border-right-width: 0px;
+}
+
+/* general.css */
+
+#bx-logo-container {
+    margin:0 3rem;
+}
+#bx-footer-wrapper {
+	height: auto;
+}
+
+/* common.css */
+
+/* make popup fullscreen */
+
+.bx-popup-responsive.bx-popup-trans-wrapper:not(.bx-popup-image-wrapper),
+.bx-popup-responsive:not(.bx-popup-image-wrapper) .bx-popup-trans,
+.bx-popup-responsive:not(.bx-popup-image-wrapper).bx-popup-applied,
+.bx-popup-responsive .bx-popup-box {
+    position:fixed !important;
+    top:0 !important;
+    left:0 !important;
+    bottom:0 !important;
+    right:0 !important;
+    width:100%;
+	width:100vw;
+    height:100%;
+    height:100vh;
+    overflow:auto;
+}
+
+.bx-popup-responsive .bx-popup-trans > div,
+.bx-popup-responsive .bx-popup-box > div {
+	border-radius:0;
+	-moz-border-radius:0;
+	-webkit-border-radius:0;
+}
+
+.bx-popup-responsive.bx-popup-image-wrapper:not(.foo) .bx-popup-border,
+.bx-popup-responsive.bx-popup-trans-wrapper:not(.foo) .bx-popup-border,
+.bx-popup-responsive.bx-popup-box-wrapper:not(.foo) .bx-popup-border {
+    border:none;
+}
+
+.bx-popup-responsive.bx-popup-image-wrapper .bx-popup,
+.bx-popup-responsive.bx-popup-trans-wrapper .bx-popup,
+.bx-popup-responsive.bx-popup-box-wrapper .bx-popup {
+	-moz-box-shadow:none;
+	-webkit-box-shadow:none;
+    box-shadow:none;
+}
+
+.bx-popup-width {
+    width:100%;
+}
+
+/* page layouts */
+
+.bx-layout-col {
+    float:none !important;
+    width:auto !important;
+}
+
+.bx-layout-cell {
+    padding:0 !important;
+}
+
+/* menu.css */
+
+ul.bx-menu-toolbar li a span.bx-menu-toolbar-item-title,
+#bx-menu-toolbar-2-container ul.bx-menu-toolbar li a span.bx-menu-toolbar-item-title {
+    display:none;
+}
+
+/* loading indicator */
+.bx-btn .bx-loading-ajax-btn {
+	margin-top: 2px;
+	margin-bottom: 2px;
+}
+.bx-btn.bx-btn-small .bx-loading-ajax-btn {
+    margin-top: 0px;
+    margin-bottom: 0px;
+}
+
+/* grid.css */
+
+html .bx-grid-table-wrapper {
+    overflow-x:scroll;
+}
+html .bx-grid-table {
+    min-width:720px;
+}
+html .bx-grid-table-wrapper2:before,
+html .bx-grid-table-wrapper2:after {
+    display:block;
+}
+
+} /* @media end */

+ 549 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/css/menu.css

@@ -0,0 +1,549 @@
+/* --------------- horizontal menu */
+
+ul.bx-menu-hor {
+    padding:0;
+    margin:0;
+    list-style:none;
+}
+
+.bx-menu-hor li {
+    display:inline-block;
+    vertical-align:middle;
+}
+
+.bx-menu-hor li a {
+    display:block;
+    text-decoration:none;
+}
+
+.bx-menu-hor-inline li a {
+	display:inline;
+}
+
+.bx-menu-hor li.bx-menu-tab-active a {
+    color:#000;
+}
+
+.bx-menu-hor li img,
+.bx-menu-hor li .sys-icon {
+	display:inline-block;
+    vertical-align:middle;
+}
+.bx-menu-hor li img {
+    height:1.6rem;
+    width:auto;    
+}
+
+/* --------------- horizontal inline menu with 'special' buttons */
+.bx-menu-hor-inline-sbtn li a {
+    position: relative;
+    display: inline-block;
+
+    height: 24px; 
+    height: 1.6rem;
+
+    line-height: 24px;
+    line-height: 1.6rem;
+}
+
+/* --------------- vertical, vertical lite, notifications menu */
+
+ul.bx-menu-notif,
+ul.bx-menu-verlit,
+ul.bx-menu-ver {
+    padding:0;
+    margin:0;
+    list-style:none;
+}
+
+.bx-menu-ver li {
+    margin-top:1px;
+}
+
+.bx-menu-notif li:first-child,
+.bx-menu-verlit li:first-child,
+.bx-menu-ver li:first-child {
+    margin-top:0px;
+}
+
+.bx-menu-verlit li a,
+.bx-menu-ver li a {
+    display:block;
+    text-decoration:none;
+}
+
+.bx-menu-notif li.bx-menu-tab-active a,
+.bx-menu-verlit li.bx-menu-tab-active a,
+.bx-menu-ver li.bx-menu-tab-active a {
+    color:#000;
+}
+
+.bx-menu-notif li img,
+.bx-menu-notif li .sys-icon,
+.bx-menu-notif li span,
+.bx-menu-verlit li img,
+.bx-menu-verlit li .sys-icon,
+.bx-menu-verlit li span,
+.bx-menu-ver li img,
+.bx-menu-ver li .sys-icon,
+.bx-menu-ver li span {
+    display:inline-block;
+    vertical-align:middle;
+}
+
+.bx-menu-notif li img,
+.bx-menu-notif li .sys-icon,
+.bx-menu-verlit li img,
+.bx-menu-verlit li .sys-icon,
+.bx-menu-ver li img,
+.bx-menu-ver li .sys-icon {
+    width:20px;
+    width:1.25rem;
+    height:20px;
+    height:1.25rem;
+    margin-right:2px;
+    margin-right:0.125rem;
+    text-align:center;
+}
+
+.bx-menu-notif li img,
+.bx-menu-verlit li img,
+.bx-menu-ver li img {
+    background-size:1.25rem 1.25rem;
+    background-position:center center;
+    background-repeat:no-repeat;
+}
+
+.bx-menu-ver li .sys-icon {
+    font-size:20px;
+    font-size:1.25rem;
+}
+
+.bx-menu-ver li .flag-icon {
+    vertical-align:baseline;
+}
+
+.bx-menu-notif li .sys-icon {
+    font-size:24px;
+    font-size:1.5rem;
+    width:24px;
+    width:1.5rem;
+    height:24px;
+    height:1.5rem;
+    margin-right:5px;
+    margin-right:0.3rem;
+}
+
+.bx-menu-ver .bx-def-hr {
+    margin:0;
+}
+
+.bx-menu-verlit li img {
+    width:24px;
+    width:1.5rem;
+    height:24px;
+    height:1.5rem;
+    background-position:center center;
+    background-repeat:no-repeat;
+    vertical-align:middle;
+}
+
+.bx-menu-notif .bx-def-unit-alert-middle {
+    position:relative;
+    float:right;
+}
+
+/* --------------- main menu */
+
+#bx-homepage-menu-sys_homepage {
+    text-align:center;
+}
+
+#bx-menu-main-submenu {
+    display:inline-block;
+    line-height:2.5rem;
+    vertical-align:top;
+}
+
+.bx-menu-main-submenu-div .sys-icon {
+    font-size:32px;
+    font-size:2rem;
+}
+
+ul.bx-menu-main-submenu {
+    padding:0;
+    margin:0;
+    list-style:none;
+    white-space:nowrap;
+}
+
+.bx-menu-main-submenu li {
+    display:inline-block;
+    margin:0;    
+    vertical-align:top;
+}
+
+.bx-menu-main-submenu li a {
+    display:inline-block;
+    text-decoration:none;
+    font-size:1.2rem;
+    line-height:2.5rem;
+    vertical-align:top;
+}
+
+.bx-menu-main-submenu li.bx-menu-tab-active a {
+    color:#000;
+}
+
+/* --------------- toolbar menu */
+.bx-menu-toolbar {
+    list-style:none;
+    margin:0 0 0 0;
+    padding:0;
+}
+ul.bx-menu-toolbar li {
+    float:left;
+    margin:0px;
+} 
+
+ul.bx-menu-toolbar li a {
+    position:relative;
+    display:block;
+    height:48px;
+    height:3rem;
+
+    line-height:48px;
+    line-height:3rem;
+    font-size:16px;
+    font-size:1rem;
+    text-align: center;
+    vertical-align:middle;
+    text-decoration:none;
+}
+
+ul.bx-menu-toolbar li a .sys-icon,
+ul.bx-menu-toolbar li a .sys-icon-a,
+ul.bx-menu-toolbar li a img,
+ul.bx-menu-toolbar li a span {
+    vertical-align:top;
+}
+
+ul.bx-menu-toolbar li a .sys-icon-a {
+    margin-top:0.75rem;
+}
+
+ul.bx-menu-toolbar li img {
+    background-position:center center;
+    background-repeat:no-repeat;
+}
+
+ul.bx-menu-toolbar li .sys-icon-a,
+ul.bx-menu-toolbar li .sys-icon {
+    display:inline-block;
+    vertical-align:middle;
+
+    font-size:24px;
+    font-size:1.5rem;
+    text-align:center;
+    width:1.5rem;
+    height:1.5rem;
+}
+
+ul.bx-menu-toolbar li .bx-menu-toolbar-item-unit {
+	display: inline-block;
+	vertical-align: middle;
+}
+
+ul.bx-menu-toolbar li a .bx-def-unit-alert-middle {
+	right: 5px;
+    right: -0.3125rem;
+}
+
+.bx-menu-toolbar-padding {
+	padding-top: 48px;
+	padding-top: 3rem;
+}
+
+#bx-menu-toolbar-1-container .bx-menu-toolbar {
+    float:left;
+}
+
+#bx-menu-toolbar-2-container .bx-menu-toolbar {
+    float:right;
+}
+
+#bx-menu-toolbar-2-container .bx-menu-toolbar .bx-menu-toolbar-item-title {
+	display: inline-block;
+	max-width: 10.0rem;
+
+	white-space: nowrap;
+	text-overflow:ellipsis;
+    overflow:hidden;
+}
+
+/* --------------- floating blocks menu */
+.bx-sliding-menu-close,
+.bx-sliding-menu-close:hover {
+    position:absolute;
+    top:0;
+    right:0;
+    text-decoration:none;
+}
+
+.bx-sliding-menu-main {
+    width:100%;    
+
+    background-color: rgba(255, 255, 255, 0.9);
+
+    overflow-x:auto;
+    -webkit-overflow-scrolling:touch;
+}
+
+.bx-sliding-menu-fullheight {
+    height:calc(100% - 3rem);
+    overflow-x:hidden;
+    overflow-y:auto;
+}
+
+.bx-sliding-menu-main ul.bx-menu-floating-blocks-big {
+    display:table;
+    margin:0 auto;
+    white-space:nowrap;
+}
+
+.bx-sliding-menu-main .bx-menu-account-popup-profile .bx-base-pofile-unit {
+    margin-bottom:0;
+}
+
+.bx-sliding-menu-main .bx-menu-account-popup-profile-switcher {
+    overflow:hidden;
+}
+
+.bx-popup-content .bx-menu-floating-blocks-wide,
+.bx-popup-content .bx-menu-floating-blocks {
+    width:256px;
+    width:16rem;
+}
+.bx-popup-content .bx-menu-account-popup .bx-menu-floating-blocks-wide,
+.bx-popup-content .bx-menu-account-popup .bx-menu-floating-blocks {
+    width:auto;
+}
+
+.bx-menu-account-popup-menu ul.bx-menu-floating-blocks-wide {
+    display:inline-block;
+    max-width:800px;
+    max-width:50rem;
+}
+
+ul.bx-menu-floating-blocks-wide,
+ul.bx-menu-floating-blocks-big,
+ul.bx-menu-floating-blocks {
+    padding:0;
+    margin:0;
+    list-style:none;
+}
+
+.bx-menu-floating-blocks-big li,
+.bx-menu-floating-blocks-wide li,
+.bx-menu-floating-blocks li {
+    display: inline-block;
+    /* For IE 7 */
+    zoom: 1;
+    *display: inline;
+    vertical-align:top;    
+
+    width:80px;
+    width:5rem;
+    height:80px;
+    height:5rem;
+}
+.bx-menu-floating-blocks-big li {
+    width:128px;
+    width:8rem;
+    height:128px;
+    height:8rem;
+}
+.bx-menu-floating-blocks-wide li {
+    position:relative;
+    width:100px;
+    width:6.25rem;
+}
+.bx-menu-floating-blocks-wide li .bx-menu-item-addon {
+    right:10px;
+    right:0.625rem;
+    bottom:auto;
+    top:0;    
+}
+
+.bx-menu-floating-blocks-big li:hover,
+.bx-menu-floating-blocks-wide li:hover,
+.bx-menu-floating-blocks li:hover {
+    opacity:0.8;
+}
+
+.bx-menu-floating-blocks-big li:active,
+.bx-menu-floating-blocks-big li.bx-menu-tab-active,
+.bx-menu-floating-blocks-wide li:active,
+.bx-menu-floating-blocks-wide li.bx-menu-tab-active,
+.bx-menu-floating-blocks li:active,
+.bx-menu-floating-blocks li.bx-menu-tab-active {
+    opacity:1.0;
+}
+
+.bx-menu-floating-blocks-big li a,
+.bx-menu-floating-blocks-wide li a,
+.bx-menu-floating-blocks li a {
+    display:block;
+    text-decoration:none;
+}
+
+.bx-menu-floating-blocks-big li.bx-menu-tab-active a,
+.bx-menu-floating-blocks-wide li.bx-menu-tab-active a,
+.bx-menu-floating-blocks li.bx-menu-tab-active a {
+    color:#000;
+}
+
+.bx-menu-floating-blocks-wide li img,
+.bx-menu-floating-blocks-wide li .sys-icon,
+.bx-menu-floating-blocks li img,
+.bx-menu-floating-blocks li .sys-icon {
+    display:block;
+    width:32px;
+    width:2rem;
+    height:32px;
+    height:2rem;
+    margin:16px 20px 3px 20px;
+    margin:1rem 1.5rem 0.2rem 1.5rem;
+    background-size:2rem 2rem;
+}
+.bx-menu-floating-blocks-big li img,
+.bx-menu-floating-blocks-big li .sys-icon {
+    display:block;
+    width:64px;
+    width:4rem;
+    height:64px;
+    height:4rem;
+    margin:16px 32px 8px 32px;
+    margin:1rem 2rem 0.5rem 2rem;
+    background-size:4rem 4rem;
+}
+.bx-menu-floating-blocks-wide li img,
+.bx-menu-floating-blocks-wide li .sys-icon {
+    margin-left:34px;
+    margin-right:34px;
+    margin-left:2.125rem;
+    margin-right:2.125rem;
+}
+
+.bx-menu-floating-blocks-big li img,
+.bx-menu-floating-blocks-wide li img,
+.bx-menu-floating-blocks li img {
+    background-position:center center;
+    background-repeat:no-repeat;
+}
+
+.bx-menu-floating-blocks-big li .sys-icon,
+.bx-menu-floating-blocks-wide li .sys-icon,
+.bx-menu-floating-blocks li .sys-icon {
+    text-align:center;
+    vertical-align:bottom;
+    line-height:32px;
+    line-height:2rem;
+    font-size:32px;
+    font-size:2rem;
+}
+.bx-menu-floating-blocks-big li .sys-icon {
+    line-height:64px;
+    line-height:4rem;
+    font-size:64px;
+    font-size:4rem;
+}
+
+.bx-menu-floating-blocks-big li .bx-menu-floating-blocks-title,
+.bx-menu-floating-blocks-wide li .bx-menu-floating-blocks-title,
+.bx-menu-floating-blocks li .bx-menu-floating-blocks-title {
+    text-align:center;
+    overflow:hidden;
+    text-overflow:ellipsis;
+    white-space:nowrap;
+}
+
+/* --------------- interactive block menu */
+ul.bx-menu-inter,
+ul.bx-menu-inter-vert {
+	padding:0;
+    margin:0;
+	list-style:none;
+}
+ul.bx-menu-inter li {
+    float:left;
+}
+li.bx-menu-inter-div {
+	padding: 0px 3px;
+}
+li.bx-menu-inter-div:first-child {
+	display: none;
+}
+li.bx-menu-inter-act {}
+li.bx-menu-inter-pas a:hover {
+	text-decoration: none;
+}
+li.bx-menu-inter-hidden {
+	display: none;
+}
+
+/* --------------- horizontal buttons menu */
+
+ul.bx-menu-btns-ver,
+ul.bx-menu-btns-hor {
+	padding:0;
+    margin:0;
+	list-style:none;
+    text-align:center;
+}
+ul.bx-menu-btns-hor li {
+    display:inline;
+    display:inline-block;
+	vertical-align: bottom;
+}
+ul.bx-menu-btns-ver li {
+    text-align:center;
+}
+ul.bx-menu-btns-ver li .bx-btn {
+    float:none;
+    display:inline-block;
+}
+
+ul.bx-menu-btns-hor li:last-child .bx-btn {
+    margin-right:0;
+}
+
+/* --------------- account popup menu */
+
+.bx-popup-content .bx-menu-account-popup {
+    min-width:17em;
+}
+
+.bx-menu-account-popup-menu {
+    text-align:center;
+}
+
+.bx-sliding-menu-main > .bx-def-page-width {
+    position:relative;
+}
+
+.bx-menu-account-popup-profile-switcher-link {
+    text-align:center;
+}
+
+.bx-menu-account-popup-profile-switcher-link a:hover,
+.bx-menu-account-popup-profile-switcher-link a {
+    text-decoration:none;
+}
+
+/* --------------- custom menu */
+
+.bx-menu-custom > li > span,
+.bx-menu-custom > li > a {
+    display:inline-block;
+}

+ 159 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/form_field_location.html

@@ -0,0 +1,159 @@
+
+<div class="bx-form-input-location-cont bx-clearfix">
+    __input__
+    <bx_if:auto_input>
+        <div id="__id_status__" class="bx-def-margin-left">__location_string__</div>
+    </bx_if:auto_input>
+</div>
+
+<input type="hidden" name="__name___lat" value="__lat__" />
+<input type="hidden" name="__name___lng" value="__lng__" />
+<input type="hidden" name="__name___country" value="__country__" />
+<input type="hidden" name="__name___state" value="__state__" />
+<input type="hidden" name="__name___city" value="__city__" />
+<input type="hidden" name="__name___zip" value="__zip__" />
+<input type="hidden" name="__name___street" value="__street__" />
+<input type="hidden" name="__name___street_number" value="__street_number__" />
+
+<script>
+
+    var oBxLocationHashMap = {
+        '__name___lat': '',
+        '__name___lng': '',
+        '__name___city': 'locality',
+        '__name___state': 'administrative_area_level_1',
+        '__name___country': 'country',
+        '__name___zip': 'postal_code',
+        '__name___street': 'route',
+        '__name___street_number': 'street_number'
+    };
+
+    var bx_location_reset = function () {
+        for (sKey in oBxLocationHashMap)
+            $('[name="' + sKey + '"]').val('');
+    };
+
+    var bx_location_process_address = function (o) {
+        var i, sKey, sCountry, sCity;
+
+        for (i in o) {
+            for (sKey in oBxLocationHashMap) {
+
+                var sName = oBxLocationHashMap[sKey];
+                
+                if ('locality' == sName && -1 == o[i].types.indexOf(sName))
+                    sName = 'postal_town';
+                
+                if ('administrative_area_level_1' == sName && -1 == o[i].types.indexOf(sName))
+                    sName = 'administrative_area_level_2';
+
+                if (o[i].types.indexOf(sName) > -1) {
+                    var sIndex = 'route' == sName || 'locality' == sName ? 'long_name' : 'short_name';
+                    $('[name="' + sKey + '"]').val(o[i][sIndex]);
+                }
+                
+                if (o[i].types.indexOf('locality') > -1)
+                    sCity = o[i].short_name;
+                else if (o[i].types.indexOf('country') > -1)
+                    sCountry = o[i].long_name;
+            }
+        }
+        if ('undefined' != typeof(sCity) && sCity.length)
+            $('#__id_status__').html(sCity + ", " + sCountry);
+        else
+            $('#__id_status__').html(sCountry);
+    };
+
+    var bx_location_geolocate = function () {
+        if (navigator.geolocation) {
+            navigator.geolocation.getCurrentPosition(function(position) {
+                var geolocation = {
+                    lat: position.coords.latitude,
+                    lng: position.coords.longitude
+                };
+                var circle = new google.maps.Circle({
+                    center: geolocation,
+                    radius: position.coords.accuracy
+                });
+                glBxAutocomplete__name__.setBounds(circle.getBounds());
+            });
+        }
+    }
+
+    $('input[name=__name__][type=checkbox]').on('change', function () {
+
+        var callbackOnLocationReset = function (error) {
+            var sKey;
+            for (sKey in oBxLocationHashMap) 
+                $('[name="' + sKey + '"]').val('');
+            $('#__id_status__').html("<bx_text_js:_sys_location_undefined />");
+        }
+
+        var callbackOnLocationSucess = function (position) {
+            bx_location_reset ();
+
+            $('[name="__name___lat"]').val(position.coords.latitude);
+            $('[name="__name___lng"]').val(position.coords.longitude);
+
+            var latlng = new google.maps.LatLng(position.coords.latitude, position.coords.longitude);
+            var geocoder = new google.maps.Geocoder();
+
+            geocoder.geocode({'latLng': latlng}, function (results, status) {
+                if (status == google.maps.GeocoderStatus.OK) {
+                    bx_location_process_address(results[0]['address_components']);
+                } 
+                else {
+                    $('#__id_status__').html(status);
+                }
+            });
+
+        }
+
+        if ($(this).is(':checked')) {
+            $('#__id_status__').html('<bx_text_js:_sys_location_determining />');
+            if (navigator.geolocation)
+                navigator.geolocation.getCurrentPosition(callbackOnLocationSucess, callbackOnLocationReset);
+            else
+                $('#__id_status__').html('<bx_text_js:_sys_location_geolocation_not_supported_by_browser />');
+        } else {
+            callbackOnLocationReset();
+        }
+    });
+
+
+    $(document).ready(function () {
+
+        var callbackFillInAddress = function () {
+            // Get the place details from the autocomplete object.
+            var place = glBxAutocomplete__name__.getPlace();
+
+            bx_location_reset ();
+
+            $('[name="__name___lat"]').val(place.geometry.location.lat);
+            $('[name="__name___lng"]').val(place.geometry.location.lng);
+            
+            bx_location_process_address(place.address_components);
+        }
+        
+        $('input[name=__name__][type=text]').on('focus', bx_location_geolocate);
+
+        $('input[name=__name__][type=text]').on('blur', function () {
+            if (!$(this).val().length)
+                bx_location_reset ();
+        });
+
+        if ($('input[name=__name__][type=text]').size()) {
+
+            // Create the autocomplete object, restricting the search to geographical location types.
+            glBxAutocomplete__name__ = new google.maps.places.Autocomplete(
+                /** @type {!HTMLInputElement} */(document.getElementById('__id_input__'))
+            );
+
+            // When the user selects an address from the dropdown, populate the address fields in the form.
+            glBxAutocomplete__name__.addListener('place_changed', callbackFillInAddress);
+        }
+
+    });
+
+</script>
+

+ 91 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/grid.html

@@ -0,0 +1,91 @@
+<div id="__id_wrap__">
+    <bx_if:display_header>
+        <div class="bx-grid-header">
+        	<div class="bx-grid-header-cnt bx-def-padding-sec-bottom bx-clearfix">
+	            <bx_if:filter>
+	                <div class="bx-grid-header-controls-filter bx-def-margin-thd-top">
+	                    __controls__
+	                </div>  
+	            </bx_if:filter>
+	            <bx_if:actions_independent>
+	                <div class="bx-grid-header-controls-independent bx-def-margin-thd-neg">
+	                    __actions_independent__
+	                </div>
+	            </bx_if:actions_independent>
+			</div>
+        </div>
+    </bx_if:display_header>
+    <div class="bx-grid-table-wrapper2 bx-grid-table-hide-shadow-before">
+        <div id="__id_cont__" class="bx-grid-table-wrapper">
+            <table id="__id_table__" class="bx-grid-table">
+                <bx_if:display_head>
+                    <thead>
+                        <tr class="bx-grid-table-row-hr">
+                            <td colspan="__columns__"><hr class="bx-def-hr" /></td>
+                        </tr>                    
+                        <tr>
+                            <bx_repeat:row_header>
+                                __header_cell__
+                            </bx_repeat:row_header>
+                        </tr>
+                        <tr class="bx-grid-table-row-hr">
+                            <td colspan="__columns__"><hr class="bx-def-hr" /></td>
+                        </tr>
+                    </thead>
+                </bx_if:display_head>
+                <tbody>
+                        <bx_repeat:rows_data>
+                            <tr id="__id_row__" class="__row_class__">
+                                __row__
+                            </tr>
+                        </bx_repeat:rows_data>
+                </tbody>
+            </table>
+        </div>
+    </div>
+    <bx_if:display_footer>
+        <div class="bx-grid-footer ">
+            <hr class="bx-def-hr" />
+            <div class="bx-grid-footer-cnt bx-def-padding-sec-top bx-def-margin-thd-neg bx-clearfix">
+                <bx_if:actions_bulk>
+                    <div style="float:left;">
+                        __actions_bulk__
+                    </div>
+                </bx_if:actions_bulk>
+    
+                <div class="bx-def-margin-thd" style="float:right;">
+                    __paginate__
+                </div>
+            </div>
+        </div>
+    </bx_if:display_footer>    
+</div>
+
+<script>
+    $(document).ready(function() {
+        if (typeof(glGrids) == 'undefined')
+            glGrids = {};
+        if (undefined == glGrids.__object__) {
+            glGrids.__object__ = new BxDolGrid('__object__', {
+                sortable: __sortable__,
+                sorting: __sorting__,
+                paginate_get_start: '__paginate_get_start__',
+                paginate_get_per_page: '__paginate_get_per_page__',
+                start: __start__,
+                per_page: __per_page__,
+                filter: '__filter__',
+                filter_get: '__filter_get__',
+                order_field: '__order_field__',
+                order_dir: '__order_dir__',
+                order_get_field: '__order_get_field__',
+                order_get_dir: '__order_get_dir__',
+                text_search_input: '<bx_text_js:_sys_grid_search />',
+                popup_options: __popup_options__,
+                query_append: __query_append__,
+                confirm_messages: __confirm_messages__,
+                columns: __columns__
+            });
+        }
+    });
+</script>
+

+ 7 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/menu_toolbar.html

@@ -0,0 +1,7 @@
+<ul class="bx-menu-toolbar bx-menu-object-__object__">
+    <bx_repeat:menu_items>
+        <li id="bx-menu-toolbar-item-__name__" class="bx-menu-toolbar-item bx-menu-item-__name__ bx-def-padding-thd-leftright bx-def-color-bg-hl-hover __class_add__">
+            <a href="__link__" target="__target__" onclick="__onclick__"><bx_if:title><span class="bx-menu-toolbar-item-title bx-def-margin-thd-leftright bx-def-font-contrasted">__title__</span></bx_if:title><bx_if:icon><i class="sys-icon __icon__ bx-def-margin-thd-leftright"></i></bx_if:icon><bx_if:icon-a><i class="sys-icon-a bx-def-margin-thd-leftright" data-rotate="down" data-icon="__icon-a__" data-color="#fff"></i></bx_if:icon-a><bx_if:image><img class="bx-def-thumb bx-def-thumb-size bx-def-margin-thd-leftright" src="__icon_url__" /></bx_if:image><bx_if:unit><div class="bx-menu-toolbar-item-unit bx-def-margin-thd-leftright">__unit__</div></bx_if:unit><bx_if:addon>__addonf__</bx_if:addon></a>
+        </li>
+    </bx_repeat:menu_items>
+</ul>

+ 29 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/page_2.html

@@ -0,0 +1,29 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
+	<head>
+        <bx_injection:injection_head_begin />
+        
+	    <meta http-equiv="Content-Type" content="text/html; charset=__page_charset__" />
+	    <title>__page_header__</title>
+	    <base href="<bx_url_root />" />
+
+	    <bx_include_css_system />
+	    <bx_include_css />
+
+	    <bx_include_js_system />
+	    <bx_include_js />
+	    __extra_js__
+
+	    __dol_images__
+		__dol_lang__
+		__dol_options__
+
+        __system_injection_head__
+	    <bx_injection:injection_head />
+	</head>
+	<body class="bx-def-font bx-def-color-bg-page" dir="__lang_direction__" <bx_injection:injection_body /> >
+		__system_injection_header__
+		<bx_injection:injection_header />
+		__page_main_code__
+	</body>
+</html>

+ 39 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/page_44.html

@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html lang="__lang__">
+<head>
+    <bx_injection:injection_head_begin />
+    
+    <meta http-equiv="Content-Type" content="text/html; charset=__page_charset__" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
+    <title>__page_header__</title>
+    <base href="<bx_url_root />" />
+
+
+
+    <meta http-equiv="Content-Style-Type" content="text/css" />
+    <bx_include_css_system />
+    <bx_include_css />
+
+    __dol_images__
+    __dol_lang__
+    __dol_options__
+
+    <bx_include_js_system />
+    <bx_include_js />
+    <script type="text/javascript" language="javascript">
+        var site_url = '<bx_url_root />';
+    </script>
+    __extra_js__
+
+    __system_injection_head__
+    <bx_injection:injection_head />
+</head>
+<body class="bx-def-font bx-def-color-bg-page" dir="__lang_direction__" <bx_injection:injection_body /> >
+	__system_injection_header__
+	<bx_injection:injection_header />
+
+	<div class="bx-def-padding-sec">
+	    __page_main_code__
+	</div>
+</body>
+</html>

+ 271 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseAccountForms.php

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

+ 849 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseCmts.php

@@ -0,0 +1,849 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * @see BxDolCmts
+ */
+class BxBaseCmts extends BxDolCmts
+{
+    protected $_sJsObjName;
+    protected $_sStylePrefix;
+
+    function __construct( $sSystem, $iId, $iInit = true, $oTemplate = false)
+    {
+        parent::__construct($sSystem, $iId, $iInit, $oTemplate);
+        if (empty($sSystem))
+            return;
+
+        $this->_sJsObjName = 'oCmts' . bx_gen_method_name($sSystem, array('_' , '-')) . $iId;
+        $this->_sStylePrefix = isset($this->_aSystem['root_style_prefix']) ? $this->_aSystem['root_style_prefix'] : 'cmt';
+
+        BxDolTemplate::getInstance()->addJsTranslation('_sys_txt_cmt_loading');
+
+        $oForm = $this->_getForm(BX_CMT_ACTION_POST, 0);
+        $this->_bHtml = (int)$oForm->aInputs['cmt_text']['html'] > 0;
+    }
+
+    /**
+     * Add comments CSS/JS
+     */
+    public function addCssJs ()
+    {
+        $this->_oTemplate->addCss(array('cmts.css'));
+        $this->_oTemplate->addJs(array('jquery.anim.js', 'jquery.form.min.js', 'BxDolCmts.js'));
+
+        $oForm = BxDolForm::getObjectInstance($this->_sFormObject, $this->_sFormDisplayPost);
+        $oForm->addCssJs();
+    }
+
+    public function getJsObjectName()
+    {
+        return $this->_sJsObjName;
+    }
+
+    /**
+     * Get initialization section of comments box
+     *
+     * @return string
+     */
+    public function getJsScript()
+    {
+        $aParams = array(
+            'sObjName' => $this->_sJsObjName,
+            'sRootUrl' => BX_DOL_URL_ROOT,
+            'sSystem' => $this->getSystemName(),
+            'iAuthorId' => $this->_getAuthorId(),
+            'iObjId' => $this->getId(),
+        	'sBaseUrl' => $this->getBaseUrl(),
+            'sPostFormPosition' => $this->_aSystem['post_form_position'],
+            'sBrowseType' => $this->_sBrowseType,
+            'sDisplayType' => $this->_sDisplayType
+        );
+
+        $this->addCssJs();
+        return BxDolTemplate::getInstance()->_wrapInTagJsCode("var " . $this->_sJsObjName . " = new BxDolCmts(" . json_encode($aParams) . ");");
+    }
+
+    /**
+     * get full comments block with initializations
+     */
+    function getCommentsBlock($aBp = array(), $aDp = array())
+    {
+		$this->_getParams($aBp, $aDp);
+
+		//add live update
+		$this->actionResumeLiveUpdate();
+
+		$sServiceCall = BxDolService::getSerializedService('system', 'get_live_updates_comments', array($this->_sSystem, $this->_iId, $this->_getAuthorId(), '{count}'), 'TemplCmtsServices');
+		BxDolLiveUpdates::getInstance()->add($this->_sSystem . '_live_updates_cmts_' . $this->_iId, 1, $sServiceCall);
+		//add live update
+
+        $sCaption = _t($this->_aT['block_comments_title'], $this->getCommentsCountAll());
+        $sContent = BxDolTemplate::getInstance()->parseHtmlByName('comments_block.html', array(
+            'system' => $this->_sSystem,
+            'list_anchor' => $this->getListAnchor(),
+            'id' => $this->getId(),
+            'comments' => $this->getComments($aBp, $aDp),
+            'post_form_top' => $this->getFormBoxPost($aBp, array_merge($aDp, array('type' => $this->_sDisplayType, 'position' => BX_CMT_PFP_TOP))),
+            'post_form_bottom'  => $this->getFormBoxPost($aBp, array_merge($aDp, array('type' => $this->_sDisplayType, 'position' => BX_CMT_PFP_BOTTOM))),
+        	'view_image_popup' => $this->_getViewImagePopup(),
+            'script' => $this->getJsScript()
+        ));
+
+        return $aDp['in_designbox'] ? DesignBoxContent($sCaption, $sContent, BX_DB_DEF, $this->_getControlsBox()) : array(
+            'title' => $sCaption,
+            'content' => $sContent,
+            'menu' => $this->_getControlsBox(),
+        );
+    }
+
+    /**
+     * get comments list for specified parent comment
+     *
+     * @param array $aBp - browse params array
+     * @param array $aDp - display params array
+     *
+     */
+    function getComments($aBp = array(), $aDp = array())
+    {
+        $this->_prepareParams($aBp, $aDp);
+
+        $aCmts = $this->getCommentsArray($aBp['vparent_id'], $aBp['filter'], $aBp['order'], $aBp['start'], $aBp['per_view']);
+        if(empty($aCmts) || !is_array($aCmts)) {
+        	if((int)$aBp['parent_id'] == 0 && !isLogged())	{
+        		$oPermalink = BxDolPermalinks::getInstance();
+        		return MsgBox(_t('_cmt_msg_login_required', $oPermalink->permalink('page.php?i=login'), $oPermalink->permalink('page.php?i=create-account')));
+        	}
+
+            return isset($aDp['show_empty']) && $aDp['show_empty'] === true ? $this->_getEmpty() : '';
+        }
+
+        $sCmts = '';
+        foreach($aCmts as $k => $aCmt)
+            $sCmts .= $this->getComment($aCmt, $aBp, $aDp);
+
+        $sCmts = $this->_getMoreLink($sCmts, $aBp, $aDp);
+        return $sCmts;
+    }
+
+    /**
+     * get comment view block with initializations
+     */
+    function getCommentBlock($iCmtId = 0, $aBp = array(), $aDp = array())
+    {
+        $mixedResult = $this->isViewAllowed();
+        if($mixedResult !== CHECK_ACTION_RESULT_ALLOWED)
+            return $mixedResult;
+
+        $sComment = $this->getComment(
+        	$iCmtId, 
+        	array_merge(array('type' => $this->_sBrowseType), $aBp), 
+        	array_merge(array('type' => BX_CMT_DISPLAY_THREADED), $aDp)
+        );
+        if (!$sComment)
+            return '';
+
+        return BxDolTemplate::getInstance()->parseHtmlByName('comment_block.html', array(
+            'system' => $this->_sSystem,
+            'id' => $this->getId(),
+            'comment' => $sComment,
+        	'view_image_popup' => $this->_getViewImagePopup(), 
+            'script' => $this->getJsScript()
+        ));
+    }
+
+    /**
+     * get one just posted comment
+     *
+     * @param  int    $iCmtId - comment id
+     * @return string
+     */
+    function getComment($mixedCmt, $aBp = array(), $aDp = array())
+    {
+        $iUserId = $this->_getAuthorId();
+        $aCmt = !is_array($mixedCmt) ? $this->getCommentRow((int)$mixedCmt) : $mixedCmt;
+        if (!$aCmt)
+            return '';
+
+        list($sAuthorName, $sAuthorLink, $sAuthorIcon) = $this->_getAuthorInfo($aCmt['cmt_author_id']);
+
+        $sClass = $sClassCnt = '';
+        if(isset($aCmt['vote_rate']) && (float)$aCmt['vote_rate'] < $this->_aSystem['viewing_threshold']) {
+            $this->_oTemplate->pareseHtmlByName('comment_hidden.html', array(
+                'js_object' => $this->_sJsObjName,
+                'id' => $aCmt['cmt_id'],
+                'title' => bx_process_output(_t('_hidden_comment', $sAuthorName)),
+                'bx_if:show_replies' => array(
+                    'condition' => $aCmt['cmt_replies'] > 0,
+                    'content' => array(
+                        'replies' => _t('_Show N replies', $aCmt['cmt_replies'])
+                    )
+                )
+            ));
+
+            $sClass = ' ' . $this->_sStylePrefix . '-hidden';
+        }
+
+        if($aCmt['cmt_author_id'] == $iUserId)
+            $sClass .= ' ' . $this->_sStylePrefix . '-mine';
+
+		if(!empty($aDp['blink']) && in_array($aCmt['cmt_id'], $aDp['blink']))
+			$sClass .= ' ' . $this->_sStylePrefix . '-blink';
+
+		if(!empty($aDp['class_comment']))
+			$sClass .= ' ' . $aDp['class_comment'];
+
+		if(!empty($aDp['class_comment_content']))
+			$sClassCnt .= ' ' . $aDp['class_comment_content'];
+			
+        $sActions = $this->_getActionsBox($aCmt, $aDp);
+
+        $aTmplReplyTo = array();
+        if((int)$aCmt['cmt_parent_id'] != 0) {
+            $aParent = $this->getCommentRow($aCmt['cmt_parent_id']);
+            list($sParAuthorName, $sParAuthorLink, $sParAuthorIcon) = $this->_getAuthorInfo($aParent['cmt_author_id']);
+
+            $aTmplReplyTo = array(
+                'style_prefix' => $this->_sStylePrefix,
+                'par_cmt_link' => $this->getItemUrl($aCmt['cmt_parent_id']),
+            	'par_cmt_title' => bx_html_attribute(_t('_in_reply_to', $sParAuthorName)),
+                'par_cmt_author' => $sParAuthorName
+            );
+        }
+
+        $sAttachments = $this->_getAttachments($aCmt);
+
+        $sReplies = '';
+        if((int)$aCmt['cmt_replies'] > 0 && !empty($aDp) && $aDp['type'] == BX_CMT_DISPLAY_THREADED) {
+        	$aDp['show_empty'] = false;
+            $sReplies = $this->getComments(array('parent_id' => $aCmt['cmt_id'], 'vparent_id' => $aCmt['cmt_id'], 'type' => $aBp['type']), $aDp);
+        }
+
+		$sAgo = bx_time_js($aCmt['cmt_time']);
+        $bObjectTitle = !empty($this->_aSystem['trigger_field_title']);
+        return $this->_oTemplate->parseHtmlByName('comment.html', array_merge(array(
+            'system' => $this->_sSystem,
+            'style_prefix' => $this->_sStylePrefix,
+            'js_object' => $this->_sJsObjName,
+            'id' => $aCmt['cmt_id'],
+        	'anchor' => $this->getItemAnchor($aCmt['cmt_id']),
+            'class' => $sClass,
+        	'class_cnt' => $sClassCnt,
+            'bx_if:show_reply_to' => array(
+                'condition' => !empty($aTmplReplyTo),
+                'content' => $aTmplReplyTo
+            ),
+            'bx_if:show_ago_link' => array(
+                'condition' => $bObjectTitle,
+                'content' => array(
+                    'style_prefix' => $this->_sStylePrefix,
+                    'view_link' => $this->getViewUrl($aCmt['cmt_id']),
+                    'ago' => $sAgo
+                )
+            ),
+            'bx_if:show_ago_text' => array(
+                'condition' => !$bObjectTitle,
+                'content' => array(
+                    'ago' => $sAgo
+                )
+            ),
+            'bx_if:show_attached' => array(
+                'condition' => !empty($sAttachments),
+                'content' => array(
+                    'style_prefix' => $this->_sStylePrefix,
+                    'attached' => $sAttachments
+                )
+            ),
+            'actions' => $sActions,
+            'replies' =>  $sReplies
+        ), $this->_getTmplVarsAuthor($aCmt), $this->_getTmplVarsText($aCmt)));
+    }
+
+	/**
+     * get one comment for "Live Search"
+     *
+     * @param  int    $iCmtId - comment id
+     * @return string
+     */
+    function getCommentLiveSearch($mixedCmt, $aParams = array())
+    {
+        $aCmt = !is_array($mixedCmt) ? $this->getCommentRow((int)$mixedCmt) : $mixedCmt;
+
+        list($sAuthorName, $sAuthorLink, $sAuthorIcon) = $this->_getAuthorInfo($aCmt['cmt_author_id']);
+        $bAuthorIcon = !empty($sAuthorIcon);
+
+        $sViewLink = $this->getViewUrl($aCmt['cmt_id']);
+        if(empty($sViewLink))
+            $sViewLink = '#';
+
+        return BxDolTemplate::getInstance()->parseHtmlByName('comment_live_search.html', array(
+            'bx_if:show_icon' => array(
+                'condition' => $bAuthorIcon,
+                'content' => array(
+                    'author_icon' => $sAuthorIcon,
+        			'view_link' => $sViewLink
+                )
+            ),
+            'bx_if:show_icon_empty' => array(
+                'condition' => !$bAuthorIcon,
+                'content' => array()
+            ),
+            'view_link' => $sViewLink,
+            'text' => BxTemplFunctions::getInstance()->getStringWithLimitedLength(strip_tags($aCmt['cmt_text']), $this->_sSnippetLenthLiveSearch),
+            'sample' => isset($aParams['txt_sample_single']) ? _t($aParams['txt_sample_single']) : ''
+        ));
+    }
+
+    function getFormBoxPost($aBp = array(), $aDp = array())
+    {
+        if(!$this->isPostReplyAllowed())
+            return '';
+
+        return $this->_getFormBox(BX_CMT_ACTION_POST, $aBp, $aDp);
+    }
+
+    function getFormBoxEdit($aBp = array(), $aDp = array())
+    {
+        return $this->_getFormBox(BX_CMT_ACTION_EDIT, $aBp, $aDp);
+    }
+
+    function getFormPost($iCmtParentId = 0, $aDp = array())
+    {
+        return $this->_getFormPost($iCmtParentId, $aDp);
+    }
+
+    function getFormEdit($iCmtId, $aDp = array())
+    {
+        return $this->_getFormEdit($iCmtId, $aDp);
+    }
+
+    function getNotification($iCountOld = 0, $iCountNew = 0)
+    {
+    	$iCount = (int)$iCountNew - (int)$iCountOld;
+    	if($iCount < 0)
+    	    return '';
+
+		$aComments = $this->_oQuery->getCommentsBy(array('type' => 'latest', 'object_id' => $this->_iId, 'author' => $this->_getAuthorId(), 'others' => 1, 'start' => '0', 'per_page' => $iCount));
+		if(empty($aComments) || !is_array($aComments))
+			return '';
+
+		$sJsObject = $this->getJsObjectName();
+		$oTemplate = BxDolTemplate::getInstance();
+
+		$aComments = array_reverse($aComments);
+		$iComments = count($aComments);
+
+		$aTmplVarsNotifs = array();
+		foreach($aComments as $iIndex => $aComment) {
+			$iCommentId = $aComment['cmt_id'];
+
+			$sShowOnClick = "javascript:" . $sJsObject . ".goTo(this, '" . $this->_sSystem . $iCommentId . "', '" . $iCommentId . "');";
+			$sReplyOnClick = "javascript:" . $sJsObject . ".goToAndReply(this, '" . $this->_sSystem . $iCommentId . "', '" . $iCommentId . "');";
+
+	    	$aTmplVarsNotifs[] = array(
+	    		'bx_if:show_as_hidden' => array(
+	    			'condition' => $iIndex < ($iComments - 1),
+	    			'content' => array(),
+	    		),
+	    		'item' => $oTemplate->parseHtmlByName('comments_notification.html', array_merge(array(
+	    			'style_prefix' => $this->_sStylePrefix,
+	    			'onclick_show' => $sShowOnClick,
+	    			'onclick_reply' => $sReplyOnClick,
+	    		), $this->_getTmplVarsAuthor($aComment), $this->_getTmplVarsText($aComment))),
+	    		'bx_if:show_previous' => array(
+	    			'condition' => $iIndex > 0,
+	    			'content' => array(
+	    				'onclick_previous' => $sJsObject . '.previousLiveUpdate(this)'
+	    			)
+	    		),
+	    		'onclick_close' => $sJsObject . '.hideLiveUpdate(this)'
+			);
+		}
+
+		return $oTemplate->parseHtmlByName('popup_chain.html', array(
+			'html_id' => $this->getNotificationId(),
+			'bx_repeat:items' => $aTmplVarsNotifs
+		));
+    }
+
+    /**
+     * private functions
+     */
+    protected function _getControlsBox()
+    {
+        $sDisplay = '';
+        $bDisplay = (int)$this->_aSystem['is_display_switch'] == 1;
+        if($bDisplay) {
+            $aDisplayLinks = array(
+                array('id' => $this->_sSystem . '-flat', 'name' => $this->_sSystem . '-flat', 'class' => '', 'title' => '_cmt_display_flat', 'target' => '_self', 'onclick' => 'javascript:' . $this->_sJsObjName . '.cmtChangeDisplay(this, \'flat\');'),
+                array('id' => $this->_sSystem . '-threaded', 'name' => $this->_sSystem . '-threaded', 'class' => '', 'title' => '_cmt_display_threaded', 'target' => '_self', 'onclick' => 'javascript:' . $this->_sJsObjName . '.cmtChangeDisplay(this, \'threaded\');')
+            );
+
+            $oMenu = new BxTemplMenuInteractive(array('template' => 'menu_interactive_vertical.html', 'menu_id'=> $this->_sSystem . '-display', 'menu_items' => $aDisplayLinks));
+            $oMenu->setSelected('', $this->_sSystem . '-' . $this->_sDisplayType);
+            $sDisplay = $oMenu->getCode();
+        }
+
+        $sBrowseType = '';
+        $bBrowseType = (int)$this->_aSystem['is_browse_switch'] == 1;
+        if($bBrowseType) {
+            $aBrowseLinks = array(
+                array('id' => $this->_sSystem . '-tail', 'name' => $this->_sSystem . '-tail', 'class' => '', 'title' => '_cmt_browse_tail', 'target' => '_self', 'onclick' => 'javascript:' . $this->_sJsObjName . '.cmtChangeBrowse(this, \'tail\');'),
+                array('id' => $this->_sSystem . '-head', 'name' => $this->_sSystem . '-head', 'class' => '', 'title' => '_cmt_browse_head', 'target' => '_self', 'onclick' => 'javascript:' . $this->_sJsObjName . '.cmtChangeBrowse(this, \'head\');'),
+                array('id' => $this->_sSystem . '-popular', 'name' => $this->_sSystem . '-popular', 'class' => '', 'title' => '_cmt_browse_popular', 'target' => '_self', 'onclick' => 'javascript:' . $this->_sJsObjName . '.cmtChangeBrowse(this, \'popular\');'),
+            );
+
+            $oMenu = new BxTemplMenuInteractive(array('template' => 'menu_interactive_vertical.html', 'menu_id'=> $this->_sSystem . '-browse', 'menu_items' => $aBrowseLinks));
+            $oMenu->setSelected('', $this->_sSystem . '-' . $this->_sBrowseType);
+            $sBrowseType = $oMenu->getCode();
+        }
+
+        $sBrowseFilter = '';
+        $bBrowseFilter = (int)$this->_aSystem['is_browse_filter'] == 1;
+        if($bBrowseFilter) {
+            $aFilterLinks = array(
+                array('id' => $this->_sSystem . '-all', 'name' => $this->_sSystem . '-all', 'class' => '', 'title' => '_cmt_browse_all', 'target' => '_self', 'onclick' => 'javascript:' . $this->_sJsObjName . '.cmtChangeFilter(this, \'all\');'),
+                array('id' => $this->_sSystem . '-friends', 'name' => $this->_sSystem . '-friends', 'class' => '', 'title' => '_cmt_browse_friends', 'target' => '_self', 'onclick' => 'javascript:' . $this->_sJsObjName . '.cmtChangeFilter(this, \'friends\');'),
+                array('id' => $this->_sSystem . '-subscriptions', 'name' => $this->_sSystem . '-subscriptions', 'class' => '', 'title' => '_cmt_browse_subscriptions', 'target' => '_self', 'onclick' => 'javascript:' . $this->_sJsObjName . '.cmtChangeFilter(this, \'subscriptions\');')
+            );
+
+            $oMenu = new BxTemplMenuInteractive(array('template' => 'menu_interactive_vertical.html', 'menu_id'=> $this->_sSystem . '-filter', 'menu_items' => $aFilterLinks));
+            $oMenu->setSelected('', $this->_sSystem . '-' . $this->_sBrowseFilter);
+            $sBrowseFilter = $oMenu->getCode();
+        }
+
+        return $this->_oTemplate->parseHtmlByName('comments_controls.html', array(
+            'display_switcher' => $bDisplay ? $sDisplay : '',
+            'bx_if:is_divider_1' => array(
+                'condition' => $bDisplay && $bBrowseType,
+                'content' => array(
+                    'style_prefix' => $this->_sStylePrefix,
+                )
+            ),
+            'browse_switcher' => $bBrowseType ? $sBrowseType : '',
+            'bx_if:is_divider_2' => array(
+                'condition' => $bBrowseType && $bBrowseFilter,
+                'content' => array(
+                    'style_prefix' => $this->_sStylePrefix,
+                )
+            ),
+            'filter_switcher' => $bBrowseFilter ? $sBrowseFilter : '',
+        ));
+    }
+
+    protected function _getActionsBox(&$aCmt, $aDp = array())
+    {
+    	$bViewOnly = isset($aDp['view_only']) && $aDp['view_only'] === true;
+    	$bDynamicMode = isset($aDp['dynamic_mode']) && $aDp['dynamic_mode'] === true;
+
+        $bMenuManage = false;
+        $sMenuActions = $sMenuManage = '';
+
+		if(!$bViewOnly) {
+	        //--- Actions Menu
+	        $oMenuActions = BxDolMenu::getObjectInstance($this->_sMenuObjActions);
+	        $oMenuActions->setCmtsData($this, $aCmt['cmt_id']);
+	        $oMenuActions->setDynamicMode($bDynamicMode);
+	        $sMenuActions = $oMenuActions->getCode();
+
+	        /*
+	        TODO: Remove before commit.
+	        $oVote = $this->getVoteObject($aCmt['cmt_id']);
+	        if($oVote !== false)
+	            $sMenuActions .= $oVote->getJsScript();
+			*/
+
+	        //--- Manage Menu
+	        $oMenuManage = BxDolMenu::getObjectInstance($this->_sMenuObjManage);
+	        $oMenuManage->setCmtsData($this, $aCmt['cmt_id']);
+	
+	        $sMenuManage = $oMenuManage->getCode();
+	        $bMenuManage = !empty($sMenuManage);
+	        if($bMenuManage) {
+	            $sMenuManage = $this->_oTemplate->parseHtmlByName('comment_manage.html', array(
+	                'style_prefix' => $this->_sStylePrefix,
+	                'content' => $sMenuManage
+	            ));
+	
+	            $sMenuManage = BxTemplFunctions::getInstance()->transBox($this->_sSystem . '-manage-' . $aCmt['cmt_id'], $sMenuManage, true);
+	        }
+		}
+
+        return $this->_oTemplate->parseHtmlByName('comment_actions.html', array(
+            'id' => $aCmt['cmt_id'],
+            'js_object' => $this->_sJsObjName,
+            'style_prefix' => $this->_sStylePrefix,
+            'menu_actions' => $sMenuActions,
+/*
+            'bx_if:hide_rate_count' => array(
+                'condition' => (int)$aCmt['cmt_rate'] <= 0,
+                'content' => array()
+            ),
+            'points' => _t(in_array($aCmt['cmt_rate'], array(-1, 0, 1)) ? '_N_point' : '_N_points', $aCmt['cmt_rate']),
+*/
+            'bx_if:show_menu_manage' => array(
+                'condition' => $bMenuManage,
+                'content' => array(
+                    'js_object' => $this->_sJsObjName,
+                    'style_prefix' => $this->_sStylePrefix,
+                    'id' => $aCmt['cmt_id'],
+                    'popup_text' => $sMenuManage
+                )
+            )
+        ));
+    }
+
+    protected function _getFormBox($sType, $aBp, $aDp)
+    {
+        $iCmtParentId = isset($aBp['parent_id']) ? (int)$aBp['parent_id'] : 0;
+        $sPosition = isset($aDp['position']) ? $aDp['position'] : '';
+
+        $sPositionSystem = $this->_aSystem['post_form_position'];
+        if(!empty($sPosition) && $sPositionSystem != $sPosition)
+            return '';
+
+        $sMethod = '_getForm' . ucfirst($sType);
+        $aForm = $this->$sMethod($iCmtParentId, $aDp);
+
+        return BxDolTemplate::getInstance()->parseHtmlByName('comment_reply_box.html', array(
+            'js_object' => $this->_sJsObjName,
+            'style_prefix' => $this->_sStylePrefix,
+            'bx_if:show_class' => array(
+                'condition' => !empty($sPosition),
+                'content' => array(
+                    'class' => $this->_sStylePrefix . '-reply-' . $sPosition
+                )
+            ),
+            'form' => $aForm['form'],
+            'form_id' => $aForm['form_id'],
+        ));
+    }
+
+    protected function _getFormPost($iCmtParentId = 0, $aDp = array())
+    {
+        $bDynamic = isset($aDp['dynamic_mode']) && (bool)$aDp['dynamic_mode'];
+
+        $oForm = $this->_getForm(BX_CMT_ACTION_POST, $iCmtParentId);
+        $oForm->aInputs['cmt_parent_id']['value'] = $iCmtParentId;
+        $oForm->initChecker();
+
+        if($oForm->isSubmittedAndValid()) {
+            $iCmtAuthorId = $this->_getAuthorId();
+            $iCmtParentId = $oForm->getCleanValue('cmt_parent_id');
+            $sCmtText = $oForm->getCleanValue('cmt_text');
+            $sCmtText = $this->_prepareTextForSave ($sCmtText);
+            $oForm->setSubmittedValue('cmt_text', $sCmtText, $oForm->aFormAttrs['method']);
+
+            $iLevel = 0;
+            $iCmtVisualParentId = 0;
+            if((int)$iCmtParentId > 0) {
+                $aParent = $this->getCommentRow($iCmtParentId);
+
+                $iLevel = (int)$aParent['cmt_level'] + 1;
+                $iCmtVisualParentId = $iLevel > $this->getMaxLevel() ? $aParent['cmt_vparent_id'] : $iCmtParentId;
+            }
+
+            $iCmtId = (int)$oForm->insert(array('cmt_vparent_id' => $iCmtVisualParentId, 'cmt_object_id' => $this->_iId, 'cmt_author_id' => $iCmtAuthorId, 'cmt_level' => $iLevel, 'cmt_time' => time()));
+            if($iCmtId != 0) {
+                if($this->isAttachImageEnabled()) {
+                    $aImages = $oForm->getCleanValue('cmt_image');
+                    if(!empty($aImages) && is_array($aImages)) {
+                        $oStorage = BxDolStorage::getObjectInstance($this->getStorageObjectName());
+
+                        foreach($aImages as $iImageId)
+                            if($this->_oQuery->saveImages($this->_aSystem['system_id'], $iCmtId, $iImageId))
+                                $oStorage->afterUploadCleanup($iImageId, $iCmtAuthorId);
+                    }
+                }
+
+                if($iCmtParentId) {
+                    $this->_oQuery->updateRepliesCount($iCmtParentId, 1);
+
+                    if(!BxDolModuleQuery::getInstance()->isEnabledByName('bx_notifications'))
+                        $this->_sendNotificationEmail($iCmtId, $iCmtParentId);
+                }
+
+                $this->_triggerComment();
+
+                $this->isPostReplyAllowed(true);
+
+                if ($this->_sMetatagsObj) {
+                    $oMetatags = BxDolMetatags::getObjectInstance($this->_sMetatagsObj);
+                    $oMetatags->metaAdd($this->_oQuery->getUniqId($this->_aSystem['system_id'], $iCmtId), $sCmtText);
+                }
+
+                $this->onPostAfter($iCmtId);
+
+                return array('id' => $iCmtId, 'parent_id' => $iCmtParentId);
+            }
+
+            return array('msg' => _t('_cmt_err_cannot_perform_action'));
+        }
+
+        return array('form' => $oForm->getCode($bDynamic), 'form_id' => $oForm->id);
+    }
+
+    protected function _getFormEdit($iCmtId, $aDp = array())
+    {
+        $bDynamic = isset($aDp['dynamic_mode']) && (bool)$aDp['dynamic_mode'];
+
+        $aCmt = $this->_oQuery->getCommentSimple ((int)$this->getId(), $iCmtId);
+        if(!$aCmt)
+            return array('msg' => _t('_No such comment'));
+
+        $iCmtAuthorId = $this->_getAuthorId();
+        if(!$this->isEditAllowed($aCmt))
+            return array('msg' => $aCmt['cmt_author_id'] == $iCmtAuthorId ? strip_tags($this->msgErrEditAllowed()) : _t('_Access denied'));
+
+        $oForm = $this->_getForm(BX_CMT_ACTION_EDIT, $aCmt['cmt_id']);
+        $aCmt['cmt_text'] = $this->_prepareTextForEdit($aCmt['cmt_text']);
+
+        $oForm->initChecker($aCmt);
+        if($oForm->isSubmittedAndValid()) {
+            $sCmtText = $oForm->getCleanValue('cmt_text');
+            $sCmtText = $this->_prepareTextForSave ($sCmtText);
+            $oForm->setSubmittedValue('cmt_text', $sCmtText, $oForm->aFormAttrs['method']);
+
+            if($oForm->update($iCmtId) !== false) {
+
+                $this->isEditAllowed(true);
+
+                if ($this->_sMetatagsObj) {
+                    $oMetatags = BxDolMetatags::getObjectInstance($this->_sMetatagsObj);
+                    $oMetatags->metaAdd($this->_oQuery->getUniqId($this->_aSystem['system_id'], $iCmtId), $sCmtText);
+                }
+
+                $this->onEditAfter($iCmtId);
+
+                return array('id' => $iCmtId, 'text' => $this->_prepareTextForOutput($sCmtText, $iCmtId));
+            }
+
+            return array('msg' => _t('_cmt_err_cannot_perform_action'));
+        }
+
+        return array('form' => $oForm->getCode($bDynamic), 'form_id' => $oForm->id);
+    }
+
+    protected function _getForm($sAction, $iId)
+    {
+        $oForm = $this->_getFormObject($sAction);
+        $oForm->setId(sprintf($oForm->getAttributeMask('id'), $sAction, $this->_sSystem, $iId));
+        $oForm->setName(sprintf($oForm->getAttributeMask('name'), $sAction, $this->_sSystem, $iId));
+        $oForm->aParams['db']['table'] = $this->_aSystem['table'];
+        $oForm->aInputs['sys']['value'] = $this->_sSystem;
+        $oForm->aInputs['id']['value'] = $this->_iId;
+        $oForm->aInputs['action']['value'] = 'Submit' . ucfirst($sAction) . 'Form';
+
+        if(!$this->isAttachImageEnabled())
+            unset($oForm->aInputs['cmt_image']);
+
+        if(isset($oForm->aInputs['cmt_text'])) {
+            $iCmtTextMin = (int)$this->_aSystem['chars_post_min'];
+            $iCmtTextMax = (int)$this->_aSystem['chars_post_max'];
+
+            $oForm->aInputs['cmt_text']['checker']['params'] = array($iCmtTextMin, $iCmtTextMax);
+            $oForm->aInputs['cmt_text']['checker']['error'] = _t('_Please enter n1-n2 characters', $iCmtTextMin, $iCmtTextMax);
+        }
+
+        return $oForm;
+    }
+
+    protected function _getMoreLink($sCmts, $aBp = array(), $aDp = array())
+    {
+        $iStart = $iPerView = 0;
+        switch($aBp['type']) {
+            case BX_CMT_BROWSE_HEAD:
+            case BX_CMT_BROWSE_POPULAR:
+            case BX_CMT_BROWSE_CONNECTION:
+                $iPerView = $aBp['per_view'];
+
+                $iStart = $aBp['start'] + $iPerView;
+                if($iStart >= $aBp['count'])
+                    return $sCmts;
+
+                break;
+
+            case BX_CMT_BROWSE_TAIL:
+                $iPerView = $aBp['per_view'];
+
+                $iStart = $aBp['start'] - $iPerView;
+                if($iStart < 0) {
+                    $iPerView += $iStart;
+                    $iStart = 0;
+                }
+
+                if($iStart == 0 && $iPerView == 0)
+                    return $sCmts;
+
+                break;
+        }
+
+        $bRoot = (int)$aBp['vparent_id'] <= 0;
+
+        $sMore = BxDolTemplate::getInstance()->parseHtmlByName('comment_more.html', array(
+            'js_object' => $this->_sJsObjName,
+            'style_prefix' => $this->_sStylePrefix,
+            'bx_if:is_root' => array(
+                'condition' => $bRoot,
+                'content' => array()
+            ),
+            'parent_id' => $aBp['vparent_id'],
+            'start' => $iStart,
+            'per_view' => $iPerView,
+            'title' => _t('_cmt_load_more_' . ($aBp['vparent_id'] == 0 ? 'comments' : 'replies') . '_' . $aBp['type'])
+        ));
+
+        switch($aBp['type']) {
+            case BX_CMT_BROWSE_HEAD:
+                case BX_CMT_BROWSE_POPULAR:
+                case BX_CMT_BROWSE_CONNECTION:
+                $sCmts .= $sMore;
+                break;
+
+            case BX_CMT_BROWSE_TAIL:
+                $sCmts = $sMore . $sCmts;
+                break;
+        }
+
+        return $sCmts;
+    }
+
+    protected function _getEmpty()
+    {
+        return BxDolTemplate::getInstance()->parseHtmlByName('comment_empty.html', array(
+            'style_prefix' => $this->_sStylePrefix,
+            'content' => MsgBox(_t('_Empty'))
+        ));
+    }
+
+    protected function _getViewImagePopup()
+    {
+    	$sViewImagePopupId = 'cmts-box-' . $this->_sSystem . '-' . $this->getId() . '-view-image-popup' ;
+        $sViewImagePopupContent = BxDolTemplate::getInstance()->parseHtmlByName('popup_image.html', array(
+    		'image_url' => ''
+    	));
+
+    	return BxTemplFunctions::getInstance()->transBox($sViewImagePopupId, $sViewImagePopupContent, true);
+    }
+
+    protected function _getAttachments($aCmt)
+    {
+        $aTmplImages = array();
+        if(!$this->isAttachImageEnabled())
+            return ''; 
+
+        $aFiles = $this->_oQuery->getFiles($this->_aSystem['system_id'], $aCmt['cmt_id']);
+        if(!empty($aFiles) && is_array($aFiles)) {
+    		$oStorage = BxDolStorage::getObjectInstance($this->getStorageObjectName());
+            $oTranscoder = BxDolTranscoderImage::getObjectInstance($this->getTranscoderPreviewName());
+
+            foreach($aFiles as $aFile) {
+            	$bImage = $oTranscoder && $oTranscoder->isMimeTypeSupported($aFile['mime_type']);
+
+            	$sPreview = '';
+            	if($oTranscoder && $bImage)
+            		$sPreview = $oTranscoder->getFileUrl($aFile['image_id']);
+
+        		if(!$sPreview)
+            		$sPreview = $this->_oTemplate->getIconUrl($oStorage->getIconNameByFileName($aFile['file_name']));
+
+				$aTmplVarsFile = array(
+				    'js_object' => $this->_sJsObjName,
+					'preview' => $sPreview,
+					'file' => $oStorage->getFileUrlById($aFile['image_id']),
+        			'file_name' => $aFile['file_name'],
+                    'file_icon' => $oStorage->getFontIconNameByFileName($aFile['file_name']),
+				    'file_size' => _t_format_size($aFile['size']),
+				);
+
+                $aTmplImages[] = array(
+                    'style_prefix' => $this->_sStylePrefix,
+                	'bx_if:show_image' => array(
+                		'condition' => $bImage,
+                		'content' => $aTmplVarsFile
+                	),
+                    'bx_if:show_file' => array(
+                		'condition' => !$bImage,
+                		'content' => $aTmplVarsFile
+                	),
+                );
+            }
+        }
+
+        return $this->_oTemplate->parseHtmlByName('comment_attachments.html', array(
+            'bx_repeat:attached' => $aTmplImages
+        ));
+    }
+
+    protected function _getTmplVarsAuthor($aCmt)
+    {
+    	list($sAuthorName, $sAuthorLink, $sAuthorIcon, $sAuthorUnit) = $this->_getAuthorInfo($aCmt['cmt_author_id']);
+    	$bAuthorIcon = !empty($sAuthorIcon);
+
+    	return array(
+    	    'author_unit' => $sAuthorUnit,
+    		'bx_if:show_icon' => array(
+                'condition' => $bAuthorIcon,
+                'content' => array(
+                    'author_icon' => $sAuthorIcon
+                )
+            ),
+            'bx_if:show_icon_empty' => array(
+                'condition' => !$bAuthorIcon,
+                'content' => array()
+            ),
+            'bx_if:show_author_link' => array(
+                'condition' => !empty($sAuthorLink),
+                'content' => array(
+                    'author_link' => $sAuthorLink,
+            		'author_title' => bx_html_attribute($sAuthorName),
+                    'author_name' => $sAuthorName
+                )
+            ),
+            'bx_if:show_author_text' => array(
+                'condition' => empty($sAuthorLink),
+                'content' => array(
+                    'author_name' => $sAuthorName
+                )
+            ),
+    	);
+    }
+    protected function _getTmplVarsText($aCmt)
+    {
+    	$sText = $aCmt['cmt_text'];
+        $sTextMore = '';
+
+        if(!$this->_bHtml) {
+            $iMaxLength = (int)$this->_aSystem['chars_display_max'];
+            if(strlen($sText) > $iMaxLength) {
+                $iLength = strpos($sText, ' ', $iMaxLength);
+    
+                $sTextMore = trim(substr($sText, $iLength));
+                $sText = trim(substr($sText, 0, $iLength));
+            }
+        }
+
+        $sText = $this->_prepareTextForOutput($sText, $aCmt['cmt_id']);
+        $sTextMore = $this->_prepareTextForOutput($sTextMore, $aCmt['cmt_id']);
+
+        return array(
+			'text' => $sText,
+            'bx_if:show_more' => array(
+                'condition' => !empty($sTextMore),
+                'content' => array(
+                    'style_prefix' => $this->_sStylePrefix,
+                    'js_object' => $this->_sJsObjName,
+                    'text_more' => $sTextMore
+                )
+            ),
+        );
+    }
+}
+
+/** @} */

+ 261 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseEditorTinyMCE.php

@@ -0,0 +1,261 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * TinyMCE editor representation.
+ * @see BxDolEditor
+ */
+class BxBaseEditorTinyMCE extends BxDolEditor
+{
+    /**
+     * Common initialization params
+     */
+    protected static $CONF_COMMON = <<<EOS
+                    jQuery('{bx_var_selector}').tinymce({
+                        {bx_var_custom_init}
+                        {bx_var_custom_conf}
+                        document_base_url: '{bx_url_root}',
+                        theme_url: '{bx_url_tinymce}themes/modern/theme.min.js',
+                        skin_url: '{bx_url_tinymce}skins/{bx_var_skin}/',
+                        language_url: '{bx_url_tinymce}langs/{bx_var_lang}.js',
+                        content_css: '{bx_var_css_path}',
+                        entity_encoding: 'raw',
+                        browser_spellcheck: true,
+                        branding: false,
+                        body_class: 'bx-def-color-bg-block',
+                        mentions: {
+                            queryBy: 'label',
+                            source: function (query, process, delimiter) {
+                                if (delimiter === '@') {
+                                   $.getJSON('{bx_url_root}searchExtended.php?action=get_authors&term=' + query, function (data) {
+                                      process(data)
+                                   });
+                                }
+                            },
+                            insert: function(item) {
+                                return '<a data-profile-id="' + item.value + '" href="' + item.url + '">' + item.label + '</a>';
+                            },
+                            render: function(item) {
+                                return '<li><a href="javascript:;">' + item.label + '</a></li>';
+                            },
+                            renderDropdown: function() {
+                                return '<ul class="rte-autocomplete"></ul>';
+                            }
+                        }
+                    });
+EOS;
+
+    /**
+     * Standard view initialization params
+     */
+    protected static $CONF_STANDARD = "
+                        external_plugins: {
+                            advlist: '{bx_url_tinymce}plugins/advlist/plugin.min.js',
+                            autolink: '{bx_url_tinymce}plugins/autolink/plugin.min.js',
+                            autosave: '{bx_url_tinymce}plugins/autosave/plugin.min.js',
+                            code: '{bx_url_tinymce}plugins/code/plugin.min.js',
+                            hr: '{bx_url_tinymce}plugins/hr/plugin.min.js', 
+                            image: '{bx_url_tinymce}plugins/image/plugin.min.js',
+                            link: '{bx_url_tinymce}plugins/link/plugin.min.js',
+                            lists: '{bx_url_tinymce}plugins/lists/plugin.min.js',
+                            media: '{bx_url_tinymce}plugins/media/plugin.min.js',
+                            paste: '{bx_url_tinymce}plugins/paste/plugin.min.js',
+                            fullscreen: '{bx_url_tinymce}plugins/fullscreen/plugin.min.js',
+                            mention: '{bx_var_plugins_path}tinymce-mention/plugin.min.js',
+                        },
+                        width: '100%',
+                        height: '270',
+                        toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image',
+                        statusbar: true,
+                        resize: true,
+    ";
+
+    /**
+     * Minimal view initialization params
+     */
+    protected static $CONF_MINI = "
+                        menubar: false,
+                        external_plugins: {
+                            autolink: '{bx_url_tinymce}plugins/autolink/plugin.min.js',
+                            image: '{bx_url_tinymce}plugins/image/plugin.min.js',
+                            link: '{bx_url_tinymce}plugins/link/plugin.min.js',
+                            lists: '{bx_url_tinymce}plugins/lists/plugin.min.js',
+                            paste: '{bx_url_tinymce}plugins/paste/plugin.min.js',
+                            fullscreen: '{bx_url_tinymce}plugins/fullscreen/plugin.min.js',
+                            mention: '{bx_var_plugins_path}tinymce-mention/plugin.min.js',
+                        },
+                        width: '100%',
+                        height: '150',                        
+                        toolbar: 'bold italic underline removeformat | bullist numlist | alignleft aligncenter alignright | blockquote | link unlink image',
+                        statusbar: true,
+                        resize: true,
+    ";
+
+    /**
+     * Full view initialization params
+     */
+    protected static $CONF_FULL = "
+                        external_plugins: {
+                            advlist: '{bx_url_tinymce}plugins/advlist/plugin.min.js',
+                            anchor: '{bx_url_tinymce}plugins/anchor/plugin.min.js',
+                            autolink: '{bx_url_tinymce}plugins/autolink/plugin.min.js',
+                            autoresize: '{bx_url_tinymce}plugins/autoresize/plugin.min.js',
+                            autosave: '{bx_url_tinymce}plugins/autosave/plugin.min.js',
+                            charmap: '{bx_url_tinymce}plugins/charmap/plugin.min.js',
+                            code: '{bx_url_tinymce}plugins/code/plugin.min.js',
+                            emoticons: '{bx_url_tinymce}plugins/emoticons/plugin.min.js',
+                            hr: '{bx_url_tinymce}plugins/hr/plugin.min.js',
+                            image: '{bx_url_tinymce}plugins/image/plugin.min.js',
+                            link: '{bx_url_tinymce}plugins/link/plugin.min.js',
+                            lists: '{bx_url_tinymce}plugins/lists/plugin.min.js',
+                            media: '{bx_url_tinymce}plugins/media/plugin.min.js',
+                            nonbreaking: '{bx_url_tinymce}plugins/nonbreaking/plugin.min.js',
+                            pagebreak: '{bx_url_tinymce}plugins/pagebreak/plugin.min.js',
+                            preview: '{bx_url_tinymce}plugins/preview/plugin.min.js',
+                            print: '{bx_url_tinymce}plugins/print/plugin.min.js',
+                            paste: '{bx_url_tinymce}plugins/paste/plugin.min.js',
+                            save: '{bx_url_tinymce}plugins/save/plugin.min.js',
+                            searchreplace: '{bx_url_tinymce}plugins/searchreplace/plugin.min.js',
+                            table: '{bx_url_tinymce}plugins/table/plugin.min.js',
+                            textcolor: '{bx_url_tinymce}plugins/textcolor/plugin.min.js',
+                            visualblocks: '{bx_url_tinymce}plugins/visualblocks/plugin.min.js',
+                            fullscreen: '{bx_url_tinymce}plugins/fullscreen/plugin.min.js',
+                            mention: '{bx_var_plugins_path}tinymce-mention/plugin.min.js',
+                        },
+                        width: '100%',
+                        height: '320',
+                        toolbar: [
+                            'undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image',
+                            'print preview media | forecolor emoticons | fullscreen'
+                        ],
+                        statusbar: true,
+                        resize: true,
+                        image_advtab: true,
+    ";
+
+    protected $_sConfCustom = '';
+
+    /**
+     * Available editor languages
+     */
+    protected static $CONF_LANGS = array('af_ZA' => 1, 'ar' => 1, 'be' => 1, 'bg_BG' => 1, 'ca' => 1, 'cs' => 1, 'cy' => 1, 'da' => 1, 'de' => 1, 'de_AT' => 1, 'dv' => 1, 'el' => 1, 'en' => 1, 'en_CA' => 1, 'en_GB' => 1, 'es' => 1, 'es_MX' => 1, 'et' => 1, 'fa_IR' => 1, 'fr_FR' => 1, 'ga' => 1, 'gl' => 1, 'he_IL' => 1, 'hr' => 1, 'hu_HU' => 1, 'it' => 1, 'ja' => 1, 'ka_GE' => 1, 'kab' => 1, 'kk' => 1, 'km_KH' => 1, 'ko_KR' => 1, 'lv' => 1, 'nb_NO' => 1, 'nl' => 1, 'pl' => 1, 'pt_BR' => 1, 'pt_PT' => 1, 'ro' => 1, 'ru' => 1, 'sk' => 1, 'sl_SI' => 1, 'sv_SE' => 1, 'ta' => 1, 'ta_IN' => 1, 'th_TH' => 1, 'tr' => 1, 'tr_TR' => 1, 'uk' => 1, 'uk_UA' => 1, 'uz' => 1, 'vi_VN' => 1, 'zh_CN' => 1, 'zh_TW' => 1);
+
+    protected $_oTemplate;
+    protected $_bJsCssAdded = false;
+
+    public function __construct ($aObject, $oTemplate)
+    {
+        parent::__construct ($aObject);
+
+        if ($oTemplate)
+            $this->_oTemplate = $oTemplate;
+        else
+            $this->_oTemplate = BxDolTemplate::getInstance();
+    }
+
+    /**
+     * Set custom TinyMCE configuration option
+     */
+    public function setCustomConf ($s)
+    {
+        $this->_sConfCustom = $s;
+    }
+
+    /**
+     * Attach editor to HTML element, in most cases - textarea.
+     * @param $sSelector - jQuery selector to attach editor to.
+     * @param $iViewMode - editor view mode: BX_EDITOR_STANDARD, BX_EDITOR_MINI, BX_EDITOR_FULL
+     * @param $bDynamicMode - is AJAX mode or not, the HTML with editor area is loaded dynamically.
+     */
+    public function attachEditor ($sSelector, $iViewMode = BX_EDITOR_STANDARD, $bDynamicMode = false)
+    {
+        // set visual mode
+        switch ($iViewMode) {
+            case BX_EDITOR_MINI:
+                 $sToolsItems = self::$CONF_MINI;
+                break;
+            case BX_EDITOR_FULL:
+                $sToolsItems = self::$CONF_FULL;
+            break;
+            case BX_EDITOR_STANDARD:
+            default:
+                 $sToolsItems = self::$CONF_STANDARD;
+        }
+
+        // detect language
+        $sLang = BxDolLanguages::getInstance()->detectLanguageFromArray (self::$CONF_LANGS);
+
+        $sCss = 'editor.less';
+        $aCss = BxDolTemplate::getInstance()->_lessCss(array(
+        	'path' => $this->_oTemplate->getCssPath($sCss),
+        	'url' => $this->_oTemplate->getCssUrl($sCss)
+        ));
+
+        // initialize editor
+        $sInitEditor = $this->_replaceMarkers(self::$CONF_COMMON, array(
+            'bx_var_custom_init' => $sToolsItems,
+            'bx_var_custom_conf' => $this->_sConfCustom,
+            'bx_var_plugins_path' => bx_js_string(BX_DOL_URL_PLUGINS_PUBLIC, BX_ESCAPE_STR_APOS),
+            'bx_var_css_path' => bx_js_string($aCss['url'], BX_ESCAPE_STR_APOS),
+            'bx_var_skin' => bx_js_string($this->_aObject['skin'], BX_ESCAPE_STR_APOS),
+            'bx_var_lang' => bx_js_string($sLang, BX_ESCAPE_STR_APOS),
+            'bx_var_selector' => bx_js_string($sSelector, BX_ESCAPE_STR_APOS),
+            'bx_url_root' => bx_js_string(BX_DOL_URL_ROOT, BX_ESCAPE_STR_APOS),
+            'bx_url_tinymce' => bx_js_string(BX_DOL_URL_PLUGINS_PUBLIC . 'tinymce/', BX_ESCAPE_STR_APOS),
+        ));
+
+        if ($bDynamicMode) {
+
+            $sScript = "<script>
+                if ('undefined' == typeof(jQuery(document).tinymce)) {
+                    window.tinyMCEPreInit = {base : '" . bx_js_string(BX_DOL_URL_PLUGINS_PUBLIC . 'tinymce', BX_ESCAPE_STR_APOS) . "', suffix : '.min', query : ''};
+                    $.getScript('" . bx_js_string(BX_DOL_URL_ROOT . 'inc/js/editor.tinymce.js', BX_ESCAPE_STR_APOS) . "');
+                    $.getScript('" . bx_js_string(BX_DOL_URL_PLUGINS_PUBLIC . 'tinymce/tinymce.min.js', BX_ESCAPE_STR_APOS) . "', function(data, textStatus, jqxhr) {
+                        $.getScript('" . bx_js_string(BX_DOL_URL_PLUGINS_PUBLIC . 'tinymce/jquery.tinymce.min.js', BX_ESCAPE_STR_APOS) . "', function(data, textStatus, jqxhr) {
+                            $sInitEditor
+                        });
+                    });
+                } else {
+                	setTimeout(function () {
+                    	$sInitEditor
+                    }, 10); // wait while html is rendered in case of dynamic adding html with tinymce
+                }
+            </script>";
+
+        } else {
+
+            $sScript = "
+            <script>
+                $(document).ready(function () {
+                    $sInitEditor
+                });
+            </script>";
+
+        }
+
+        return $this->_addJsCss($bDynamicMode) . $sScript;
+    }
+
+    /**
+     * Add css/js files which are needed for editor display and functionality.
+     */
+    protected function _addJsCss($bDynamicMode = false, $sInitEditor = '')
+    {
+        if ($bDynamicMode)
+            return '';
+        if ($this->_bJsCssAdded)
+            return '';
+        $this->_oTemplate->addJs(array('tinymce/tinymce.min.js', 'tinymce/jquery.tinymce.min.js', 'editor.tinymce.js'));
+        $this->_bJsCssAdded = true;
+        return '';
+    }
+
+}
+
+/** @} */

+ 613 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseFunctions.php

@@ -0,0 +1,613 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+class BxBaseFunctions extends BxDolFactory implements iBxDolSingleton
+{
+    protected $_oTemplate;
+    protected $_sDesignBoxIcon = 'chevron';
+
+    protected function __construct($oTemplate)
+    {
+        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->_oTemplate = $oTemplate ? $oTemplate : BxDolTemplate::getInstance();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    public static function getInstance($oTemplate = null)
+    {
+        if(!isset($GLOBALS['bxDolClasses']['BxTemplFunctions']))
+            $GLOBALS['bxDolClasses']['BxTemplFunctions'] = new BxTemplFunctions($oTemplate);
+
+        return $GLOBALS['bxDolClasses']['BxTemplFunctions'];
+    }
+
+    function TemplPageAddComponent($sKey)
+    {
+        switch( $sKey ) {
+            case 'something':
+                return false; // return here additional components
+            default:
+                return false; // if you have not such component, return false!
+        }
+    }
+
+    function msgBox($sText, $iTimer = 0, $sOnClose = "")
+    {
+        $iId = time() . mt_rand(1, 1000);
+
+        if($iTimer > 0)
+            BxDolTemplate::getInstance()->addJs(array('jquery.anim.js'));
+
+        return $this->_oTemplate->parseHtmlByName('messageBox.html', array(
+            'id' => $iId,
+            'msgText' => $sText,
+            'bx_if:timer' => array(
+                'condition' => $iTimer > 0,
+                'content' => array(
+                    'id' => $iId,
+                    'time' => 1000 * $iTimer,
+                    'on_close' => $sOnClose,
+                )
+            )
+        ));
+    }
+
+    /**
+     * Get standard popup box with title.
+     *
+     * @param  string $sName    - unique name
+     * @param  string $sTitle   - translated title
+     * @param  string $sContent - content of the box
+     * @return HTML   string
+     */
+    function popupBox($sName, $sTitle, $sContent, $isHiddenByDefault = false)
+    {
+        $iId = !empty($sName) ? $sName : time();
+
+        return $this->_oTemplate->parseHtmlByName('popup_box.html', array(
+            'id' => $iId,
+            'wrapper_style' => $isHiddenByDefault ? 'display:none;' : '',
+            'title' => $sTitle,
+            'content' => $sContent
+        ));
+    }
+
+    /**
+     * Get popup box without title.
+     *
+     * @param  string $sName    - unique name
+     * @param  string $sContent - content of the box
+     * @return HTML   string
+     */
+    function transBox($sName, $sContent, $isHiddenByDefault = false, $isPlaceInCenter = false)
+    {
+    	return $this->simpleBox($sName, $sContent, $isHiddenByDefault, $isPlaceInCenter, 'popup_trans.html');
+    }
+
+    function slideBox($sName, $sContent, $isHiddenByDefault = false)
+    {
+    	return $this->simpleBox($sName, $sContent, $isHiddenByDefault, false, 'popup_slide.html');
+    }
+
+	function inlineBox($sName, $sContent, $isHiddenByDefault = false)
+    {
+        return $this->simpleBox($sName, $sContent, $isHiddenByDefault, false, 'popup_inline.html');
+    }
+
+    protected function simpleBox($sName, $sContent, $isHiddenByDefault, $isPlaceInCenter, $sTemplate) {
+    	$iId = !empty($sName) ? $sName : time();
+
+		$sContent = $this->_oTemplate->parseHtmlByName($sTemplate, array(
+            'id' => $iId,
+            'wrapper_style' => $isHiddenByDefault ? 'display:none;' : '',
+            'content' => $sContent
+        ));
+
+        if($isPlaceInCenter)
+        	$sContent = '<div class="login_ajax_wrap">' . $sContent . '</div>';
+
+        return $sContent;
+    }
+
+    function getTemplateIcon($sName)
+    {
+        $sUrl = $this->_oTemplate->getIconUrl($sName);
+        return !empty($sUrl) ? $sUrl : $this->_oTemplate->getIconUrl('spacer.gif');
+    }
+
+    function getTemplateImage($sName)
+    {
+        $sUrl = $this->_oTemplate->getImageUrl($sName);
+        return !empty($sUrl) ? $sUrl : $this->_oTemplate->getImageUrl('spacer.gif');
+    }
+
+    function sysIcon ($sIcon, $sName, $sUrl = '', $iWidth = 0)
+    {
+        return '<div class="sys_icon">' . ($sUrl ? '<a title="'.$sName.'" href="'.$sUrl.'">' : '') . '<img alt="'.$sName.'" src="'.$sIcon.'" '.($iWidth ? 'width='.$iWidth : '').' />' . ($sUrl ? '</a>' : '') . '</div>';
+    }
+
+    /**
+     * functions for limiting maximal string length
+     */
+    function getStringWithLimitedLength($sString, $iWidth = 45, $isPopupOnOverflow = false, $bReturnString = true)
+    {
+        if (empty($sString) || mb_strlen($sString, 'UTF-8') <= $iWidth)
+            return $bReturnString ? $sString : array($sString);
+
+        $sResult = '';
+        $aWords = mb_split("[\s\r\n]", $sString);
+        $iPosition = 0;
+        $iWidthReal = $iWidth - 3;
+        $iWidthMin = $iWidth - 15;
+        foreach($aWords as $sWord) {
+            $sWord = trim($sWord);
+            $iWord = mb_strlen($sWord, 'UTF-8');
+            if ($iPosition + $iWord > $iWidthReal)
+                break;
+
+            // add word and continue
+            $sResult .= ' ' . $sWord;
+            $iPosition += 1 + $iWord;
+        }
+
+        // last word is too long, cut it
+        if(!$iPosition || $iPosition < $iWidthMin)
+            $sResult .= ' ' . mb_substr($sWord, 0, $iWidthReal - $iPosition - $iWord, 'UTF-8');
+        $sResult = trim($sResult);
+
+        // add tripple dot
+        if(!$isPopupOnOverflow) {
+            $sResult .= '...';
+            return $bReturnString ? $sResult : array($sResult);
+        }
+
+        // add button width popup
+        $sId = 'bx-str-limit-' . rand(1, PHP_INT_MAX);
+        $sPopup = '<img class="bx-str-limit" onclick="$(\'#' . $sId . '\').dolPopup({pointer:{el:$(this), offset:\'10 1\'}})" src="' . $this->getTemplateImage('str-limit.png') . '"/>';
+        $sPopup .= '<div id="' . $sId . '" style="display:none;">' . BxTemplFunctions::getInstance()->transBox('', '<div class="bx-def-padding bx-def-color-bg-block">'.$sString.'</div>') . '</div>';
+
+        return $bReturnString ? $sResult . $sPopup : array($sResult, $sPopup);
+    }
+
+    /**
+     * Display design box with specified title, template, content and menu.
+     * @param $sTitle - design box title, please note that some templates don't use title.
+     * @param $sContent - design box content.
+     * @param $iTemplateNum - number of design box template, use predefined contants only, default is BX_DB_DEF.
+     * @param $mixedMenu - design box menu, it can be:
+     *      - object: instance of BxTemplMenu class
+     *      - string: menu object identifier
+     *      - array: array of menu links to create menu from
+     * @return string
+     *
+     * @see BX_DB_CONTENT_ONLY
+     * @see BX_DB_DEF
+     * @see BX_DB_EMPTY
+     * @see BX_DB_NO_CAPTION
+     * @see BX_DB_PADDING_CONTENT_ONLY
+     * @see BX_DB_PADDING_DEF
+     * @see BX_DB_PADDING_NO_CAPTION
+     */
+    function designBoxContent ($sTitle, $sContent, $iTemplateNum = BX_DB_DEF, $mixedMenu = false)
+    {
+        return $this->_oTemplate->parseHtmlByName('designbox_' . (int)$iTemplateNum . '.html', array(
+            'title' => $sTitle,
+            'designbox_content' => $sContent,
+            'caption_item' => $this->designBoxMenu ($mixedMenu, array (array('menu' => 1))),
+        ));
+    }
+
+    function designBoxMenu ($mixedMenu, $aButtons = array ())
+    {
+        $sCode = '';
+        $aButtonMenu = false;
+        if ($mixedMenu) {
+
+            $sMenu = '';
+
+            if (is_string($mixedMenu)) {
+
+                $oMenu = BxTemplMenu::getObjectInstance($mixedMenu);
+                $sMenu = $oMenu ? $oMenu->getCode () : $mixedMenu;
+
+            } elseif (is_object($mixedMenu) && is_a($mixedMenu, 'BxTemplMenu')) {
+
+                $sMenu = $mixedMenu->getCode();
+
+            } elseif (is_array($mixedMenu)) {
+
+                if (isset($mixedMenu['template']) && isset($mixedMenu['menu_items']))
+                    $aMenu = $mixedMenu;
+                else
+                    $aMenu = array ('template' => 'menu_vertical.html', 'menu_items' => $mixedMenu);
+
+                $oMenu = new BxTemplMenu($aMenu, $this->_oTemplate);
+                $sMenu = $oMenu->getCode ();
+            }
+
+            if ($sMenu) {
+                $sId = 'bx-menu-db-' . time() . rand(0, PHP_INT_MAX);
+                $sCode .= $this->slideBox($sId, $sMenu, true);
+                $aButtonMenu = array ('icon-a' => $this->_sDesignBoxIcon, 'onclick' => "bx_menu_slide('#" . $sId . "', this)");
+            }
+
+        }
+
+        if ($aButtons) {
+            $sCode .= '<div class="bx-db-menu"><div class="bx-db-menu-tab bx-db-menu-tab-btn">';
+            foreach ($aButtons as $aButton) {
+                if (isset($aButton['menu']) && $aButton['menu']) {
+                    if (!$aButtonMenu)
+                        continue;
+                    $aButton = $aButtonMenu;
+                }
+
+                $aAttrs = array ();
+                if (!empty($aButton['onclick']))
+                    $aAttrs['onclick'] = $aButton['onclick'];
+                if (!empty($aButton['class']))
+                    $aAttrs['class'] = $aButton['class'];
+                $sAttrs = bx_convert_array2attrs ($aAttrs, 'bx-def-margin-sec-left');
+
+                $sCode .= '<a href="javascript:void(0);" ' . $sAttrs . '>';
+                $sCode .= !empty($aButton['icon']) ? '<i class="sys-icon ' . $aButton['icon'] . ' bx-def-font-h2"></i>' : '';
+                $sCode .= !empty($aButton['icon-a']) ? '<i class="sys-icon-a" data-rotate="down" data-icon="' . $aButton['icon-a'] . '"></i>' : '';
+                $sCode .= !empty($aButton['title']) ? $aButton['title'] : '';
+                $sCode .= '</a>';
+            }
+            $sCode .= '</div></div>';
+        }
+
+        return $sCode;
+    }
+
+    /**
+     * Get logo URL.
+     * @return string
+     */
+    function getMainLogoUrl()
+    {
+		$oDesigns = BxDolDesigns::getInstance();
+
+        $iFileId = (int)$oDesigns->getSiteLogo();
+        if (!$iFileId) 
+            return false;
+
+        $oStorage = BxDolTranscoder::getObjectInstance('sys_custom_images');
+        $sFileUrl = $oStorage->getFileUrl($iFileId);
+        if (!$sFileUrl) 
+            return false;
+
+		$iLogoWidth = (int)$oDesigns->getSiteLogoWidth();
+        if($iLogoWidth > 0)
+            $sFileUrl = bx_append_url_params($sFileUrl, array('x' => $iLogoWidth));
+
+		$iLogoHeight = (int)$oDesigns->getSiteLogoHeight();
+        if($iLogoHeight > 0)
+            $sFileUrl = bx_append_url_params($sFileUrl, array('y' => $iLogoHeight));
+
+        return $sFileUrl;
+    }
+
+    /**
+     * Get logo HTML.
+     * @return string
+     */
+    function getMainLogo()
+    {
+		$oDesigns = BxDolDesigns::getInstance();
+
+        $sAlt = $oDesigns->getSiteLogoAlt();
+        if(empty($sAlt))
+        	$sAlt = getParam('site_title');
+
+        $sLogo = '<span>' . $sAlt . '</span>';
+
+        $sFileUrl = $this->getMainLogoUrl();
+        if (!empty($sFileUrl)) {
+        	$iLogoWidth = (int)$oDesigns->getSiteLogoWidth();
+            $sMaxWidth = $iLogoWidth > 0 ? 'max-width:' . round($iLogoWidth/16, 3) . 'rem;' : '';
+
+            $iLogoHeight = (int)$oDesigns->getSiteLogoHeight();
+            $sMaxHeight = $iLogoHeight > 0 ? 'max-height:' . round($iLogoHeight/16, 3) . 'rem;' : '';
+
+            $sLogo = '<img style="' . $sMaxWidth . $sMaxHeight . '" src="' . $sFileUrl . '" id="bx-logo" alt="' . bx_html_attribute($sAlt, BX_ESCAPE_STR_QUOTE) . '" />';
+        }
+
+        return '<a class="bx-def-font-contrasted" href="' . BX_DOL_URL_ROOT . '" title="' . bx_html_attribute($sAlt, BX_ESCAPE_STR_QUOTE) . '">' . $sLogo . '</a>';
+    }
+
+    /**
+     * Get HTML code for manifests.
+     * @return HTML string to insert into HEAD section
+     */
+    function getManifests()
+    {
+        $sRet = '';
+        $bLogged = isLogged();
+
+        /*
+         * OneSignal manifest must appear before any other <link rel="manifest" ...> in <head>
+         */
+        $sPushAppId = getParam('sys_push_app_id');
+        if($bLogged && !empty($sPushAppId)) {
+            $aUrlRoot = parse_url(BX_DOL_URL_ROOT);
+
+            $sUrl = BX_DOL_URL_PLUGINS_PUBLIC .  'onesignal/manifest.json.php';
+            $sUrl = bx_append_url_params($sUrl, array('bx_name' => $aUrlRoot['host']));
+
+            $sRet .= '<link rel="manifest" href="' . $sUrl . '" />';
+        }
+
+        return $sRet;
+    }
+
+    /**
+     * Get HTML code for meta icons.
+     * @return HTML string to insert into HEAD section
+     */
+    function getMetaIcons()
+    {
+        $iId = (int)getParam('sys_site_icon');
+        $sImageUrlFav = $sImageUrlFcb = $sImageUrlApl = '';
+
+        if(!empty($iId)) {
+
+            // favicon icon
+            $oTranscoder = BxDolTranscoderImage::getObjectInstance(BX_DOL_TRANSCODER_OBJ_ICON_FAVICON);
+            $sImageUrlFav = $oTranscoder->getFileUrl($iId);
+
+            // facebook icon
+            $oTranscoder = BxDolTranscoderImage::getObjectInstance(BX_DOL_TRANSCODER_OBJ_ICON_FACEBOOK);
+            $sImageUrlFcb = $oTranscoder->getFileUrl($iId);
+
+            // apple touch icon
+            $oTranscoder = BxDolTranscoderImage::getObjectInstance(BX_DOL_TRANSCODER_OBJ_ICON_APPLE);
+            $sImageUrlApl = $oTranscoder->getFileUrl($iId);
+        }
+
+        if(empty($sImageUrlFav))
+            $sImageUrlFav = $this->_oTemplate->getIconUrl('favicon.png');
+
+        if(empty($sImageUrlFcb))
+            $sImageUrlFcb = $this->_oTemplate->getIconUrl('facebook-icon.png');
+
+        if(empty($sImageUrlApl))
+            $sImageUrlApl = $this->_oTemplate->getIconUrl('apple-touch-icon.png');
+
+        $sRet = '';
+        $sRet .= '<link rel="shortcut icon" type="image/x-icon" href="' . $sImageUrlFav . '" />';
+        $sRet .= '<link rel="image_src" sizes="100x100" href="' . $sImageUrlFcb . '" />';
+        $sRet .= '<link rel="apple-touch-icon" sizes="152x152" href="' . $sImageUrlApl . '" />';
+
+        return $sRet;
+    }
+
+    function getInjectionHead() 
+    {
+        return $this->getInjection('getInjHead');
+    }
+
+    function getInjectionHeader() 
+    {
+        return $this->getInjection('getInjHeader');
+    }
+
+    function getInjectionFooter() 
+    {
+        return $this->getInjection('getInjFooter');
+    }
+
+    public function getPopupAlert()
+    {
+        return $this->transBox('bx-popup-alert', $this->_oTemplate->parseHtmlByName('popup_trans_alert_cnt.html', array()), true);
+    }
+
+    public function getPopupConfirm()
+    {
+        return $this->transBox('bx-popup-confirm', $this->_oTemplate->parseHtmlByName('popup_trans_confirm_cnt.html', array()), true);
+    }
+
+    public function getPopupPrompt()
+    {
+        $sInputText = 'bx-popup-prompt-value';
+        $aInputText = array(
+            'type' => 'text',
+            'name' => $sInputText,
+            'attrs' => array(
+                'id' => $sInputText,
+            ),
+            'value' => '',
+            'caption' => ''
+        );
+
+        $oForm = new BxTemplFormView(array(), $this->_oTemplate);
+        return $this->transBox('bx-popup-prompt', $this->_oTemplate->parseHtmlByName('popup_trans_prompt_cnt.html', array(
+            'input' => $oForm->genRow($aInputText)
+        )), true);
+    }
+
+    /**
+     * Output time wrapped in <time> tag in HTML.
+     * Then time is autoformatted using JS upon page load, this is aumatically converted to user's timezone and
+     * updated in realtime in case of short periods of 'from now' time format.
+     *
+     * Short version of this function:
+     * @see bx_time_js
+     *
+     * @param $iUnixTimestamp time as unixtimestamp
+     * @param $sFormatIdentifier output format identifier
+     *     @see BX_FORMAT_DATE
+     *     @see BX_FORMAT_TIME
+     *     @see BX_FORMAT_DATE_TIME
+     * @param $bForceFormat force provided format and don't use "from now" time autoformat.
+     */
+    function timeForJs ($iUnixTimestamp, $sFormatIdentifier = BX_FORMAT_DATE, $bForceFormat = false)
+    {
+        $sDateUTC = bx_time_utc ($iUnixTimestamp);
+        return $this->timeForJsFullDate ($sDateUTC, $sFormatIdentifier, $bForceFormat);
+    }
+
+    /**
+     * Same as @see timeForJs but instead of unxtimestamp full date format is used (ex: 2005-08-15T15:52:01) as passec date param
+     */ 
+    function timeForJsFullDate ($sDateUTC, $sFormatIdentifier = BX_FORMAT_DATE, $bForceFormat = false)
+    {
+        return '<time datetime="' . $sDateUTC . '" data-bx-format="' . getParam($sFormatIdentifier) . '" data-bx-autoformat="' . ($bForceFormat ? 0 : getParam('sys_format_timeago')) . '">' . $sDateUTC . '</time>';
+    }
+    
+    function statusOnOff ($mixed, $isMsg = false)
+    {
+        if ((is_bool($mixed) && !$mixed) || (is_string($mixed) && 'fail' == $mixed))
+            return '<i class="sys-icon circle col-red2"></i> ' . ($isMsg ? _t('_sys_off') : '');
+        elseif (is_string($mixed) && 'warn' == $mixed)
+            return '<i class="sys-icon circle col-red3"></i> ' . ($isMsg ? _t('_sys_warn') : '');
+        elseif (is_string($mixed) && 'undef' == $mixed)
+            return '<i class="sys-icon circle col-gray"></i> ' . ($isMsg ? _t('_undefined') : '');
+        else
+            return '<i class="sys-icon circle col-green1"></i> ' . ($isMsg ? _t('_sys_on') : '');
+    }
+
+    /**
+     * Ouputs HTML5 video player.
+     * @param $sUrlPoster video poster image
+     * @param $sUrlMP4 .mp4 video
+     * @param $sUrlWebM .webm video
+     * @param $aAttrs custom attributes, defaults are: controls="" preload="none" autobuffer=""
+     * @param $sStyles custom styles, defaults are: width:100%; height:auto;
+     */
+    function videoPlayer ($sUrlPoster, $sUrlMP4, $sUrlWebM = '', $aAttrs = false, $sStyles = 'width:100%; height:auto;')
+    {
+        $aAttrsDefaults = array(
+            'controls' => '',
+            'preload' => 'none',
+            'autobuffer' => '', 
+        );
+        $aAttrs = array_merge($aAttrsDefaults, is_array($aAttrs) ? $aAttrs : array());
+        $sAttrs = bx_convert_array2attrs($aAttrs, '', $sStyles);
+
+        return '<video ' . $sAttrs . ' poster="' . $sUrlPoster . '">
+                    ' . ($sUrlWebM ? '<source type="video/webm; codecs="vp8, vorbis" src="' . $sUrlWebM . '" />' : '') . '
+                    ' . ($sUrlMP4  ? '<source type="video/mp4" src="' . $sUrlMP4 . '" />' : '') . '
+                </video>';
+    }
+
+    protected function getInjection($sPrefix)
+    {
+        $sContent = '';
+
+        $aMethods = get_class_methods($this);
+        foreach($aMethods as $sMethod)
+            if(substr($sMethod, 0, strlen($sPrefix)) == $sPrefix)
+                $sContent .= $this->$sMethod();
+
+        return $sContent;
+    }
+
+    protected function getInjHeadLiveUpdates() 
+    {
+        return BxDolLiveUpdates::getInstance()->init();
+    }
+
+    protected function getInjHeaderPushNotifications() 
+    {
+        $iProfileId = bx_get_logged_profile_id();
+        if(empty($iProfileId))
+            return '';
+
+        $sAppId = getParam('sys_push_app_id');
+        if(empty($sAppId))
+            return '';
+
+        $sShortName = getParam('sys_push_short_name');
+        $sSafariWebId = getParam('sys_push_safari_id');
+
+        $sSubfolder = '/plugins_public/onesignal/';
+        $aUrl = parse_url(BX_DOL_URL_ROOT);
+        if(!empty($aUrl['path'])) {
+            $sPath = trim($aUrl['path'], '/');
+            if(!empty($sPath))
+                $sSubfolder = '/' . $sPath . $sSubfolder;
+        }
+
+        $this->_oTemplate->addJs(array(
+            'https://cdn.onesignal.com/sdks/OneSignalSDK.js',
+            'BxDolPush.js',
+        ));
+
+        $sJsClass = 'BxDolPush';
+        $sJsObject = 'oBxDolPush';
+
+        $sContent = "var " . $sJsObject . " = new " . $sJsClass . "(" . json_encode(array(
+            'sObjName' => $sJsObject,
+            'sSiteName' => getParam('site_title'),
+            'iProfileId' => $iProfileId,
+            'sAppId' => $sAppId,
+            'sShortName' => $sShortName,
+            'sSafariWebId' => $sSafariWebId,
+            'sSubfolder' => $sSubfolder,
+            'sNotificationUrl' => BX_DOL_URL_ROOT,
+            'sTxtNotificationRequest' => bx_js_string(_t('_sys_push_notification_request', getParam('site_title'))),
+            'sTxtNotificationRequestYes' => bx_js_string(_t('_sys_push_notification_request_yes')),
+            'sTxtNotificationRequestNo' => bx_js_string(_t('_sys_push_notification_request_no')),
+        )) . ");";
+
+        return $this->_oTemplate->_wrapInTagJsCode($sContent);
+    }
+
+    protected function getInjFooterPopupLoading() 
+    {
+        return $this->transBox('bx-popup-loading', $this->_oTemplate->parsePageByName('popup_loading.html', array()), true);  
+    }
+
+    protected function getInjFooterPopupMenus() 
+    {
+        $sContent = '';
+
+        $oSearch = new BxTemplSearch();
+        $oSearch->setLiveSearch(true);
+        $sContent .= $this->_oTemplate->parsePageByName('search.html', array(
+            'search_form' => $oSearch->getForm(BX_DB_CONTENT_ONLY),
+            'results' => $oSearch->getResultsContainer(),
+        ));
+
+        $sContent .= $this->_oTemplate->getMenu ('sys_site');
+        if(isLogged()) {
+            $sContent .= $this->_oTemplate->getMenu ('sys_add_content');
+            $sContent .= $this->_oTemplate->getMenu ('sys_account_popup');
+        }
+
+        return $sContent;
+    }
+
+    protected function getInjFooterPopups() 
+    {
+        $sContent = '';
+
+        $sContent .= $this->getPopupAlert();
+        $sContent .= $this->getPopupConfirm();
+        $sContent .= $this->getPopupPrompt();
+
+        return $sContent;
+    }
+}
+
+/** @} */

+ 651 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseGrid.php

@@ -0,0 +1,651 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * Grid representation.
+ * @see BxDolGrid
+ */
+class BxBaseGrid extends BxDolGrid
+{
+    protected $_oTemplate;
+    protected $_aPopupOptions = false;
+    protected $_aQueryAppend = false;
+    protected $_aQueryReset = false;
+    protected $_aConfirmMessages = false;
+    protected $_bSelectAll = false;
+    protected $_isDisplayPopupOnTextOverflow = true;
+
+    public function __construct ($aOptions, $oTemplate)
+    {
+        parent::__construct ($aOptions);
+
+        if ($oTemplate)
+            $this->_oTemplate = $oTemplate;
+        else
+            $this->_oTemplate = BxDolTemplate::getInstance();
+
+        $this->_aQueryReset = array($this->_aOptions['filter_get'], $this->_aOptions['order_get_field'], $this->_aOptions['order_get_dir'], $this->_aOptions['paginate_get_start'], $this->_aOptions['paginate_get_per_page']);
+    }
+
+    public function performActionDisplay()
+    {
+        require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+
+        echoJson(array('grid' => $this->getCode(false)));
+    }
+
+    public function performActionReorder()
+    {
+        $this->_replaceMarkers ();
+
+        $aOrder = bx_get($this->_sObject . '_row');
+        $iOrder = 0;
+        foreach ($aOrder as $mixedId)
+            $this->_updateOrder($mixedId, ++$iOrder);
+
+        echoJson(array());
+    }
+
+    public function performActionDelete()
+    {
+        $this->_replaceMarkers ();
+
+        $iAffected = 0;
+        $aIds = bx_get('ids');
+        if (!$aIds || !is_array($aIds)) {
+            echoJson(array());
+            exit;
+        }
+
+        foreach ($aIds as $mixedId)
+            $iAffected += $this->_delete($mixedId) ? 1 : 0;
+
+        echo echoJson(array_merge(
+            array(
+                'grid' => $this->getCode(false),
+            ),
+            $iAffected ? array() : array('msg' => _t("_sys_grid_delete_failed"))
+        ));
+    }
+
+    public function performActionEnable()
+    {
+        $this->_replaceMarkers ();
+
+        $aIds = bx_get('ids');
+        if (!$aIds || !is_array($aIds)) {
+            echoJson(array());
+            exit;
+        }
+
+        $aAffectedIds = array ();
+        foreach ($aIds as $mixedId)
+            if ($this->_enable($mixedId, (int)bx_get('checked')))
+                $aAffectedIds[] = $mixedId;
+
+        $sAction = (int)bx_get('checked') ? 'enable' : 'disable';
+        echo echoJson(array($sAction => $aAffectedIds));
+    }
+
+    public function getCode ($isDisplayHeader = true)
+    {
+        $this->_replaceMarkers ();
+
+        if ($isDisplayHeader && empty($this->_aOptions['paginate_url'])) {
+            // reset page query params if grid is just initialized and it uses AJAX paginate
+            $this->resetQueryParams();
+        }
+
+        $sPaginate = '';
+        $aData = array();
+
+        $sIdWrapper = 'bx-grid-wrap-' . $this->_sObject;
+        $sIdContainer = 'bx-grid-cont-' . $this->_sObject;
+        $sIdTable = 'bx-grid-table-' . $this->_sObject;
+
+        $sFilter = bx_unicode_urldecode(bx_process_input(bx_get($this->_aOptions['filter_get'])));
+        $sOrderField = bx_unicode_urldecode(bx_process_input(bx_get($this->_aOptions['order_get_field'])));
+        $sOrderDir = 0 === strcasecmp('desc', bx_get($this->_aOptions['order_get_dir'])) ? 'DESC' : 'ASC';
+
+        if ($this->_aOptions['paginate_get_start'])
+            $iStart = (int)bx_get($this->_aOptions['paginate_get_start']);
+        else
+            $iStart = 0;
+
+        if ($this->_aOptions['paginate_get_per_page'] && (int)bx_get($this->_aOptions['paginate_get_per_page']) > 0)
+            $iPerPage = (int)bx_get($this->_aOptions['paginate_get_per_page']);
+        elseif ($this->_aOptions['paginate_per_page'])
+            $iPerPage = (int)$this->_aOptions['paginate_per_page'];
+        else
+            $iPerPage = 10;
+
+        if ($this->_aOptions['paginate_get_start']) {
+
+            $aData = $this->_getData ($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage + 1);
+
+            $sPageUrl = false;
+            if (!empty($this->_aOptions['paginate_url'])) {
+
+                $sPageUrl = $this->_aOptions['paginate_url'];
+
+                $aParamsAppend = array();
+                if ($sFilter) {
+                    $aParamsAppend['filter'] = bx_process_input(bx_get($this->_aOptions['filter_get']));
+                }
+                if ($sOrderField) {
+                    $aParamsAppend['order_field'] = bx_process_input(bx_get($this->_aOptions['order_get_field']));
+                    $aParamsAppend['order_dir'] = bx_process_input(bx_get($this->_aOptions['order_get_dir']));
+                }
+                if ($aParamsAppend)
+                    $sPageUrl = bx_append_url_params($sPageUrl, $aParamsAppend);
+            }
+
+            $aPaginateParams = array(
+                'start' => $iStart,
+                'per_page' => $iPerPage,
+                'page_url' =>  $sPageUrl ? $sPageUrl : "javascript:glGrids." . $this->_sObject . ".reload('{start}'); void(0);",
+            );
+
+            $oPaginate = new BxTemplPaginate($aPaginateParams, $this->_oTemplate);
+            $oPaginate->setNumFromDataArray($aData);
+
+            if (isset($this->_aOptions['paginate_simple']) && false !== $this->_aOptions['paginate_simple'])
+                $sPaginate = $oPaginate->getSimplePaginate($this->_aOptions['paginate_simple']);
+            else
+                $sPaginate = $oPaginate->getPaginate();
+
+        } else {
+
+            $aData = $this->_getData ($sFilter, $sOrderField, $sOrderDir, $iStart, $iPerPage);
+
+        }
+
+        if((empty($aData) || !is_array($aData)) && isset($this->_aBrowseParams['empty_message']) && !(bool)$this->_aBrowseParams['empty_message'])
+            return '';
+
+        $sPopupOptions = '{}';
+        if (!empty($this->_aPopupOptions) && is_array($this->_aPopupOptions))
+            $sPopupOptions = json_encode($this->_aPopupOptions);
+
+        $aQueryAppend = array_merge(is_array($this->_aQueryAppend) ? $this->_aQueryAppend : array(), is_array($this->_aMarkers) ? $this->_aMarkers : array());
+        $sQueryAppend = '{}';
+        if (!empty($aQueryAppend) && is_array($aQueryAppend))
+            $sQueryAppend = json_encode($aQueryAppend);
+
+        $sConfirmMessages = '{}';
+        if (!empty($this->_aConfirmMessages) && is_array($this->_aConfirmMessages))
+            $sConfirmMessages = json_encode($this->_aConfirmMessages);
+
+		$iColumns = count($this->_aOptions['fields']);
+		$aVarsHead = $this->_getRowHead();
+
+        $aVars = array (
+            'object' => $this->_sObject,
+            'id_table' => $sIdTable,
+            'id_cont' => $sIdContainer,
+            'id_wrap' => $sIdWrapper,
+            'sortable' => empty($this->_aOptions['field_order']) ? 0 : 1,
+            'sorting' => empty($this->_aOptions['sorting_fields']) ? 0 : 1,
+            'sorting_field' => $sOrderField,
+            'sorting_dir' => $sOrderDir,
+        	'bx_if:display_head' => array(
+        		'condition' => !empty($aVarsHead),
+        		'content' => array(
+        			'bx_repeat:row_header' => $aVarsHead,
+        			'columns' => $iColumns,
+        		)
+        	),
+            'bx_repeat:rows_data' => $this->_getRowsDataDesign ($aData),
+            'paginate_get_start' => $this->_aOptions['paginate_get_start'],
+            'paginate_get_per_page' => $this->_aOptions['paginate_get_per_page'],
+            'start' => $iStart,
+            'per_page' => $iPerPage,
+            'filter' => bx_js_string($sFilter, BX_ESCAPE_STR_APOS),
+            'filter_get' => bx_js_string($this->_aOptions['filter_get']),
+            'order_field' => bx_js_string($sOrderField, BX_ESCAPE_STR_APOS),
+            'order_dir' => bx_js_string($sOrderDir, BX_ESCAPE_STR_APOS),
+            'order_get_field' => bx_js_string($this->_aOptions['order_get_field']),
+            'order_get_dir' => bx_js_string($this->_aOptions['order_get_dir']),
+            'popup_options' => $sPopupOptions,
+            'query_append' => $sQueryAppend,
+            'confirm_messages' => $sConfirmMessages,
+            'columns' => $iColumns,
+            'bx_if:display_footer' => array(
+        		'condition' => !empty($this->_aOptions['actions_bulk']) || !empty($sPaginate),
+        		'content' => array(
+		            'bx_if:actions_bulk' => array (
+		                'condition' => !empty($this->_aOptions['actions_bulk']),
+		                'content' => array(
+		                    'actions_bulk' => $this->_getActions ('bulk'),
+		                ),
+		            ),
+		            'paginate' => $sPaginate,
+				)
+			),
+            'bx_if:display_header' => array (
+                'condition' => $isDisplayHeader,
+                'content' => array (
+                    'bx_if:actions_independent' => array (
+                        'condition' => !empty($this->_aOptions['actions_independent']),
+                        'content' => array(
+                            'actions_independent' => $this->_getActions ('independent'),
+                        ),
+                    ),
+                    'bx_if:filter' => array (
+                        'condition' => !empty($this->_aOptions['filter_fields']) || !empty($this->_aOptions['filter_fields_translatable']),
+                        'content' => array(
+                            'controls' => $this->_getFilterControls(),
+                        ),
+                    ),
+                ),
+            ),
+        );
+
+        $this->_addJsCss();
+
+        return $this->_oTemplate->parseHtmlByName('grid.html', $aVars);
+    }
+
+    /**
+     * Reset query params, like filter and page number
+     */
+    public function resetQueryParams()
+    {
+        foreach ($this->_aQueryReset as $sKey) {
+            unset($_GET[$sKey]);
+            unset($_POST[$sKey]);
+        }
+    }
+
+    protected function _getRowHead ()
+    {
+        $aRet = array();
+        foreach ($this->_aOptions['fields'] as $sKey => $a) {
+
+            $sMethod = '_getCellHeaderDefault';
+            $sCustomMethod = '_getCellHeader' . $this->_genMethodName($sKey);
+            if (method_exists($this, $sCustomMethod))
+                $sMethod = $sCustomMethod;
+
+            $aRet[] = array('header_cell' => $this->$sMethod($sKey, $a));
+        }
+        return $aRet;
+    }
+
+    protected function _getCellHeaderDefault ($sKey, $aField)
+    {
+        $sHeader = bx_process_output($aField['title']);
+
+        if (!empty($this->_aOptions['sorting_fields']) && is_array($this->_aOptions['sorting_fields']) && in_array($sKey, $this->_aOptions['sorting_fields'])) {
+            $sHeader = '<a href="javascript:void(0);" class="bx-grid-sort-handle">' . $sHeader . '</a><span class="bx-grid-sort-indi"></span>';
+            $aField['attr_head']['bx_grid_sort_head'] = $sKey;
+        }
+
+        $sAttr = $this->_convertAttrs(
+            $aField, 'attr_head',
+            'bx-def-padding-sec-bottom bx-def-padding-sec-top bx-grid-header-' . $sKey, // add default classes
+            isset($aField['width']) ? 'width:' . $aField['width'] : false // add default styles
+        );
+
+        return $this->_getCellHeaderWrapper ($sKey, $aField, $sHeader, $sAttr);
+    }
+
+    protected function _getCellHeaderCheckbox ($sKey, $aField)
+    {
+    	$aAttr = array(
+    		'type' => 'checkbox',
+    		'id' => $this->_sObject . '_check_all',
+    		'name' => $this->_sObject . '_check_all',
+    		'onclick' => "$('input[name=" . $this->_sObject . "_check]:not([disabled])').attr('checked', this.checked)"
+    	);
+    	if($this->_bSelectAll)
+    		$aAttr['checked'] = 'checked';
+
+    	$aField['attr'] = isset($aField['attr']) && is_array($aField['attr']) ? array_merge($aAttr, $aField['attr']) : $aAttr;
+
+        $sAttrHead = $this->_convertAttrs(
+            $aField, 'attr_head',
+            'bx-def-padding-sec-bottom bx-def-padding-sec-top bx-grid-header-' . $sKey, // add default classes
+            isset($aField['width']) ? 'width:' . $aField['width'] : false // add default styles
+        );
+        return $this->_getCellHeaderWrapper ($sKey, $aField, ' <input ' . $this->_convertAttrs($aField, 'attr') . ' /> ', $sAttrHead);
+    }
+
+    protected function _getCellHeaderWrapper ($sKey, $aField, $sHeader, $sAttr)
+    {
+        return '<th ' . $sAttr . '>' . $sHeader . '</th>';
+    }
+
+    /**
+     * Check if the whole row is disabled.
+     * When row is disabled - checkbox is not selectable, actions aren't clickable and text is grayed out.
+     * By default all rows aren't disabled.
+     * @param $aRow row array
+     * @return boolean
+     */
+    protected function _isRowDisabled($aRow)
+    {
+        if (isset($aRow[$this->_aOptions['field_active']]) && !$this->_switcherState2Checked($aRow[$this->_aOptions['field_active']]))
+            return true;
+        return false;
+    }
+
+    /**
+     * Determine how actions are disabled when whole row is disabled.
+     * @param $aRow row array
+     * @return null - disable/enable actions when row is disabled/enabled, true - actions are always disabled, false - actions are always enabled
+     */
+    protected function _getActionsDisabledBehavior($aRow)
+    {
+        return null;
+    }
+
+    /**
+     * Check if the checkbox is disabled.
+     * @param $aRow row array
+     * @return boolean
+     */
+    protected function _isCheckboxDisabled($aRow)
+    {
+        return $this->_isRowDisabled($aRow);
+    }
+
+    /**
+     * Is checkbox checked by default ?
+     * By default no one checkbox is selected.
+     * @return boolean
+     */
+    protected function _isCheckboxSelected($mixedValue, $sKey, $aField, $aRow)
+    {
+        return $this->_bSelectAll;
+    }
+
+    /**
+     * Is switcher on by default ?
+     * By default no one switcher is on.
+     * @return boolean
+     */
+    protected function _isSwitcherOn($mixedValue, $sKey, $aField, $aRow)
+    {
+        return $this->_switcherState2Checked($mixedValue);
+    }
+
+    /**
+     * Convert switcher checked status to the actual value ?
+     * @return boolean
+     */
+    protected function _switcherChecked2State($isChecked)
+    {
+        return $isChecked ? 1 : 0;
+    }
+
+    /**
+     * Convert switcher value to checked(boolean) value ?
+     * @return boolean
+     */
+    protected function _switcherState2Checked($mixedState)
+    {
+        return $mixedState ? true : false;
+    }
+
+    protected function _getRowsDataDesign (&$aData)
+    {
+        $aGrid = array();
+        if (empty($aData)) {
+
+            $aGrid[] = array(
+                'row' => '<td class="bx-def-padding-sec-bottom bx-def-padding-sec-top" colspan="' . count($this->_aOptions['fields']) . '">' . MsgBox(_t('_Empty')) . '</td>',
+                'id_row' => 0,
+                'row_class' => 'bx-grid-table-row-empty',
+            );
+
+        } else {
+
+            foreach ($aData as $aRow) {
+                $sRow = '';
+                foreach ($this->_aOptions['fields'] as $sKey => $aField)
+                    $sRow .= $this->_getCellDesign($sKey, $aField, $aRow);
+
+                $aGrid[] = array(
+                    'row' => $sRow,
+                    'id_row' => $this->_sObject . '_row_' . $aRow[$this->_aOptions['field_id']],
+                    'row_class' => $this->_isRowDisabled($aRow) ? 'bx-grid-table-row-disabled bx-def-font-grayed' : '',
+                );
+            }
+        }
+        return $aGrid;
+    }
+
+    protected function _getCellDesign($sKey, $aField, $aRow)
+    {
+        $sMethod = '_getCellDefault';
+        if ($this->_aOptions['field_order'] == $sKey)
+            $sMethod = '_getCellOrder';
+
+        $sCustomMethod = '_getCell' . $this->_genMethodName($sKey);
+        if (method_exists($this, $sCustomMethod))
+            $sMethod = $sCustomMethod;
+
+        $mixedValue = $this->_getCellData($sKey, $aField, $aRow);
+
+        if ($aField['translatable'])
+            $mixedValue = _t($mixedValue);
+
+        $mixedValue = $this->_limitMaxLength($mixedValue, $sKey, $aField, $aRow, $this->_isDisplayPopupOnTextOverflow);
+
+        return $this->$sMethod($mixedValue, $sKey, $aField, $aRow);
+    }
+
+    protected function _getCellCheckbox ($mixedValue, $sKey, $aField, $aRow)
+    {
+        $sAttr = $this->_convertAttrs(
+            $aField, 'attr_cell',
+            'bx-def-padding-sec-bottom bx-def-padding-sec-top', // add default classes
+            isset($aField['width']) ? 'width:' . $aField['width'] : false  // add default styles
+        );
+        $sDisabled = ($this->_isCheckboxDisabled($aRow) ? 'disabled="disabled"' : '');
+        $sSelected = ($this->_isCheckboxSelected($mixedValue, $sKey, $aField, $aRow) ? 'checked="checked"' : '');
+        $sVal = $aRow[$this->_aOptions['field_id']];
+        return '<td ' . $sAttr . '><input type="checkbox" name="'. $this->_sObject . '_check" value="' . $sVal . '" ' . $sDisabled . ' ' . $sSelected . '/></td>';
+    }
+
+    protected function _getCellSwitcher ($mixedValue, $sKey, $aField, $aRow)
+    {
+        $sAttr = $this->_convertAttrs(
+            $aField, 'attr_cell',
+            'bx-def-padding-sec-bottom bx-def-padding-sec-top', // add default classes
+            isset($aField['width']) ? 'width:' . $aField['width'] : false  // add default styles
+        );
+
+        $oForm = new BxTemplFormView(array(), $this->_oTemplate);
+        $oForm->addCssJs();
+        $aInput = array(
+            'type' => 'switcher',
+            'name' => $this->_sObject . '_switch_' . $aRow[$this->_aOptions['field_id']],
+            'caption' => '',
+            'attrs' => array (
+                'bx_grid_action_single' => 'enable',
+                'bx_grid_action_confirm' => '',
+                'bx_grid_action_data' => $aRow[$this->_aOptions['field_id']],
+            ),
+            'value' => $aRow[$this->_aOptions['field_id']],
+            'checked' => $this->_isSwitcherOn(isset($aRow[$this->_aOptions['field_active']]) ? $aRow[$this->_aOptions['field_active']] : false, $sKey, $aField, $aRow),
+        );
+        $sSwitcher = $oForm->genInput($aInput);
+        return '<td ' . $sAttr . '>' . $sSwitcher . '</td>';
+    }
+
+    protected function _getCellOrder ($mixedValue, $sKey, $aField, $aRow)
+    {
+        $sAttr = $this->_convertAttrs(
+            $aField, 'attr_cell',
+            'bx-def-padding-sec-bottom bx-def-padding-sec-top', // add default classes
+            isset($aField['width']) ? 'width:' . $aField['width'] : false  // add default styles
+        );
+        return '<td ' . $sAttr . '><div id="' . $this->_sObject . '_cell_' . $aRow[$this->_aOptions['field_id']] . '" class="bx-grid-drag-handle"><i class="sys-icon align-justify"></i></div></td>';
+    }
+
+    protected function _getCellActions ($mixedValue, $sKey, $aField, $aRow)
+    {
+        $sAttr = $this->_convertAttrs(
+            $aField, 'attr_cell',
+            'bx-def-padding-sec-bottom bx-def-padding-sec-top', // add default classes
+            isset($aField['width']) ? 'width:' . $aField['width'] : false  // add default styles
+        );
+
+        $mixedDisabledBehavior = $this->_getActionsDisabledBehavior($aRow);
+        $sActions = $this->_getActions('single', $aRow[$this->_aOptions['field_id']], false, null === $mixedDisabledBehavior ? $this->_isRowDisabled($aRow) : $mixedDisabledBehavior, null !== $mixedDisabledBehavior, $aRow);
+
+        return '<td ' . $sAttr . '><div class="bx-grid-cell-single-actions-wrapper bx-def-margin-thd-neg">' . $sActions . '</div></td>';
+    }
+
+    protected function _getCellDefault ($mixedValue, $sKey, $aField, $aRow)
+    {
+        $sAttr = $this->_convertAttrs(
+            $aField, 'attr_cell',
+            'bx-def-padding-sec-bottom bx-def-padding-sec-top', // add default classes
+            isset($aField['width']) ? 'width:' . $aField['width'] : false // add default styles
+        );
+        return '<td ' . $sAttr . '>' . $mixedValue . '</td>';
+    }
+
+    protected function _getActions ($sType, $sActionData = false, $isSmall = false, $isDisabled = false, $isPermanentState = false, $aRow = array())
+    {
+        $sActionsType = 'actions_' . $sType;
+        if (empty($this->_aOptions[$sActionsType]) || !is_array($this->_aOptions[$sActionsType]))
+            return '';
+        $sRet = '';
+        foreach ($this->_aOptions[$sActionsType] as $sKey => $a) {
+            $sFunc = '_getAction' . $this->_genMethodName($sKey);
+            if (!method_exists($this, $sFunc))
+                $sFunc = empty($a) ? '_getActionDivider' : '_getActionDefault';
+
+            if (!isset($a['attr']['bx_grid_action'])) {
+                $a['attr']['bx_grid_action_' . $sType] = $sKey;
+                if (!empty($sActionData))
+                    $a['attr']['bx_grid_action_data'] = $sActionData;
+                if (isset($a['confirm']))
+                    $a['attr']['bx_grid_action_confirm'] = $a['confirm'] ? 1 : 0;
+                $a['attr']['bx_grid_action_reset_paginate'] = false === strpos($sKey, 'delete') ? 0 : 1; // reset paginate after deleting row 
+            }
+
+            if ($isPermanentState && !isset($a['attr']['bx_grid_permanent_state']))
+                $a['attr']['bx_grid_permanent_state'] = 1;
+
+            $sRet .= $this->$sFunc($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+        }
+        return $sRet;
+    }
+
+    protected function _getActionDefault ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if ($a['icon_only'] && empty($a['attr']['title']) && !empty($a['title']))
+            $a['attr']['title'] = $a['title'];
+
+        $sAttr = $this->_convertAttrs(
+            $a, 'attr',
+            'bx-btn bx-def-margin-thd' . ($isSmall ? ' bx-btn-small' : '') . ($isDisabled ? ' bx-btn-disabled' : '') // add default classes
+        );
+
+        $sIcon = '';
+        $sImage = '';
+        if (!empty($a['icon'])) {
+            if (false === strpos($a['icon'], '.'))
+                $sIcon = '<i class="sys-icon ' . $a['icon'] . '"></i>';
+            elseif ($sIconUrl = $this->_oTemplate->getIconUrl($a['icon']))
+                $sImage = '<img style="background-image:url(' . $sIconUrl . ');" src="' . $this->_oTemplate->getIconUrl('spacer.gif') .'" />';
+        }
+        return '<button ' . $sAttr . '>' . $sIcon . $sImage . ($a['icon_only'] || empty($a['title']) ? '' : '<u>' . $a['title'] . '</u>') . '</button>';
+    }
+
+    protected function _getActionDivider ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        return '<div class="bx-grid-actions-divider bx-def-margin-sec-left"> | </div>';
+    }
+
+    protected function _getFilterControls ()
+    {
+        $oForm = new BxTemplStudioFormView(array());
+
+        $aInput = array(
+            'type' => 'text',
+            'name' => 'keyword',
+            'attrs' => array(
+                'id' => 'bx-grid-search-' . $this->_sObject
+            )
+        );
+
+        $this->_oTemplate->addCss('forms.css');
+        return $oForm->genRow($aInput);
+    }
+
+    protected function _limitMaxLength ($mixedValue, $sKey, $aField, $aRow, $isDisplayPopupOnTextOverflow, $bReturnString = true)
+    {
+        if ($aField['chars_limit'] > 0)
+            $mixedValue = BxTemplFunctions::getInstance()->getStringWithLimitedLength($mixedValue, $aField['chars_limit'], $isDisplayPopupOnTextOverflow, $bReturnString);
+        return $mixedValue;
+    }
+
+    protected function _convertAttrs ($aField, $sAttrName, $sClasses = false, $sStyles = false)
+    {
+        return bx_convert_array2attrs(
+            isset($aField[$sAttrName]) && is_array($aField[$sAttrName]) ? $aField[$sAttrName] : array(),
+            $sClasses,
+            $sStyles
+        );
+    }
+
+    protected function _updateOrder($mixedId, $iOrder)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sTable = $this->_aOptions['table'];
+        $sFieldId = $this->_aOptions['field_id'];
+        $sFieldOrder = $this->_aOptions['field_order'];
+        $sQuery = $oDb->prepare("UPDATE `{$sTable}` SET `{$sFieldOrder}` = ? WHERE `{$sFieldId}` = ?", $iOrder, $mixedId);
+        return $oDb->query($sQuery);
+    }
+
+    protected function _delete ($mixedId)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sTable = $this->_aOptions['table'];
+        $sFieldId = $this->_aOptions['field_id'];
+        $sQuery = $oDb->prepare("DELETE FROM `{$sTable}` WHERE `{$sFieldId}` = ?", $mixedId);
+        return $oDb->query($sQuery);
+    }
+
+    protected function _enable ($mixedId, $isChecked)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sTable = $this->_aOptions['table'];
+        $sFieldId = $this->_aOptions['field_id'];
+        $sFieldActive = $this->_aOptions['field_active'];
+        $sQuery = $oDb->prepare("UPDATE `{$sTable}` SET `$sFieldActive` = ? WHERE `{$sFieldId}` = ?", $this->_switcherChecked2State($isChecked), $mixedId);
+        return $oDb->query($sQuery);
+    }
+
+    protected function _addJsCss()
+    {
+        if ($this->_aOptions['field_order']) {
+            $this->_oTemplate->addJs(array(
+                'jquery-ui/jquery.ui.core.min.js',
+                'jquery-ui/jquery.ui.widget.min.js',
+                'jquery-ui/jquery.ui.mouse.min.js',
+                'jquery-ui/jquery.ui.sortable.min.js',
+            ));
+        }
+
+        $this->_oTemplate->addJs('BxDolGrid.js');
+        $this->_oTemplate->addCss('grid.css');
+
+        $this->_oTemplate->addJsTranslation('_sys_grid_confirmation');
+    }
+}
+
+/** @} */

+ 58 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseMenuCustom.php

@@ -0,0 +1,58 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ *
+ * @{
+ */
+
+class BxBaseMenuCustom extends BxTemplMenu
+{
+    protected static $_sTmplContentCustomItem;
+
+    public function __construct($aObject, $oTemplate = false)
+    {
+        parent::__construct($aObject, $oTemplate);
+
+        if(empty(self::$_sTmplContentCustomItem))
+            self::$_sTmplContentCustomItem = $this->_oTemplate->getHtml('menu_custom_item.html');
+    }
+
+    protected function _getMenuItem ($aItem)
+    {
+    	if (isset($aItem['active']) && !$aItem['active'])
+			return false;
+
+		if (isset($aItem['visible_for_levels']) && !$this->_isVisible($aItem))
+        	return false;
+
+    	$sMethod = '_getMenuItem' . str_replace(' ', '', ucwords(str_replace('-', ' ', $aItem['name'])));
+
+    	if(!method_exists($this, $sMethod)) {
+    		$aItem = parent::_getMenuItem($aItem);
+    		if($aItem === false)
+    			return false;
+
+			$sItem = $this->_getMenuItemDefault($aItem);
+    	}
+    	else
+    		$sItem = $this->$sMethod($aItem);
+
+    	if(empty($sItem))
+    		return false;
+
+		return array(
+			'class' => $this->_isSelected($aItem) ? 'bx-menu-tab-active' : '',
+			'item' => $sItem
+		);
+    }
+
+    protected function _getMenuItemDefault ($aItem)
+    {
+        return $this->_oTemplate->parseHtmlByContent(self::$_sTmplContentCustomItem, $aItem);
+    }
+}
+
+/** @} */

+ 92 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseMenuToolbar.php

@@ -0,0 +1,92 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * Menu representation.
+ * @see BxDolMenu
+ */
+class BxBaseMenuToolbar extends BxTemplMenu
+{
+    public function __construct ($aObject, $oTemplate)
+    {
+        parent::__construct ($aObject, $oTemplate);
+    }
+
+    public function getMenuItems ()
+    {
+        $a = parent::getMenuItems ();
+
+        if (!isLogged())
+            return $a;
+
+        foreach ($a as $k => $r) {
+            if($r['name'] != 'account')
+                continue;
+
+            if($a[$k]['bx_if:image']['condition'])
+                break;
+
+            $oProfile = BxDolProfile::getInstance(bx_get_logged_profile_id());
+
+            $sUnit = $oProfile->getUnit(0, array('template' => 'unit_wo_info_links'));
+            if(!empty($sUnit)) {
+                $a[$k]['bx_if:icon']['condition'] = false;
+                $a[$k]['bx_if:unit'] = array (
+                    'condition' => true,
+                    'content' => array('unit' => $sUnit),
+                );
+
+                break;
+            }
+
+            $sThumb = $oProfile->getThumb();
+            if(!empty($sThumb)) {
+                $a[$k]['bx_if:icon']['condition'] = false;
+                $a[$k]['bx_if:image'] = array (
+                    'condition' => true,
+                    'content' => array('icon_url' => $sThumb),
+                );
+
+                break;
+            }
+        }
+
+        return $a;
+    }
+
+    protected function _getMenuItem ($a)
+    {
+        $a = parent::_getMenuItem ($a);
+        if($a === false)
+            return $a;
+
+        $a['bx_if:unit'] = array(
+        	'condition' => false,
+        	'content' => array()
+        );
+
+        if ('add-content' == $a['name'] || 'search' == $a['name'])
+            $a['class_add'] .= ' bx-def-media-phone-hide';
+
+        return $a;
+    }
+
+    protected function _getTmplVarsAddon($mixedAddon, $aMenuItem)
+    {
+        return array(
+            'addon' => $mixedAddon,
+            'addonf' => $this->_oTemplate->parseHtmlByTemplateName('menu_item_addon', array(
+		        'content' => $mixedAddon
+		    ))
+			
+        );
+    }
+}
+
+/** @} */

+ 182 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseSearchExtended.php

@@ -0,0 +1,182 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * Extended Search objects representation.
+ * 
+ * @see BxDolSearchExtended
+ */
+class BxBaseSearchExtended extends BxDolSearchExtended
+{
+    protected $_sFormClassName;
+    protected $_sFormClassPath;
+
+    protected $_oForm;
+    protected $_oTemplate;
+
+    public function __construct ($aObject, $oTemplate = null)
+    {
+        parent::__construct ($aObject);
+
+        if ($oTemplate)
+            $this->_oTemplate = $oTemplate;
+        else
+            $this->_oTemplate = BxDolTemplate::getInstance();
+
+        $this->_sFormClassName = '';
+        $this->_sFormClassPath = '';
+
+        $this->_oForm = null;
+    }
+
+    public function getForm()
+    {
+        if(!$this->isEnabled())
+            return '';
+
+        $oForm = $this->prepareForm();
+        return $oForm->getCode();
+    }
+
+    /**
+     * Get search results from search form or from custom condition
+     *
+     * @param $aCondition custom condition to pass instead of form submission, 
+     *        conditions are key&value pair, where 'key' is form input name and 
+     *        'value' is the term to search for
+     * @param $sUnitTemplate custom unit templates to use
+     * @param $iStart position of first record to display 
+     * @param $iPerPage number of items per page
+     * @return HTML string with search results
+     */ 
+    public function getResults($aCondition = array(), $sUnitTemplate = '', $iStart = 0, $iPerPage = 0)
+    {
+        if(!$this->isEnabled())
+            return '';
+
+        $oForm = $this->prepareForm();
+        if ($aCondition) {
+            $mixedSubmitName = $oForm->aParams['db']['submit_name'];
+            if (is_array($mixedSubmitName))
+                $mixedSubmitName = array_pop($mixedSubmitName);
+            $aCondition[$mixedSubmitName] = 1;
+            $oForm->aFormAttrs['method'] = BX_DOL_FORM_METHOD_SPECIFIC;
+            $oForm->aParams['csrf']['disable'] = true;
+        }
+        $oForm->initChecker(array(), $aCondition);
+        if(!$oForm->isSubmittedAndValid()) 
+            return '';
+
+        $oContentInfo = BxDolContentInfo::getObjectInstance($this->_aObject['object_content_info']);
+        if(!$oContentInfo)
+            return '';
+
+        $aParams = array();
+        foreach($this->_aObject['fields'] as $aField) {
+            $sValue = $oForm->getCleanValue($aField['name']);
+            if(empty($sValue) || (is_array($sValue) && bx_is_empty_array($sValue)))
+                continue;
+
+            $aParams[$aField['name']] = array(
+                'type' => $aField['search_type'],
+                'value' => $sValue,
+                'operator' => $aField['search_operator']
+            );
+        }
+
+        if(empty($aParams) || !is_array($aParams))
+            return '';
+
+        if (!$iPerPage) {
+            bx_import('BxDolSearch');
+            $iPerPage = BX_DOL_SEARCH_RESULTS_PER_PAGE_DEFAULT;
+        }
+
+        $aResults = false;
+        bx_alert('search', 'get_data', 0, false, array('object' => $this->_aObject, 'search_params' => $aParams, 'search_results' => &$aResults));
+    	if($aResults === false)
+    	    $aResults = $oContentInfo->getSearchResultExtended($aParams, $iStart, $iPerPage);
+
+    	if(empty($aResults) || !is_array($aResults))
+    	    return '';
+
+    	$sResults = '';
+    	foreach($aResults as $iId)
+    	    $sResults .= $oContentInfo->getContentSearchResultUnit($iId, $sUnitTemplate);   	
+
+        return $sResults;
+    }
+
+    protected function &prepareForm()
+    {
+        if(!empty($this->_oForm) && $this->_oForm instanceof BxDolForm)
+            return $this->_oForm;
+
+        $sForm = 'sys_search_extended_' . $this->_sObject;
+        $sFormSubmit = 'search' . $this->_sObject;
+
+        $aForm = array(
+            'form_attrs' => array(
+                'id' => $sForm,
+                'name' => $sForm,
+                'action' => '',
+                'method' => 'post'
+            ),
+            'params' => array(
+                'db' => array(
+                    'table' => '',
+                    'key' => '',
+                    'uri' => '',
+                    'uri_title' => '',
+                    'submit_name' => $sFormSubmit
+                ),
+            ),
+            'inputs' => array()
+        );
+
+        foreach ($this->_aObject['fields'] as $aField) {
+            if((int)$aField['active'] == 0)
+                continue;
+
+            if(in_array($aField['search_type'], array('checkbox_set', 'select_multiple')) && isset($aField['values']['']))
+                unset($aField['values']['']);
+
+            $aForm['inputs'][$aField['name']] = array(
+                'type' => $aField['search_type'],
+                'name' => $aField['name'],
+                'caption' => _t($aField['caption']),
+                'values' => $aField['values'],
+                'value' => $aField['search_value'],
+                'db' => array(
+                    'pass' => !empty($aField['pass']) ? $aField['pass'] : 'Xss'
+                )
+            );
+        }
+
+        $aForm['inputs']['search'] = array(
+            'type' => 'submit',
+            'name' => $sFormSubmit,
+            'value' => _t('_Search')
+        );
+
+        $sClass = 'BxTemplSearchExtendedForm';
+        if(!empty($this->_sFormClassName)) {
+            $sClass = $this->_sFormClassName;
+            if(!empty($this->_sFormClassPath))
+                require_once(BX_DIRECTORY_PATH_ROOT . $this->_sFormClassPath);
+        }
+
+        $this->_oForm = new $sClass($aForm, $this->_oTemplate);
+        $this->_oForm->initChecker();
+
+        return $this->_oForm;
+    }
+}
+
+/** @} */

+ 52 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseSearchExtendedServices.php

@@ -0,0 +1,52 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * System services related to Extended Search.
+ * 
+ * @see BxDolSearchExtended
+ */
+class BxBaseSearchExtendedServices extends BxDol
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    public function serviceGetForm($aParams)
+    {
+        if(empty($aParams['object']))
+            return '';
+
+        $oSearch = BxDolSearchExtended::getObjectInstance($aParams['object']);
+        if(!$oSearch || !$oSearch->isEnabled())
+            return '';
+
+        return $oSearch->getForm();
+    }
+
+    public function serviceGetResults($aParams)
+    {
+        if(empty($aParams['object']))
+            return '';
+
+        $oSearch = BxDolSearchExtended::getObjectInstance($aParams['object']);
+        if(!$oSearch || !$oSearch->isEnabled())
+            return '';
+
+        $bShowEmpty = isset($aParams['show_empty']) && (bool)$aParams['show_empty'];
+
+        $a = isset($aParams['cond']) && $aParams['cond'] ? $aParams['cond'] : array();
+        $sResults = $oSearch->getResults($a, !empty($aParams['template']) ? $aParams['template'] : '', !empty($aParams['start']) ? $aParams['start'] : 0, !empty($aParams['per_page']) ? $aParams['per_page'] : 0);
+
+        return !empty($sResults) ? $sResults : ($bShowEmpty ? MsgBox(_t('_Empty')) : ''); 
+    }
+}
+
+/** @} */

+ 356 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseSearchResult.php

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

+ 72 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseServiceMetatags.php

@@ -0,0 +1,72 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+define('BX_METATAGS_KEYWORDS_IN_CLOUD', 32); ///< default number of tags in tags cloud
+
+/**
+ * System services for metatags functionality.
+ */
+class BxBaseServiceMetatags extends BxDol
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Get keywords cloud.
+     * @param $sObject metatgs object to get keywords cloud for
+     * @param $mixedSection search section to refer when keyword is clicked, set the same as $sObject to show content withing the module only, it can be one value or array of values, leave empty to show all possible content upon keyword click
+     * @param $iMaxCount number of tags in keywords cloud, by default @see BX_METATAGS_KEYWORDS_IN_CLOUD
+     * @return tags cloud HTML string
+     */
+    public function serviceKeywordsCloud($sObject, $mixedSection, $aParams = array())
+    {
+    	$iMaxCount = isset($aParams['max_count']) ? (int)$aParams['max_count'] : BX_METATAGS_KEYWORDS_IN_CLOUD;
+    	$bShowEmpty = isset($aParams['show_empty']) ? (bool)$aParams['show_empty'] : false;
+
+        $sResult = BxDolMetatags::getObjectInstance($sObject)->getKeywordsCloud($mixedSection, $iMaxCount);
+        if(empty($sResult))
+			return $bShowEmpty ? MsgBox(_t('_Empty')) : '';
+
+		return $sResult;
+    }
+
+    /**
+     * Get location map.
+     * @param $sObject metatgs object to get keywords cloud for
+     * @param $iId content id
+     * @return map HTML string
+     */
+    public function serviceLocationsMap($sObject, $iId, $sMapSize = '1000x144')
+    {
+        return BxDolMetatags::getObjectInstance($sObject)->getLocationsMap($iId, $sMapSize);
+    }
+    
+	/**
+     * Get notification data for Notifications module - action Mention. 
+     */
+	public function serviceGetNotificationsPostMention($aEvent)
+    {
+    	$iProfile = (int)$aEvent['object_owner_id'];
+    	$oProfile = BxDolProfile::getInstance($iProfile);
+        if(!$oProfile)
+			return array();
+
+		return array(
+			'entry_sample' => '_sys_profile_sample_single',
+			'entry_url' => BX_DOL_URL_ROOT . bx_append_url_params('searchKeyword.php', array('type' => 'mention', 'keyword' => $iProfile)),
+			'entry_caption' => $oProfile->getDisplayName(),
+			'entry_author' => $iProfile,
+			'lang_key' => '_sys_metatags_mention_added',
+		);
+    }
+}
+
+/** @} */

+ 178 - 0
upgrade/files/9.0.0.RC4-9.0.0.RC5/files/template/scripts/BxBaseServices.php

@@ -0,0 +1,178 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaBaseView UNA Base Representation Classes
+ * @{
+ */
+
+/**
+ * System services.
+ */
+class BxBaseServices extends BxDol implements iBxDolProfileService
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    public function serviceProfileUnit ($iContentId, $aParams = array())
+    {
+        return $this->_serviceProfileFunc('getUnit', $iContentId);
+    }
+
+    public function serviceHasImage ($iContentId)
+    {
+        return false;
+    }
+
+    public function serviceProfilePicture ($iContentId)
+    {
+        return $this->_serviceProfileFunc('getPicture', $iContentId);
+    }
+
+    public function serviceProfileAvatar ($iContentId)
+    {
+        return $this->_serviceProfileFunc('getAvatar', $iContentId);
+    }
+
+    public function serviceProfileEditUrl ($iContentId)
+    {
+        return BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=account-settings-info');
+    }
+
+    public function serviceProfileThumb ($iContentId)
+    {
+        return $this->_serviceProfileFunc('getThumb', $iContentId);
+    }
+
+    public function serviceProfileIcon ($iContentId)
+    {
+        return $this->_serviceProfileFunc('getIcon', $iContentId);
+    }
+
+    public function serviceProfileName ($iContentId)
+    {
+        return $this->_serviceProfileFunc('getDisplayName', $iContentId);
+    }
+
+    public function serviceProfileUrl ($iContentId)
+    {
+        return $this->_serviceProfileFunc('getUrl', $iContentId);
+    }
+
+    /**
+     * @see iBxDolProfileService::serviceCheckAllowedProfileView
+     */ 
+    public function serviceCheckAllowedProfileView($iContentId)
+    {
+        return _t('_Access denied');
+    }
+
+    /**
+     * @see iBxDolProfileService::serviceCheckAllowedPostInProfile
+     */ 
+    public function serviceCheckAllowedPostInProfile($iContentId)
+    {
+        return _t('_Access denied');
+    }
+
+    public function serviceFormsHelper ()
+    {
+        return new BxTemplAccountForms();
+    }
+
+    public function serviceActAsProfile ()
+    {
+        return false;
+    }
+
+    public function servicePrepareFields ($aFieldsProfile)
+    {
+        return $aFieldsProfile;
+    }
+
+    public function serviceProfilesSearch ($sTerm, $iLimit)
+    {
+        $oDb = BxDolAccountQuery::getInstance();
+        $aRet = array();
+        $a = $oDb->searchByTerm($sTerm, $iLimit);
+        foreach ($a as $r)
+            $aRet[] = array ('label' => $this->serviceProfileName($r['content_id']), 'value' => $r['profile_id']);
+        return $aRet;
+    }
+
+    public function serviceKeywordSearch ($sSection, $aCondition, $sTemplate = '', $iStart = 0, $iPerPage = 0, $bLiveSearch = 0)
+    {
+        if (!$sSection || !isset($aCondition['keyword']))
+            return '';
+
+        $sClass = 'BxTemplSearch';
+
+        $sElsName = 'bx_elasticsearch';
+        $sElsMethod = 'is_configured';
+        if(BxDolRequest::serviceExists($sElsName, $sElsMethod) && BxDolService::call($sElsName, $sElsMethod)) {
+             $oModule = BxDolModule::getInstance($sElsName);
+
+             bx_import('Search', $oModule->_aModule);
+             $sClass = 'BxElsSearch';
+        }
+
+        $oSearch = new $sClass(array($sSection));
+        $oSearch->setLiveSearch($bLiveSearch);
+        $oSearch->setMetaType(isset($aCondition['meta_type']) ? $aCondition['meta_type'] : '');
+        $oSearch->setCategoryObject(isset($aCondition['cat']) ? $aCondition['cat'] : '');
+        $oSearch->setCustomSearchCondition($aCondition);
+        $oSearch->setRawProcessing(true);
+        $oSearch->setCustomCurrentCondition(array(
+            'paginate' => array (
+                'start' => $iStart,
+                'perPage' => $iPerPage ? $iPerPage : BX_DOL_SEARCH_RESULTS_PER_PAGE_DEFAULT,
+            )));
+        if ($sTemplate)
+            $oSearch->setUnitTemplate($sTemplate);
+        
+        return $oSearch->response();
+    }
+    
+    public function _serviceProfileFunc ($sFunc, $iContentId)
+    {
+        if (!$iContentId)
+            return false;
+        if (!($oAccount = BxDolAccount::getInstance($iContentId)))
+            return false;
+        return $oAccount->$sFunc();
+    }
+
+    public function serviceAlertResponseProcessInstalled()
+    {
+        BxDolTranscoderImage::registerHandlersSystem();
+    }
+
+    public function serviceAlertResponseProcessStorageChange ($oAlert)
+    {
+        if ('sys_storage_default' != $oAlert->aExtras['option'])
+            return;
+
+        $aStorages = BxDolStorageQuery::getStorageObjects();
+        foreach ($aStorages as $r) {
+            if (0 == $r['current_size'] && 0 == $r['current_number'] && ($oStorage = BxDolStorage::getObjectInstance($r['object'])))
+                $oStorage->changeStorageEngine($oAlert->aExtras['value']);
+        }
+
+    }
+
+    /**
+     * This service adds notification for users which open your site on mobile devices
+     * and suggest to add your site to their mobile homepage.
+     */ 
+    public function serviceAddToMobileHomepage ()
+    {
+        BxDolTemplate::getInstance()->addJs('cubiq-add-to-homescreen/addtohomescreen.min.js');
+        BxDolTemplate::getInstance()->addCss(BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'cubiq-add-to-homescreen/style/|addtohomescreen.css');
+        return "<script>addToHomescreen();</script>";
+    }
+}
+
+/** @} */

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio