Explorar el Código

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

AntonLV hace 9 años
padre
commit
adc009d222
Se han modificado 100 ficheros con 31300 adiciones y 2 borrados
  1. 1 1
      inc/version.inc.php
  2. 1 1
      install/index.php
  3. 9 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/check.php
  4. 20 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/README.md
  5. 28 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/cart.php
  6. 34 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/chart.php
  7. 245 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDol.php
  8. 433 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolAccount.php
  9. 288 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolAccountQuery.php
  10. 718 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolAcl.php
  11. 128 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolCaptcha.php
  12. 119 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolCategory.php
  13. 122 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolChart.php
  14. 88 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolChartGrowth.php
  15. 77 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolChartGrowthSpeed.php
  16. 42 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolChartQuery.php
  17. 1256 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolCmts.php
  18. 503 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolCmtsQuery.php
  19. 736 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolConnection.php
  20. 286 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolConnectionQuery.php
  21. 303 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolContentInfo.php
  22. 83 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolContentInfoCmts.php
  23. 154 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolCover.php
  24. 88 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolCronAccount.php
  25. 1127 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolDb.php
  26. 156 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolEditor.php
  27. 188 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolEmailTemplates.php
  28. 234 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolFavorite.php
  29. 190 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolFeature.php
  30. 118 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolFileHandler.php
  31. 1668 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolForm.php
  32. 215 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolFormQuery.php
  33. 565 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolGrid.php
  34. 104 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolGridSubscribedMe.php
  35. 40 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolGridSubscriptions.php
  36. 120 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolInformer.php
  37. 126 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolKey.php
  38. 244 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolLiveUpdates.php
  39. 296 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolMenu.php
  40. 592 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolMetatags.php
  41. 102 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolModuleTemplate.php
  42. 259 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolPage.php
  43. 302 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolPayments.php
  44. 265 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolPermalinks.php
  45. 350 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolPrivacy.php
  46. 192 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolPrivacyQuery.php
  47. 502 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolProfile.php
  48. 161 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolProfileUndefined.php
  49. 233 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolReport.php
  50. 90 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolRss.php
  51. 877 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolSearch.php
  52. 108 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolSearchExtended.php
  53. 89 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolSearchExtendedQuery.php
  54. 196 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolSession.php
  55. 60 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolSessionQuery.php
  56. 1405 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolStorage.php
  57. 223 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolStorageS3.php
  58. 2454 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolTemplate.php
  59. 164 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolTranscoderImage.php
  60. 68 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolTranscoderProxy.php
  61. 265 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolTranscoderVideo.php
  62. 437 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolUploader.php
  63. 181 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolView.php
  64. 81 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolViewQuery.php
  65. 311 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolVote.php
  66. 159 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/design.inc.php
  67. 112 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/js/classes/BxDolChartGrowth.js
  68. 661 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/js/classes/BxDolCmts.js
  69. 437 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/js/classes/BxDolGrid.js
  70. 141 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/js/classes/BxDolReport.js
  71. 760 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/js/functions.js
  72. 39 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/js/jquery.dolConverLinks.js
  73. 596 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/js/jquery.dolPopup.js
  74. 264 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/js/jquery.webForms.js
  75. 92 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/params.inc.php
  76. 1559 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/utils.inc.php
  77. 12 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/version.inc.php
  78. 92 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/general/classes/BxBaseModGeneralCmtsSearchResult.php
  79. 92 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/general/classes/BxBaseModGeneralConfig.php
  80. 153 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/general/classes/BxBaseModGeneralDb.php
  81. 388 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/general/classes/BxBaseModGeneralFormsEntryHelper.php
  82. 177 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/general/classes/BxBaseModGeneralGridAdministration.php
  83. 1190 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/general/classes/BxBaseModGeneralModule.php
  84. 115 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/general/classes/BxBaseModGeneralPageEntry.php
  85. 137 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/general/classes/BxBaseModGeneralTemplate.php
  86. 536 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/groups/classes/BxBaseModGroupsModule.php
  87. 240 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/notifications/classes/BxBaseModNotificationsDb.php
  88. 100 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/notifications/classes/BxBaseModNotificationsTemplate.php
  89. 144 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/payment/classes/BxBaseModPaymentCart.php
  90. 148 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/payment/classes/BxBaseModPaymentConfig.php
  91. 198 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/payment/classes/BxBaseModPaymentModule.php
  92. 49 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/payment/classes/BxBaseModPaymentOrders.php
  93. 138 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/payment/classes/BxBaseModPaymentTemplate.php
  94. 3 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/payment/template/center.html
  95. 193 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/profile/classes/BxBaseModProfileFormsEntryHelper.php
  96. 52 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/profile/classes/BxBaseModProfileInstaller.php
  97. 771 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/profile/classes/BxBaseModProfileModule.php
  98. 229 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/profile/classes/BxBaseModProfileTemplate.php
  99. 156 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/profile/template/css/main.css
  100. 47 0
      upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/text/classes/BxBaseModTextDb.php

+ 1 - 1
inc/version.inc.php

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

+ 9 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/check.php

@@ -0,0 +1,9 @@
+<?php
+
+$mixCheckResult = 'Update can not be applied';
+
+$sVer = $this->oDb->getOne("SELECT `version` FROM `sys_modules` WHERE `name` = 'system'");
+if ('9.0.0.B5' == $sVer || '9.0.0-B5' == $sVer)
+    $mixCheckResult = true;
+
+return $mixCheckResult;

+ 20 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/README.md

@@ -0,0 +1,20 @@
+# UNA - [Community Management System](https://una.io)
+[![UNA Official Website](https://img.shields.io/badge/website-una.io-blue.svg)](https://una.io)
+[![license](https://img.shields.io/github/license/unaio/una.svg)](https://github.com/unaio/una/blob/master/license.txt)
+[![UNA All Releases](https://img.shields.io/github/downloads/unaio/una/total.svg)](https://github.com/unaio/una)
+[![UNA Build Status](http://ci.boonex.com:8080/buildStatus/icon?job=Trident)](http://ci.boonex.com:8080/job/Trident/)
+[![Follow UNA Team](https://img.shields.io/twitter/follow/unateam.svg?style=social&label=Follow)](https://twitter.com/unateam)
+
+
+[![UNA - Community Management System](https://user-images.githubusercontent.com/22210428/27618752-754e87be-5c01-11e7-8c85-e9248db730a5.png)](https://una.op)
+
+
+UNA is a **C**MS - **Community** Management System - a full-stack web platform for creating and running a community website. A social website powered by UNA can work as a niche social network, business collaboration hub or any kind of multi-user system with various memberships, roles, content types and permissions defining the rules of the community operation.  UNA can be used as a conventional CMS or blogging platform with focus on content publishing.
+
+
+- [UNA Community Management System - Official Website](https://una.io)
+- [Documentation](https://github.com/unaio/una/wiki)
+- [Discussion Forum](https://una.io/page/discussions-home)
+- [Contact UNA Team](https://una.io/page/contact)
+- [Installation Manual](https://una.io/page/view-article?id=8)
+

+ 28 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/cart.php

@@ -0,0 +1,28 @@
+<?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');
+
+check_logged();
+
+$iSellerId = 0;
+if(bx_get('seller_id') !== false)
+    $iSellerId = (int)bx_get('seller_id');
+
+$sUrl = BxDolPayments::getInstance()->getCartUrl($iSellerId);
+if(empty($sUrl))
+	BxDolTemplate::getInstance()->displayPageNotFound();
+
+header('Location: ' . $sUrl);
+exit;
+
+/** @} */

+ 34 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/chart.php

@@ -0,0 +1,34 @@
+<?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');
+
+check_logged();
+
+$sObject = isset($_REQUEST['object']) ? bx_process_input($_REQUEST['object']) : '';
+$sAction = isset($_REQUEST['action']) && preg_match ('/^[A-Za-z_-]+$/', $_REQUEST['action']) ? bx_process_input($_REQUEST['action']) : '';
+
+$oChart = BxDolChart::getObjectInstance($sObject);
+if(!$oChart || !$sAction) {
+    echoJson(array());
+    exit;
+} 
+
+$sMethod = 'action' . bx_gen_method_name($sAction);
+if(!method_exists($oChart, $sMethod)) {
+    echoJson(array());
+    exit;
+}
+
+$oChart->$sMethod();
+
+/** @} */

+ 245 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDol.php

@@ -0,0 +1,245 @@
+<?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 getPicture();
+    public function getAvatar();
+    public function getThumb();
+    public function getIcon();
+    public function getIconModule();
+    public function getEditUrl();
+    public function isActive();
+
+    /**
+     * 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);
+}
+/** @} */

+ 433 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolAccount.php

@@ -0,0 +1,433 @@
+<?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 $_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()
+    {
+        return $this->_oQuery->getIdById($this->_iAccountID);
+    }
+
+    /**
+     * 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)) {
+            bx_alert('account', $isConfirmed ? 'confirm' : 'unconfirm', $iId);
+            return true;
+        }
+        return false;
+    }
+
+    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;
+
+        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)
+    {
+        return $this->_oQuery->getInfoById((int)$iAccountId ? (int)$iAccountId : $this->_iAccountID);
+    }
+
+    /**
+     * Get account display name
+     */
+    public function getDisplayName($iAccountId = false)
+    {
+        $aInfo = $this->getInfo($iAccountId);
+        return bx_process_output($aInfo['name']);
+    }
+
+    /**
+     * Get account url
+     */
+    public function getUrl($iAccountId = false)
+    {
+        return 'javascript:void(0);';
+    }
+
+    /**
+     * Get account url
+     */
+    public function getUnit($iAccountId = false)
+    {
+        return '<div>' . $this->getDisplayName($iAccountId) . '</div>';
+    }
+
+    /**
+     * 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;
+
+        // create system event
+        bx_alert('account', 'delete', $this->_iAccountID, 0, array ('delete_with_content' => $bDeleteWithContent));
+
+        // unset class instance to prevent creating the instance again
+        $this->_iAccountID = 0;
+        $sClass = get_class($this) . '_' . $this->_iAccountID;
+        unset($GLOBALS['bxDolClasses'][$sClass]);
+
+        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;
+    }
+
+}
+
+/** @} */

+ 288 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolAccountQuery.php

@@ -0,0 +1,288 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * All queries related to account
+ */
+class BxDolAccountQuery extends BxDolDb implements iBxDolSingleton
+{
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolAccountQuery();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    public function getOperators ()
+    {
+        $sSql = $this->prepare("SELECT `id` FROM `sys_accounts` WHERE `role`&" . BX_DOL_ROLE_ADMIN);
+        return $this->getColumn($sSql);
+    }
+    
+    /**
+     * Get account by specified field name and value.
+     * It is for internal usage only.
+     * Use other funtions to get account info, like getInfoById, etc.
+     * @param  string $sField database field name
+     * @param  mixed  $sValue database field value
+     * @return array  with porfile info
+     */
+    protected function _getDataByField ($sField, $sValue)
+    {
+        $sSql = $this->prepare("SELECT * FROM `sys_accounts` WHERE `$sField` = ? LIMIT 1", $sValue);
+        return $this->getRow($sSql);
+    }
+
+    /**
+     * Get account info by id
+     * @param  int   $iID account id
+     * @return array with account info
+     */
+    public function getInfoById( $iID )
+    {
+        return $this->_getDataByField('id', (int)$iID);
+    }
+
+    /**
+     * get account id by emial
+     */
+    public function getIdByEmail($sEmail)
+    {
+        return (int)$this->_getFieldByField('id', 'email', $sEmail);
+    }
+
+    /**
+     * get account id by id
+     */
+    public function getIdById($iId)
+    {
+        return (int)$this->_getFieldByField('id', 'id', $iId);
+    }
+
+    /**
+     * get first studio operator id
+     */
+    public function getStudioOperatorId()
+    {
+        return (int)$this->_getFieldByField('id', 'role', 3);
+    }
+
+    /**
+     * Get account email by id
+     * @param  string  $s search account by this id
+     * @return account email
+     */
+    public function getEmail($iID)
+    {
+        return $this->_getFieldByField('email', 'id', (int)$iID);
+    }
+
+    /**
+     * Get account password by id
+     * @param  string  $s search account by this id
+     * @return account password
+     */
+    public function getPassword($iID)
+    {
+        return $this->_getFieldByField('password', 'id', (int)$iID);
+    }
+
+	/**
+     * Is account online by id
+     * @param  int $iId account id 
+     * @return account online status
+     */
+    public function isOnline($iId)
+    {
+        $sSql = $this->prepare("SELECT 
+        		`ta`.`id` 
+        	FROM `sys_accounts` AS `ta` 
+        	INNER JOIN `sys_sessions` AS `ts` ON `ta`.`id`=`ts`.`user_id` 
+        	WHERE 
+        		`ta`.`id` = ? AND 
+        		`ts`.`date` > (UNIX_TIMESTAMP() - 60 * ?) 
+        	LIMIT 1", $iId, (int)getParam('sys_account_online_time'));
+        return (int)$this->getOne($sSql) > 0;
+    }
+
+    /**
+     * Update last logged in time
+     * @param $sPasswordHash - password hash
+     * @param $sSalt - pasword salt
+     * @param $iAccountId - account id to update password for
+     * @return number of affected rows
+     */
+    public function updatePassword($sPasswordHash, $sSalt, $iAccountId)
+    {
+        $sQuery = $this->prepare("UPDATE `sys_accounts` SET `password` = ?, `salt` = ? WHERE `id`= ?", $sPasswordHash, $sSalt, $iAccountId);
+        return $this->query($sQuery);
+    }
+
+    /**
+     * Update last logged in time
+     * @param  int    $iID account id
+     * @return number of affected rows
+     */
+    public function updateLoggedIn($iID)
+    {
+        return $this->_updateField ($iID, 'logged', time());
+    }
+
+    /**
+     * Update language
+     * @param  int    $iID account id
+     * @return number of affected rows
+     */
+    public function updateLanguage($iID, $iLangId)
+    {
+        return $this->_updateField ($iID, 'lang_id', $iLangId);
+    }
+
+    /**
+     * Update current profile id associated with account
+     * @param  int    $iID        account id
+     * @param  int    $iProfileId set current profile id to this value
+     * @return number of affected rows
+     */
+    public function updateCurrentProfile($iID, $iProfileId)
+    {
+        return $this->_updateField ($iID, 'profile_id', $iProfileId);
+    }
+
+    /**
+     * Update 'email_confirmed' field.
+     * @param  int    $isConfirmed - 0: mark email as unconfirmed, 1: as confirmed
+     * @param  int    $iID         - account id
+     * @return number of affected rows
+     */
+    public function updateEmailConfirmed($isConfirmed, $iID)
+    {
+        return $this->_updateField ($iID, 'email_confirmed', $isConfirmed ? 1 : 0);
+    }
+
+    /**
+     * Get account field by specified field name and value.
+     * In most cases it is for internal usage only.
+     * Use other funtions to get account info, like getIdByEmail, etc.
+     * @param  string    $sFieldRequested database field name to return
+     * @param  string    $sFieldSearch    database field name to search for
+     * @param  mixed     $sValue          database field value
+     * @return specified account field value
+     */
+    protected function _getFieldByField ($sFieldRequested, $sFieldSearch, $sValue)
+    {
+        $sSql = $this->prepare("SELECT `$sFieldRequested` FROM `sys_accounts` WHERE `$sFieldSearch` = ? LIMIT 1", $sValue);
+        return $this->getOne($sSql);
+    }
+
+    /**
+     * Update some field by account id
+     * In most cases it is for internal usage only.
+     * Use other funtions to get account info, like updateLogged, etc.
+     * @param  string    $sFieldRequested database field name to return
+     * @param  string    $sFieldSearch    database field name to search for
+     * @param  mixed     $sValue          database field value
+     * @return specified account field value
+     */
+    protected function _updateField ($iId, $sFieldForUpdate, $sValue)
+    {
+        $sSql = $this->prepare("UPDATE `sys_accounts` SET `$sFieldForUpdate` = ? WHERE `id` = ? LIMIT 1", $sValue, $iId);
+        return $this->query($sSql);
+    }
+
+    /**
+     * Delete account info by id
+     * @param  int      $iID profile id
+     * @return affected rows
+     */
+    public function delete($iID)
+    {
+        $sSql = $this->prepare("DELETE FROM `sys_accounts` WHERE `id` = ? LIMIT 1", $iID);
+        return $this->query($sSql);
+    }
+
+	/**
+     * Get account(s) by params
+     * @param  array   $aParams browse params
+     * @return array with account(s)
+     */
+    public function getAccounts($aParams)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+
+    	$sFieldsClause = "`ta`.*"; 
+    	$sJoinClause = $sWhereClause = $sGroupClause = $sOrderClause = "";
+
+    	switch($aParams['type']) {
+    		case 'by_join_date':
+    			$aMethod['params'][1] = array(
+    				'date' => $aParams['date']
+    			);
+
+    			$sWhereClause = " AND `ta`.`added` > :date AND `ta`.`added` < UNIX_TIMESTAMP()";
+    			break;
+    	}
+
+    	$sGroupClause = $sGroupClause ? "GROUP BY " . $sGroupClause : "";
+		$sOrderClause = $sOrderClause ? "ORDER BY " . $sOrderClause : "";
+
+		$aMethod['params'][0] = "SELECT
+				" . $sFieldsClause . "
+            FROM `sys_accounts` AS `ta`" . $sJoinClause . "
+            WHERE 1" . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause;
+
+        return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+
+    /**
+     * Search account profile by keyword
+     */
+    public function searchByTerm($sTerm, $iLimit)
+    {
+    	$aBindings = array(
+    		'system' => 'system',
+    		'status' => BX_PROFILE_STATUS_ACTIVE,
+    		'limit' => (int)$iLimit
+    	);
+
+        $sWhere = '';
+        $aFieldsQuickSearch = array('name', 'email');
+        foreach ($aFieldsQuickSearch as $sField) {
+        	$aBindings[$sField] = '%' . $sTerm . '%';
+
+            $sWhere .= " OR `c`.`$sField` LIKE :$sField ";
+        }
+
+        $sQuery = "SELECT `c`.`id` AS `content_id`, `p`.`account_id`, `p`.`id` AS `profile_id`, `p`.`status` AS `profile_status` FROM `sys_accounts` AS `c` INNER JOIN `sys_profiles` AS `p` ON (`p`.`content_id` = `c`.`id` AND `p`.`type` = :system) WHERE `p`.`status` = :status AND (0 $sWhere) ORDER BY `added` DESC LIMIT :limit";
+        return $this->getAll($sQuery, $aBindings);
+    }
+}
+
+/** @} */

+ 718 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolAcl.php

@@ -0,0 +1,718 @@
+<?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
+ * @{
+ */
+
+/**
+ * Message constants passed to _t_ext() function by checkAction()
+ *
+ * NOTE: checkAction() returns language dependent messages
+ */
+define('CHECK_ACTION_MESSAGE_NOT_ALLOWED',			"_sys_acl_action_not_allowed");
+define('CHECK_ACTION_MESSAGE_LIMIT_REACHED',		"_sys_acl_action_limit_reached");
+define('CHECK_ACTION_MESSAGE_MESSAGE_EVERY_PERIOD',	"_sys_acl_action_every_period");
+define('CHECK_ACTION_MESSAGE_NOT_ALLOWED_BEFORE',	"_sys_acl_action_not_allowed_before");
+define('CHECK_ACTION_MESSAGE_NOT_ALLOWED_AFTER',	"_sys_acl_action_not_allowed_after");
+
+define('CHECK_ACTION_MESSAGE_UNAUTHENTICATED',	    "_sys_acl_action_unauthenticated");
+define('CHECK_ACTION_MESSAGE_UNCONFIRMED',			"_sys_acl_action_unconfirmed");
+define('CHECK_ACTION_MESSAGE_PENDING',			    "_sys_acl_action_pending");
+define('CHECK_ACTION_MESSAGE_SUSPENDED',			"_sys_acl_action_suspended");
+
+/**
+ * Nodes of $args array that are passed to _t_ext() function by checkAction()
+ */
+define('CHECK_ACTION_LANG_FILE_ACTION', 1);
+define('CHECK_ACTION_LANG_FILE_MEMBERSHIP', 2);
+define('CHECK_ACTION_LANG_FILE_LIMIT', 3);
+define('CHECK_ACTION_LANG_FILE_PERIOD', 4);
+define('CHECK_ACTION_LANG_FILE_AFTER', 5);
+define('CHECK_ACTION_LANG_FILE_BEFORE', 6);
+define('CHECK_ACTION_LANG_FILE_SITE_EMAIL',	7);
+
+/**
+ * Standard membership ID's
+ */
+define('MEMBERSHIP_ID_NON_MEMBER', 1);
+define('MEMBERSHIP_ID_ACCOUNT', 2);
+define('MEMBERSHIP_ID_STANDARD', 3);
+define('MEMBERSHIP_ID_UNCONFIRMED', 4);
+define('MEMBERSHIP_ID_PENDING', 5);
+define('MEMBERSHIP_ID_SUSPENDED', 6);
+define('MEMBERSHIP_ID_MODERATOR', 7);
+define('MEMBERSHIP_ID_ADMINISTRATOR', 8);
+
+/**
+ * Indices for checkAction() result array
+ */
+define('CHECK_ACTION_RESULT', 0);
+define('CHECK_ACTION_MESSAGE', 1);
+define('CHECK_ACTION_PARAMETER', 3);
+
+/**
+ * CHECK_ACTION_RESULT node values
+ */
+define('CHECK_ACTION_RESULT_ALLOWED', 0);
+define('CHECK_ACTION_RESULT_NOT_ALLOWED', 1);
+define('CHECK_ACTION_RESULT_NOT_ACTIVE', 2);
+define('CHECK_ACTION_RESULT_LIMIT_REACHED', 3);
+define('CHECK_ACTION_RESULT_NOT_ALLOWED_BEFORE', 4);
+define('CHECK_ACTION_RESULT_NOT_ALLOWED_AFTER', 5);
+
+/**
+ * Standard period units
+ */
+define('MEMBERSHIP_PERIOD_UNIT_DAY', 'day');
+define('MEMBERSHIP_PERIOD_UNIT_WEEK', 'week');
+define('MEMBERSHIP_PERIOD_UNIT_MONTH', 'month');
+define('MEMBERSHIP_PERIOD_UNIT_YEAR', 'year');
+
+
+class BxDolAcl extends BxDolFactory implements iBxDolSingleton
+{
+    protected $oDb;
+
+    protected $_aStandardMemberships = array(
+        MEMBERSHIP_ID_NON_MEMBER => 1,
+        MEMBERSHIP_ID_ACCOUNT => 1,
+        MEMBERSHIP_ID_UNCONFIRMED => 1,
+        MEMBERSHIP_ID_PENDING => 1,
+        MEMBERSHIP_ID_SUSPENDED => 1,
+        MEMBERSHIP_ID_STANDARD => 1,
+    );
+
+    protected $_aProfileStatus2LevelMap = array (
+        BX_PROFILE_STATUS_SUSPENDED => MEMBERSHIP_ID_SUSPENDED,
+        BX_PROFILE_STATUS_PENDING => MEMBERSHIP_ID_PENDING,
+    );
+
+    protected $_aLevel2MessageMap = array (
+        MEMBERSHIP_ID_NON_MEMBER => '_sys_acl_action_unauthenticated',
+        MEMBERSHIP_ID_ACCOUNT => '_sys_acl_action_account',
+        MEMBERSHIP_ID_UNCONFIRMED => '_sys_acl_action_unconfirmed',
+        MEMBERSHIP_ID_PENDING => '_sys_acl_action_pending',
+        MEMBERSHIP_ID_SUSPENDED => '_sys_acl_action_suspended',
+    );
+
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+
+        $this->oDb = BxDolAclQuery::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
+     */
+    public static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxTemplAcl();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+	/**
+     * Get necessary condition array to use membership level in search classes
+     * @param $sContentField content table field name
+     * @param $mixedLevelId level ID or array of level IDs
+     * @return array of conditions is returned
+     */
+    public function getContentByLevelAsCondition($sContentField, $mixedLevelId)
+    {
+        return array(
+        	'restriction_sql' => ' AND (`tlm`.DateStarts IS NULL OR `tlm`.DateStarts <= NOW()) AND (`tlm`.DateExpires IS NULL OR `tlm`.DateExpires > NOW())',
+            'restriction' => array (
+                'acl_members' => array(
+                    'value' => $mixedLevelId,
+                    'field' => 'IDLevel',
+                    'operator' => is_array($mixedLevelId) ? 'in' : '=',
+                    'table' => 'tlm',
+                ),
+            ),
+            'join' => array (
+                'acl_members' => array(
+                    'type' => 'INNER',
+                    'table' => 'sys_acl_levels_members',
+                    'table_alias' => 'tlm',
+                    'mainField' => $sContentField,
+                    'onField' => 'IDMember',
+                    'joinFields' => array(),
+                ),
+                'acl_levels' => array(
+                    'type' => 'INNER',
+                    'table' => 'sys_acl_levels',
+                    'table_alias' => 'tl',
+                    'mainTable' => 'tlm',
+                    'mainField' => 'IDLevel',
+                    'onField' => 'ID',
+                    'joinFields' => array(),
+                ),
+            ),
+        );
+    }
+
+	/**
+     * Get necessary parts of SQL query to use membership levels in other queries
+     * @param $sContentTable content table name
+     * @param $sContentField content table field name
+     * @param $mixedLevelId level ID or array of level IDs
+     * @return array of SQL string parts, for now 'where' part only is returned
+     */
+    public function getContentByLevelAsSQLPart($sContentTable, $sContentField, $mixedLevelId)
+    {
+        return $this->oDb->getContentByLevelAsSQLPart($sContentTable, $sContentField, $mixedLevelId);
+    }
+
+    /**
+     * Check if member has one of the provided membership levels
+     * @param $mixedPermissions - integer value (every bit is matched with some membership ID) or an array of membership IDs to check permissions for
+     * @param $iProfileId - profile to check, if it isn't provided or is false then currently logged in profile is used.
+     * @return true if member has privided membership levels, or false if member hasn't.
+     */
+    public function isMemberLevelInSet($mixedPermissions, $iProfileId = false)
+    {
+        $iPermissions = 0;
+        if(is_array($mixedPermissions))
+            foreach($mixedPermissions as $iPermissionId)
+                $iPermissions += pow(2, $iPermissionId - 1);
+        else if(is_numeric($mixedPermissions))
+            $iPermissions = (int)$mixedPermissions;
+
+        if(!$iPermissions)
+            return false;
+
+        if(false === $iProfileId) {
+            $oProfile = BxDolProfile::getInstance();
+            $iProfileId = $oProfile ? $oProfile->id() : 0;
+        }
+
+        $aACL = $this->getMemberMembershipInfo($iProfileId);
+        return ($iPermissions & pow(2, $aACL['id'] - 1));
+    }
+
+    /**
+     * Checks if a given action is allowed for a given profile and updates action information if the
+     * action is performed.
+     *
+     * @param  int     $iProfileId     ID of a profile that is going to perform an action
+     * @param  int     $iActionId      ID of the action itself
+     * @param  boolean $bPerformAction if true, then action information is updated, i.e. action is 'performed'
+     * @return array(
+     *                                CHECK_ACTION_RESULT => CHECK_ACTION_RESULT_ constant,
+     *                                CHECK_ACTION_MESSAGE => CHECK_ACTION_MESSAGE_ constant,
+     *                                CHECK_ACTION_PARAMETER => additional action parameter (string)
+     *                                )
+     *
+     * NOTES:
+     *
+     * $aResult[CHECK_ACTION_MESSAGE] contains a message with detailed information about the result,
+     * already processed by the language file
+     *
+     * if $aResult[CHECK_ACTION_RESULT] === CHECK_ACTION_RESULT_ALLOWED then this node contains
+     * an empty string
+     *
+     * The error messages themselves are stored in the language file. Additional variables are
+     * passed to the languages.inc.php function _t_ext() as an array and can be used there in the form of
+     * {0}, {1}, {2} ...
+     *
+     * Additional variables passed to the lang. file on errors (can be used in error messages):
+     *
+     * 	For all errors:
+     *
+     * 		$arg0[CHECK_ACTION_LANG_FILE_ACTION]	= name of the action
+     * 		$arg0[CHECK_ACTION_LANG_FILE_MEMBERSHIP]= name of the current membership
+     *
+     * 	CHECK_ACTION_RESULT_LIMIT_REACHED:
+     *
+     * 		$arg0[CHECK_ACTION_LANG_FILE_LIMIT]		= limit on number of actions allowed for the profile
+     * 		$arg0[CHECK_ACTION_LANG_FILE_PERIOD]	= period that the limit is set for (in hours, 0 if unlimited)
+     *
+     * 	CHECK_ACTION_RESULT_NOT_ALLOWED_BEFORE:
+     *
+     * 		$arg0[CHECK_ACTION_LANG_FILE_BEFORE]	= date/time since when the action is allowed
+     *
+     * 	CHECK_ACTION_RESULT_NOT_ALLOWED_AFTER:
+     *
+     * 		$arg0[CHECK_ACTION_LANG_FILE_AFTER]		= date/time since when the action is not allowed
+     *
+     * $aResult[CHECK_ACTION_PARAMETER] contains an additional parameter that can be considered
+     * when performing the action (like the number of profiles to show in search result)
+    */
+    function checkAction($iProfileId, $iActionId, $bPerformAction = false)
+    {
+        $aResult = array();
+        $aLangFileParams = array();
+
+        $iProfileId = (int)$iProfileId;
+        $iActionId = (int)$iActionId;
+        $bPerformAction = $bPerformAction ? true : false;
+
+        $aMembership = $this->getMemberMembershipInfo($iProfileId); // get current profile's membership information
+
+        $aLangFileParams[CHECK_ACTION_LANG_FILE_MEMBERSHIP] = _t($aMembership['name']);
+        $aLangFileParams[CHECK_ACTION_LANG_FILE_SITE_EMAIL] = getParam('site_email');
+
+        $aAction = $this->oDb->getAction($aMembership['id'], $iActionId);
+        if (!$aAction)
+            bx_trigger_error('Unknown action ID: ' . $iActionId, 2);
+
+        $aResult[CHECK_ACTION_PARAMETER] = $aAction['additional_param_value'];
+        $aLangFileParams[CHECK_ACTION_LANG_FILE_ACTION] = !empty($aAction['title']) ? _t($aAction['title']) : $aAction['name'];
+
+        /**
+         * Action is not allowed for the current membership
+         */
+        if (is_null($aAction['id'])) {
+
+            $sLangKey = CHECK_ACTION_MESSAGE_NOT_ALLOWED;
+            if (isset($this->_aLevel2MessageMap[$aMembership['id']]))
+                $sLangKey = $this->_aLevel2MessageMap[$aMembership['id']];
+
+            $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_NOT_ALLOWED;
+            $aResult[CHECK_ACTION_MESSAGE] = _t_ext($sLangKey, $aLangFileParams);
+            return $aResult;
+        }
+
+        /**
+         * Check fixed period limitations if present (also for non-members)
+         */
+        if($aAction['allowed_period_start'] && time() < $aAction['allowed_period_start']) {
+            $aLangFileParams[CHECK_ACTION_LANG_FILE_BEFORE] = bx_time_js($aAction['allowed_period_start'], BX_FORMAT_DATE_TIME);
+
+            $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_NOT_ALLOWED_BEFORE;
+            $aResult[CHECK_ACTION_MESSAGE] = _t_ext(CHECK_ACTION_MESSAGE_NOT_ALLOWED_BEFORE, $aLangFileParams);
+
+            return $aResult;
+        }
+
+        if($aAction['allowed_period_end'] && time() > $aAction['allowed_period_end']) {
+            $aLangFileParams[CHECK_ACTION_LANG_FILE_AFTER] = bx_time_js($aAction['allowed_period_end'], BX_FORMAT_DATE_TIME);
+
+            $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_NOT_ALLOWED_AFTER;
+            $aResult[CHECK_ACTION_MESSAGE] = _t_ext(CHECK_ACTION_MESSAGE_NOT_ALLOWED_AFTER, $aLangFileParams);
+
+            return $aResult;
+        }
+
+        /**
+         * if non-member, allow action without performing further checks
+         */
+        if ($aMembership['id'] == MEMBERSHIP_ID_NON_MEMBER) {
+            $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_ALLOWED;
+            return $aResult;
+        }
+
+        /**
+         * Check other limitations (for members only)
+         */
+        $iAllowedCnt = (int)$aAction['allowed_count']; ///< Number of allowed actions. Unlimited if not specified or 0
+        $iPeriodLen = (int)$aAction['allowed_period_len']; ///< Period for AllowedCount in hours. If not specified, AllowedCount is treated as total number of actions permitted.
+
+        if($iAllowedCnt > 0) {
+            $aActionTrack = $this->oDb->getActionTrack($iActionId, $iProfileId);
+
+            $iActionsLeft = $bPerformAction ? $iAllowedCnt - 1 : $iAllowedCnt;
+            $iValidSince = time();
+
+            /**
+             * Member is requesting/performing this action for the first time,
+             * and there is no corresponding record in sys_acl_actions_track table.
+             */
+            if(!$aActionTrack) {
+                $this->oDb->insertActionTarck($iActionId, $iProfileId, $iActionsLeft, $iValidSince);
+
+                $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_ALLOWED;
+                return $aResult;
+            }
+
+            /**
+             * Action has been requested/performed at least once at this point and there is a corresponding record in sys_acl_actions_track table
+             *
+             * Action record in sys_acl_actions_track table is out of date.
+             */
+            $iPeriodEnd = (int)$aActionTrack['valid_since'] + $iPeriodLen * 3600; //ValidSince is in seconds, PeriodLen is in hours
+            if($iPeriodLen > 0 && $iPeriodEnd < time()) {
+                $this->oDb->updateActionTrack($iActionId, $iProfileId, $iActionsLeft, $iValidSince);
+
+                $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_ALLOWED;
+                return $aResult;
+            }
+
+            $iActionsLeft = (int)$aActionTrack['actions_left']; ///< Action record is up to date
+
+            /**
+             * Action limit reached for now
+             */
+            if($iActionsLeft <= 0){
+                $aLangFileParams[CHECK_ACTION_LANG_FILE_LIMIT] = $iAllowedCnt;
+                $aLangFileParams[CHECK_ACTION_LANG_FILE_PERIOD] = $iPeriodLen;
+
+                $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_LIMIT_REACHED;
+                $aResult[CHECK_ACTION_MESSAGE] = '<div style="width: 80%">' . _t_ext(CHECK_ACTION_MESSAGE_LIMIT_REACHED, $aLangFileParams) . ($iPeriodLen > 0 ? _t_ext(CHECK_ACTION_MESSAGE_MESSAGE_EVERY_PERIOD, $aLangFileParams) : '') . '.</div>';
+
+                return $aResult;
+            }
+
+            if($bPerformAction) {
+                $iActionsLeft--;
+                $this->oDb->updateActionTrack($iActionId, $iProfileId, $iActionsLeft);
+            }
+        }
+
+        $aResult[CHECK_ACTION_RESULT] = CHECK_ACTION_RESULT_ALLOWED;
+        return $aResult;
+    }
+
+    /**
+     * Get the list of existing memberships
+     *
+     * @param  bool   $bPurchasableOnly if true, fetches only purchasable memberships; 'purchasable' here means that:
+     *                                  - MemLevels.Purchasable = 'yes'
+     *                                  - MemLevels.Active = 'yes'
+     *                                  - there is at least one pricing option for the membership
+     * @return array( membershipID_1 => membershipName_1,  membershipID_2 => membershipName_2, ...) if no such memberships, then just array()
+     */
+    function getMemberships($bPurchasableOnly = false, $bActiveOnly = false, $isTranslate = true)
+    {
+        $sType = 'all_pair';
+        if($bPurchasableOnly)
+            $sType = 'all_active_purchasble_pair';
+        else if($bActiveOnly)
+            $sType = 'all_active_pair';
+
+        $aLevels = array();
+        $this->oDb->getLevels(array('type' => $sType), $aLevels, false);
+        if ($isTranslate)
+            foreach ($aLevels as $k => $s)
+                $aLevels[$k] = _t($s);
+        return $aLevels;
+    }
+
+    /**
+     * Get info about a given membership
+     *
+     * @param  int    $iLevelId membership to get info about
+     * @return array(
+     *                         'id'					=>	ID,
+     *                         'name'					=>	name,
+     *                         'icon'					=>	icon,
+     *                         'description'			=>	description,
+     *                         'active'				=>	active,
+     *                         'purchasable'			=>	purchasable,
+     *                         'removable'				=>	removable
+     *                         'quota_size'			=>	quota size,
+     *                         'quota_number'			=>	quota number,
+     *                         'quota_max_file_size'	=>	quota max file size
+     *                         )
+     */
+    function getMembershipInfo($iLevelId)
+    {
+        $aLevel = array();
+        $this->oDb->getLevels(array('type' => 'by_id', 'value' => $iLevelId), $aLevel, false);
+        return $aLevel;
+    }
+
+    /**
+     * Retrieves information about membership for a given profile at a given moment.
+     *
+     * If there are no memberships purchased/assigned to the
+     * given profile or all of them have expired at the given point,
+     * the profile is assumed to be a standard profile, and the function
+     * returns	information about the Standard membership. This will
+     * also happen if a profile wasnt actually registered in the database
+     * at that point - the function will still return info about Standard
+     * membership, not the Non-member one.
+     *
+     * If there is no profile with the given $iProfileId,
+     * the function returns information about the Non-member or Authenticated
+     * predefined membership.
+     *
+     * The Standard, Authenticated and Non-member memberships have their
+     * DateStarts and DateExpires attributes set to NULL.
+     *
+     * @param  int    $iProfileId ID of a profile to get info about
+     * @param  int    $time       specifies the time to use when determining membership; if not specified, the function takes the current time
+     * @return array(
+     *                           'id'			=> membership id,
+     *                           'name'			=> membership name,
+     *                           'date_starts'	=> (UNIX timestamp) date/time purchased,
+     *                           'date_expires'	=> (UNIX timestamp) date/time expires
+     *                           )
+     */
+    function getMemberMembershipInfo($iProfileId, $iTime = 0)
+    {
+        $aMembershipCurrent = $this->getMemberMembershipInfoCurrent($iProfileId, $iTime);
+        if (isset($this->_aStandardMemberships[$aMembershipCurrent['id']]))
+            return $aMembershipCurrent;
+
+        $aMembership = $aMembershipCurrent;
+        do {
+            $iDateStarts = $aMembership['date_starts'];
+            $aMembership = $this->getMemberMembershipInfoCurrent($iProfileId, ((int)$iDateStarts < 1 ? 0 : $iDateStarts - 1));
+        }
+        while($aMembership['id'] == $aMembershipCurrent['id'] && (int)$aMembership['date_starts']);
+
+        $aMembership = $aMembershipCurrent;
+        do {
+            $iDateExpires = $aMembership['date_expires'];
+            $aMembership = $this->getMemberMembershipInfoCurrent($iProfileId, $iDateExpires);
+        } while($aMembership['id'] == $aMembershipCurrent['id'] && (int)$aMembership['date_expires']);
+
+        $aMembershipCurrent['date_starts'] = $iDateStarts;
+        $aMembershipCurrent['date_expires'] = $iDateExpires;
+
+        return $aMembershipCurrent;
+    }
+
+    /**
+     * Set a membership for a profile
+     *
+     * @param  int     $iProfileId profile that is going to get the membership
+     * @param  int     $iLevelId   membership that is going to be assigned to the profile
+     *                             if $iLevelId == MEMBERSHIP_ID_STANDARD then $days and $bStartsNow parameters are not used,
+     *                             so Standard membership is always set immediately and `forever`
+     * @param  mixed   $mixedPeriod  number of Days to set membership for or an array with 'period'-'period unit' pair. If number or 'period' in pair equal 0, then the membership is set forever
+     * @param  boolean $bStartsNow if true, the membership will start immediately if false, the membership will start after the current membership expires
+     * @return boolean true in case of success, false in case of failure
+     */
+    function setMembership($iProfileId, $iLevelId, $mixedPeriod = 0, $bStartsNow = false, $sTransactionId = '')
+    {
+        $iProfileId = (int)$iProfileId;
+        $iLevelId = (int)$iLevelId;
+        $bStartsNow = $bStartsNow ? true : false;
+
+        if (!$iProfileId)
+            $iProfileId = -1;
+
+        if (empty($sTransactionId))
+            $sTransactionId = 'NULL';
+
+        // check if profile exists
+        if(($sProfileEmail = BxDolProfileQuery::getInstance()->getEmailById($iProfileId)) === false)
+            return false;
+
+        // check if membership exists
+        $aLevel = array();
+        $this->oDb->getLevels(array('type' => 'by_id', 'value' => $iLevelId), $aLevel, false);
+        if(empty($aLevel) || !is_array($aLevel))
+            return false;
+
+        if($iLevelId == MEMBERSHIP_ID_NON_MEMBER)
+            return false;
+
+        $aMembershipCurrent = $this->getMemberMembershipInfo($iProfileId);
+        $aMembershipLatest = $this->getMemberMembershipInfoLatest($iProfileId);
+
+        // setting Standard membership level
+        if ($iLevelId == MEMBERSHIP_ID_STANDARD) {
+            if ($aMembershipCurrent['id'] == MEMBERSHIP_ID_STANDARD)
+                return true;
+
+            // delete present and future memberships
+            return $this->oDb->deleteLevelByProfileId($iProfileId);
+        }
+
+        if ((is_numeric($mixedPeriod) && (int)$mixedPeriod < 0) || (is_array($mixedPeriod) && (!isset($mixedPeriod['period']) || $mixedPeriod['period'] < 0)))
+            return false;
+
+        $iDateStarts = time();
+        if (!$bStartsNow) {
+            // Make the membership starts after the latest membership expires or return false if latest membership isn't Standard and is lifetime membership.
+            if(!empty($aMembershipLatest['date_expires']))
+                $iDateStarts = $aMembershipLatest['date_expires'];
+            else if(empty($aMembershipLatest['date_expires']) && $aMembershipLatest['id'] != MEMBERSHIP_ID_STANDARD)
+                return false;
+        }
+        else {
+            // Delete any profile's membership level and actions traces
+            $this->oDb->deleteLevelByProfileId($iProfileId, true); 
+            $this->oDb->clearActionsTracksForMember($iProfileId);
+        }
+
+        // set lifetime membership if 0 days is used.
+        if(is_numeric($mixedPeriod))
+        	$mixedPeriod = array('period' => (int)$mixedPeriod, 'period_unit' => MEMBERSHIP_PERIOD_UNIT_DAY);
+
+        if(!$this->oDb->insertLevelByProfileId($iProfileId, $iLevelId, $iDateStarts, $mixedPeriod, $sTransactionId))
+           return false;
+
+        // raise membership alert
+        $oZ = new BxDolAlerts('profile', 'set_membership', '', $iProfileId, array('mlevel'=> $iLevelId, 'period' => $mixedPeriod['period'], 'period_unit' => $mixedPeriod['period_unit'], 'starts_now' => $bStartsNow, 'txn_id' => $sTransactionId));
+        $oZ->alert();
+
+        // Send notification
+        $aTemplate = BxDolEmailTemplates::getInstance()->parseTemplate('t_MemChanged', array('membership_level' => _t($aLevel['name'])), 0, $iProfileId);
+        if ($aTemplate)
+            sendMail($sProfileEmail, $aTemplate['Subject'], $aTemplate['Body']);
+
+        return true;
+    }
+
+    function unsetMembership($iProfileId, $iLevelId, $sTransactionId)
+    {
+    	return $this->oDb->deleteLevelBy(array(
+    		'IDMember' => $iProfileId,
+    		'IDLevel' => $iLevelId,
+    		'TransactionID' => $sTransactionId
+    	));
+    }
+
+    /**
+     * get action id by module and name
+     * @param $sAction action name
+     * @param $sModule module name
+     * @param $aActions array of actions from sys_acl_actions table, with default array keys (starting from 0) and text values
+     */
+    function getMembershipActionId($sAction, $sModule)
+    {
+        $this->oDb->getActions(array('type' => 'by_names_and_module', 'value' => $sAction, 'module' => $sModule), $aActions, false);
+        if (count($aActions) > 1)
+            trigger_error('Duplicate action - name:' . $sAction . ', module:' . $sModule, E_USER_ERROR);
+        $aAction = array_pop($aActions);
+        return $aAction['id'];
+    }
+
+    function getExpirationLetter($iProfileId, $sLevelName, $iLevelExpireDays )
+    {
+        $iProfileId = (int)$iProfileId;
+
+        if(!$iProfileId)
+            return false;
+
+        $oProfileQuery = BxDolProfileQuery::getInstance();
+        $sProfileEmail = $oProfileQuery->getEmailById($iProfileId);
+
+        $aPlus = array(
+            'membership_name' => $sLevelName,
+            'expire_days' => $iLevelExpireDays
+        );
+
+        $aTemplate = BxDolEmailTemplates::getInstance()->parseTemplate('t_MemExpiration', $aPlus, 0, $iProfileId);
+
+        $iResult = $aTemplate && sendMail($sProfileEmail, $aTemplate['Subject'], $aTemplate['Body'], $iProfileId, $aPlus);
+        return !empty($iResult);
+    }
+
+    /**
+     * clear expired membership levels
+     */
+    public function maintenance ()
+    {
+        $iDaysToCleanMemLevels = (int) getParam("db_clean_mem_levels");
+        return $this->oDb->maintenance ($iDaysToCleanMemLevels);
+    }
+
+    protected function getMemberMembershipInfoCurrent($iProfileId, $iTime = 0)
+    {
+        $aMemLevel = false;
+
+        // get profile status
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        $aProfileInfo = $oProfile ? $oProfile->getInfo() : false;
+        $sProfileStatus = $aProfileInfo ? $aProfileInfo['status'] : false;
+        $sProfileType = $aProfileInfo ? $aProfileInfo['type'] : false;
+
+        // account profile
+	    if($sProfileType == 'system') {
+        	$aMemLevel = $this->oDb->getLevelByIdCached(MEMBERSHIP_ID_ACCOUNT);
+        	if (!$aMemLevel)
+                trigger_error ('Standard member level is missing: MEMBERSHIP_ID_ACCOUNT', E_USER_ERROR);
+
+        	return $aMemLevel;
+        }
+
+        // check if account is unconfirmed, every account's profile is unconfirmed if account is unconfirmed
+        $oAccount = $aProfileInfo ? BxDolAccount::getInstance($aProfileInfo['account_id']) : false;
+        if ($oAccount && !$oAccount->isConfirmed()) {
+            $aMemLevel = $this->oDb->getLevelByIdCached(MEMBERSHIP_ID_UNCONFIRMED);
+            if (!$aMemLevel)
+                trigger_error ('Standard member level is missing: MEMBERSHIP_ID_UNCONFIRMED', E_USER_ERROR);
+
+            return $aMemLevel;
+        }
+        
+        // profile is not active, so return standard memberships according to profile status        
+        if (false === $sProfileStatus || BX_PROFILE_STATUS_ACTIVE != $sProfileStatus) {                
+            if (!isset($this->_aProfileStatus2LevelMap[$sProfileStatus]))
+                $iLevelId = MEMBERSHIP_ID_NON_MEMBER; // if there is no profile status - then it isn't member
+            else
+                $iLevelId = $this->_aProfileStatus2LevelMap[$sProfileStatus]; // get member level id which associated with every non-active status
+
+            $aMemLevel = $this->oDb->getLevelByIdCached($iLevelId);
+
+            if (!$aMemLevel)
+                trigger_error ('Standard member level is missing: ' . $iLevelId, E_USER_ERROR);
+
+            return $aMemLevel;
+        }
+
+        // profile is active get memebr level from profile
+        $aMemLevel = $this->oDb->getLevelCurrent((int)$iProfileId, $iTime);
+
+        // There are no purchased/assigned memberships for the profile or all of them have expired.
+        // In this case the profile is assumed to have Standard membership.
+        if (!$aMemLevel || is_null($aMemLevel['id'])) {
+            $aMemLevel = $this->oDb->getLevelByIdCached(MEMBERSHIP_ID_STANDARD);
+            if (!$aMemLevel)
+                trigger_error ('Standard member level is missing: ' . MEMBERSHIP_ID_NON_MEMBER, E_USER_ERROR);
+        }
+
+        return $aMemLevel;
+    }
+
+    protected function getMemberMembershipInfoLatest($iProfileId, $iTime = 0)
+    {
+        $aMembershipCurrent = $this->getMemberMembershipInfoCurrent($iProfileId, $iTime);
+        if (isset($this->_aStandardMemberships[$aMembershipCurrent['id']]))
+            return $aMembershipCurrent;
+
+        $aMembership = $aMembershipCurrent;
+        while ($aMembership['id'] != MEMBERSHIP_ID_STANDARD) {
+            $aMembershipLast = $aMembership;
+            if(!isset($aMembership['date_expires']) || (int)$aMembership['date_expires'] == 0)
+                break;
+
+            $aMembership = $this->getMemberMembershipInfoCurrent($iProfileId, $aMembership['date_expires']);
+        }
+
+        return $aMembershipLast;
+    }
+
+    public function onProfileDelete ($iProfileId)
+    {
+        return $this->oDb->deleteLevelByProfileId($iProfileId, true);
+    }
+}
+
+function checkAction($iProfileId, $iActionId, $bPerformAction = false)
+{
+    return BxDolAcl::getInstance()->checkAction($iProfileId, $iActionId, $bPerformAction);
+}
+
+function checkActionModule($iProfileId, $sActionName, $sModuleName, $bPerformAction = false)
+{
+    $oACL = BxDolAcl::getInstance();
+
+    $iActionId = $oACL->getMembershipActionId($sActionName, $sModuleName);
+
+    return $oACL->checkAction($iProfileId, $iActionId, $bPerformAction);
+}
+
+/** @} */

+ 128 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolCaptcha.php

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

+ 119 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolCategory.php

@@ -0,0 +1,119 @@
+<?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
+ * @{
+ */
+
+/**
+ * Category object, it can make regular form lists to act as categories by making it clickable and display search results with all contents with the same category.
+ * Also it provides services to list all categories with number if items in each category.
+ *
+ * @section editor_create Creating the Category object:
+ *
+ *
+ * Add record to 'sys_objects_category' table:
+ *
+ * - object: name of the category object, in the format: vendor prefix, underscore, module prefix, underscore, internal identifier or nothing; 
+ * - search_object: associated search object to display search results in 
+ * - form_object: form object which displays this category, so category will be transformed to the clickable url
+ * - list_name: form list name which values are used to populate categories from
+ * - table: table where category value is stores
+ * - field: table field name where category value is stored
+ * - join: custom SQL JOIN to use when getting number of items in the particular category (filter inactive items here)
+ * - where: custom SQL WHERE to use when getting number of items in the particular category (filter inactive items here)
+ * - override_class_name: user defined class name which is derived from 'Templ' class.
+ * - override_class_file: the location of the user defined class, leave it empty if class is located in system folders.
+ *
+ */
+class BxDolCategory extends BxDolFactory implements iBxDolFactoryObject
+{
+    protected $_sObject;
+    protected $_aObject;
+
+    /**
+     * Constructor
+     * @param $aObject array of editor options
+     */
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+
+        $this->_sObject = $aObject['object'];
+        $this->_aObject = $aObject;
+    }
+
+    /**
+     * Get category object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    static public function getObjectInstance($sObject = false)
+    {
+        if (isset($GLOBALS['bxDolClasses']['BxDolCategory!'.$sObject]))
+            return $GLOBALS['bxDolClasses']['BxDolCategory!'.$sObject];
+
+        $aObject = BxDolCategoryQuery::getCategoryObject($sObject);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        $sClass = 'BxTemplCategory';
+        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']['BxDolCategory!'.$sObject] = $o);
+    }
+
+    /**
+     * Get category object instance by form object and list name
+     * @param $sObjectForm form object name
+     * @patam $sListName list name
+     * @return object instance or false on error
+     */
+    static public function getObjectInstanceByFormAndList($sObjectForm, $sListName)
+    {
+        $aObject = BxDolCategoryQuery::getCategoryObjectByFormAndList($sObjectForm, $sListName);
+        return self::getObjectInstance($aObject['object']);
+    }
+
+    /**
+     * Get object name 
+     */
+    public function getObjectName()
+    {
+        return $this->_sObject;
+    }
+
+    /**
+     * Set condition for search results object for category object
+     * @param $oSearchResult search results object
+     * @param $mixedCategory category
+     */
+    public function setSearchCondition($oSearchResult, $mixedCategory)
+    {
+        $oSearchResult->aCurrent['restriction']['category_' . $this->_sObject] = array(
+            'value' => $mixedCategory,
+            'field' => $this->_aObject['field'],
+            'operator' => '=',
+            'table' => $this->_aObject['table'],
+        );
+    }
+
+    /**
+     * Get number of items in the specified category
+     * @param $sCategoryValue category value
+     * @return number
+     */
+    public function getItemsNum($sCategoryValue)
+    {
+        return BxDolCategoryQuery::getItemsNumInCategory ($this->_aObject, $sCategoryValue);
+    }
+}
+
+/** @} */

+ 122 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolChart.php

@@ -0,0 +1,122 @@
+<?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 BxDolChart extends BxDolFactory implements iBxDolFactoryObject
+{
+    protected $_oDb;
+
+    protected $_sObject;
+    protected $_aObject;
+
+    protected $_sStatusActive;
+
+    /**
+     * Constructor
+     * @param $aObject array of chart options
+     */
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+
+        $this->_oDb = new BxDolChartQuery($this->_aObject);
+
+        $this->_sObject = $aObject['object'];
+        $this->_aObject = $aObject;
+
+        $this->_sStatusActive = 'active';
+    }
+
+    /**
+     * Get editor object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    static public function getObjectInstance($sObject = false, $oTemplate = false)
+    {
+        if(isset($GLOBALS['bxDolClasses']['BxDolChart!' . $sObject]))
+            return $GLOBALS['bxDolClasses']['BxDolChart!' . $sObject];
+
+        $aObject = BxDolChartQuery::getChartObject($sObject);
+        if(!$aObject || !is_array($aObject))
+            return false;
+
+        $sClass = 'BxDolChart';
+        if(!empty($aObject['class_name'])) {
+            $sClass = $aObject['class_name'];
+            if(!empty($aObject['class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aObject['class_file']);
+        }
+
+        $o = new $sClass($aObject, $oTemplate);
+        return ($GLOBALS['bxDolClasses']['BxDolChart!' . $sObject] = $o);
+    }
+
+    public function actionLoadDataByInterval()
+    {
+        $mixedResult = $this->checkAllowedView();
+        if($mixedResult !== CHECK_ACTION_RESULT_ALLOWED)
+            return array('error' => $mixedResult);
+
+        $iFrom = $this->_getTimestamp(bx_get('from'));
+        $iTo = $this->_getTimestamp(bx_get('to'), true);
+        if(!$iFrom || !$iTo)
+            return echoJson(array('error' => _t('_Error Occured')));
+
+        $aData = $this->_getDataByInterval($iFrom, $iTo);
+        if(empty($aData) || !is_array($aData))
+            return echoJson(array('error' => _t('_Empty')));
+
+        return echoJson(array (
+            'title' => _t($this->_aObject['title']),
+            'data' => $aData,
+            'hide_date_range' => $this->_aObject['field_date_dt'] || $this->_aObject['field_date_ts'] ? false : true,
+            'column_date' => $this->_aObject['column_date'] >= 0 ? $this->_aObject['column_date'] : false,
+            'column_count' => $this->_aObject['column_count'] >= 0 ? $this->_aObject['column_count'] : false,
+            'type' => $this->_aObject['type'] ? $this->_aObject['type'] : 'line',
+            'options' => $this->_aObject['options'] ? unserialize($this->_aObject['options']) : false,
+        ));
+    }
+
+    public function checkAllowedView($isPerformAction = false)
+    {
+        return BxDolService::call('system', 'check_allowed_view', array($isPerformAction), 'TemplChartServices');
+    }
+
+    protected function _getQuery()
+    {
+        if(!empty($this->_aObject['query'])) 
+            return $this->_aObject['query'];
+
+        $sWhereClause = "";
+        if(!empty($this->_aObject['field_status'])) {
+            $aStatusFields = explode(',', $this->_aObject['field_status']);
+            foreach($aStatusFields as $sStatusField)
+                $sWhereClause .= " AND `" . $sStatusField . "`='" . $this->_sStatusActive . "'";
+        }
+
+        return "SELECT {field_date_formatted} AS `period`, COUNT(*) AS {object} FROM {table} WHERE 1 " . $sWhereClause . " {where_inteval} GROUP BY `period` ORDER BY {field_date} ASC";
+    }
+
+    protected function _getTimestamp($sDate, $isNowIfError = false)
+    {
+        $aDate = explode('-', $sDate); // YYYY-MM-DD
+        if(!$aDate || empty($aDate[0]) || empty($aDate[1]) || empty($aDate[2]) || !(int)$aDate[0] || !(int)$aDate[1] || !(int)$aDate[2])
+            return $isNowIfError ? time() : false;
+
+        return mktime(0, 0, 0, $aDate[1], $aDate[2], $aDate[0]);
+    }
+
+    protected function _getDate($iDate)
+    {
+        return date('Y-m-d', $iDate);
+    }
+}
+
+/** @} */

+ 88 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolChartGrowth.php

@@ -0,0 +1,88 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+
+class BxDolChartGrowth extends BxDolChart
+{
+    protected function __construct($aObject)
+    {
+        parent::__construct($aObject);
+    }
+
+    protected function _getDataByInterval($iFrom, $iTo)
+    {
+        $sFieldDate = $this->_aObject['field_date_dt'] ? $this->_aObject['field_date_dt'] : $this->_aObject['field_date_ts'];
+        $aMarkers = array (
+        	'field_date' => "`" . $sFieldDate . "`",
+            'field_date_formatted' => "DATE_FORMAT(" . ($this->_aObject['field_date_dt'] ? "`{$this->_aObject['field_date_dt']}`" : "FROM_UNIXTIME(`{$this->_aObject['field_date_ts']}`)") . ", '%Y-%m-%d')",
+            'object' => $this->_aObject['object'],
+            'table' => "`{$this->_aObject['table']}`",
+            'where_inteval' => "AND `" . $this->_aObject['table'] . "`.`" . $sFieldDate . "` >= :from AND `" . $this->_aObject['table'] . "`.`" . $sFieldDate . "` <= :to"
+        );
+
+        // build query
+        $sQuery = $this->_getQuery();
+        $sQueryInterval = bx_replace_markers($sQuery, $aMarkers);
+
+        $aBindings = array(
+            'from' => $this->_aObject['field_date_dt'] ? $this->_getDate($iFrom) . ' 00:00:00' : $iFrom,
+            'to' => $this->_aObject['field_date_dt'] ? $this->_getDate($iTo) . ' 23:59:59' : $iTo + 24*3600 - 1,
+        );
+
+        // get data
+        if ($this->_aObject['column_date'] >= 0)
+            $aData = $this->_oDb->getAllWithKey($sQueryInterval, $this->_aObject['column_date'], $aBindings, PDO::FETCH_NUM);
+        else
+            $aData = $this->_oDb->getAll($sQueryInterval, array(), $aBindings, PDO::FETCH_NUM);
+
+        if (!$aData)
+            return false;
+
+        // get interval first value
+        $aMarkers['where_inteval'] = "AND `" . $this->_aObject['table'] . "`.`" . $sFieldDate . "` < :from";
+        $aDataInit = $this->_oDb->getAll(bx_replace_markers($sQuery, $aMarkers), array(
+            'from' => $aBindings['from']
+        ));
+
+        $iCountInit = 0;
+        foreach($aDataInit as $aDataItem) 
+            $iCountInit += $aDataItem[$this->_aObject['object']];
+
+        // fill in missed days and convert values to numbers
+        if ($this->_aObject['column_date'] >= 0) {
+            $iCountCurrent = 0;
+            $aDataSlice = array_slice($aData, 0, 1);
+            $iColumnsNum = count(array_pop($aDataSlice));
+            for ($i = $iFrom ; $i <= ($iTo + 24*3600 - 1); $i += 24*60*60) {
+                $sDate = $this->_getDate($i);
+                $aRow = array ();
+                for ($j = 0 ; $j < $iColumnsNum ; ++$j) {
+                    if($this->_aObject['column_date'] != $j) {
+                        $iCountCurrent += isset($aData[$sDate]) ? (int)$aData[$sDate][$j] : 0;
+                        $aRow[$j] = $iCountInit + $iCountCurrent;
+                    }
+                    else
+                        $aRow[$j] = $sDate;
+                }
+                $aData[$sDate] = $aRow;
+            }
+        } 
+        else
+            foreach ($aData as $k => $v)
+                foreach ($aData[$k] as $kk => $vv)
+                    if ($kk > 0)
+                        $aData[$k][$kk] = (int)$aData[$k][$kk];
+
+        // return values only
+        ksort($aData);
+        return array_values($aData);
+    }
+}
+
+/** @} */

+ 77 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolChartGrowthSpeed.php

@@ -0,0 +1,77 @@
+<?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 BxDolChartGrowthSpeed extends BxDolChart
+{
+    protected function __construct($aObject)
+    {
+        parent::__construct($aObject);
+    }
+
+    protected function _getDataByInterval($iFrom, $iTo)
+    {
+        $sFieldDate = $this->_aObject['field_date_dt'] ? $this->_aObject['field_date_dt'] : $this->_aObject['field_date_ts'];
+        $aMarkers = array (
+        	'field_date' => "`" . $sFieldDate . "`",
+            'field_date_formatted' => "DATE_FORMAT(" . ($this->_aObject['field_date_dt'] ? "`{$this->_aObject['field_date_dt']}`" : "FROM_UNIXTIME(`{$this->_aObject['field_date_ts']}`)") . ", '%Y-%m-%d')",
+            'object' => $this->_aObject['object'],
+            'table' => "`{$this->_aObject['table']}`",
+            'where_inteval' => "AND `" . $this->_aObject['table'] . "`.`" . $sFieldDate . "` >= :from AND `" . $this->_aObject['table'] . "`.`" . $sFieldDate . "` <= :to"
+        );
+
+        // build query
+        $sQuery = $this->_getQuery();
+        $sQuery = bx_replace_markers($sQuery, $aMarkers);
+
+        $aBindings = array(
+            'from' => $this->_aObject['field_date_dt'] ? $this->_getDate($iFrom) . ' 00:00:00' : $iFrom,
+            'to' => $this->_aObject['field_date_dt'] ? $this->_getDate($iTo) . ' 23:59:59' : $iTo + 24*3600 - 1,
+        );
+
+        // get data
+        if ($this->_aObject['column_date'] >= 0)
+            $aData = $this->_oDb->getAllWithKey($sQuery, $this->_aObject['column_date'], $aBindings, PDO::FETCH_NUM);
+        else
+            $aData = $this->_oDb->getAll($sQuery, array(), $aBindings, PDO::FETCH_NUM);
+
+        if (!$aData)
+            return false;
+
+        // fill in missed days and convert values to numbers
+        if ($this->_aObject['column_date'] >= 0) {
+            $iCountCurrent = 0;
+            $aDataSlice = array_slice($aData, 0, 1);
+            $iColumnsNum = count(array_pop($aDataSlice));
+            for ($i = $iFrom ; $i <= ($iTo + 24*3600 - 1); $i += 24*60*60) {
+                $sDate = $this->_getDate($i);
+                $aRow = array ();
+                for ($j = 0 ; $j < $iColumnsNum ; ++$j) {
+                    if($this->_aObject['column_date'] != $j) {
+                        $aRow[$j] = isset($aData[$sDate]) ? (int)$aData[$sDate][$j] : 0;
+                    }
+                    else
+                        $aRow[$j] = $sDate;
+                }
+                $aData[$sDate] = $aRow;
+            }
+        } 
+        else
+            foreach ($aData as $k => $v)
+                foreach ($aData[$k] as $kk => $vv)
+                    if ($kk > 0)
+                        $aData[$k][$kk] = (int)$aData[$k][$kk];
+
+        // return values only
+        ksort($aData);
+        return array_values($aData);
+    }
+}
+
+/** @} */

+ 42 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolChartQuery.php

@@ -0,0 +1,42 @@
+<?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 BxDolChartQuery extends BxDolDb
+{
+    public function __construct($aObject = array())
+    {
+        parent::__construct();
+    }
+
+    static public function getChartObject($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_chart` WHERE `object` = ?", $sObject);
+
+        $aObject = $oDb->getRow($sQuery);
+        if(!$aObject || !is_array($aObject))
+            return false;
+
+        return $aObject;
+    }
+
+    static public function getChartObjects()
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $aObjects = $oDb->getAll("SELECT * FROM `sys_objects_chart` WHERE `active` = '1' ORDER BY `order` ASC");
+        if(!$aObjects || !is_array($aObjects))
+            return false;
+
+        return $aObjects;
+    }
+}
+
+/** @} */

+ 1256 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolCmts.php

@@ -0,0 +1,1256 @@
+<?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 $_sSystem = 'profile'; ///< current comment system name
+    protected $_aSystem = array (); ///< current comments system array
+    protected $_iId = 0; ///< obect id to be commented
+
+    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->_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 ()
+    {
+        if (!isset($GLOBALS['bx_dol_cmts_systems'])) {
+            $GLOBALS['bx_dol_cmts_systems'] = 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['bx_dol_cmts_systems'];
+    }
+
+    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 getBaseUrl()
+    {
+        return $this->_replaceMarkers($this->_sBaseUrl);
+    }
+
+    public function getListUrl()
+    {
+        return $this->getBaseUrl() . '#' . $this->getListAnchor();
+    }
+
+    public function getListAnchor()
+    {
+        return sprintf($this->_sListAnchor, str_replace('_', '-', $this->getSystemName()), $this->getId());
+    }
+
+	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 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 onModuleUninstall ($sModuleName, &$iFiles = null)
+    {
+        $aSystems = self::getSystems();
+        foreach($aSystems as $sSystem => $aSystem) {
+            if (0 !== strncasecmp($sModuleName, $sSystem, strlen($sModuleName)))
+                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 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 '';
+
+        $mixedResult = BxDolService::call($this->_aSystem['module'], 'check_allowed_comments_view', array($this->getId(), $this->getSystemName()));
+        if($mixedResult !== 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 '';
+
+        $mixedResult = BxDolService::call($this->_aSystem['module'], 'check_allowed_comments_view', array($this->getId(), $this->getSystemName()));
+        if($mixedResult !== 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;
+        };
+
+        $iCmtId = 0;
+        if(bx_get('Cmt') !== false)
+            $iCmtId = bx_process_input(bx_get('Cmt'), BX_DATA_INT);
+
+        $aCmt = $this->_oQuery->getCommentSimple ($this->getId(), $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;
+        }
+
+        $iCmtAuthorId = $this->_getAuthorId();
+        if(!$this->isRemoveAllowed($aCmt)) {
+            echoJson(array('msg' => $aCmt['cmt_author_id'] == $iCmtAuthorId ? strip_tags($this->msgErrRemoveAllowed()) : _t('_Access denied')));
+            return;
+        }
+
+        $iCmtObjectId = $this->getId();
+        if($this->_oQuery->removeComment($iCmtObjectId, $aCmt['cmt_id'], $aCmt['cmt_parent_id'])) {
+            $this->_triggerComment();
+
+            $oStorage = BxDolStorage::getObjectInstance($this->getStorageObjectName());
+
+            $aImages = $this->_oQuery->getFiles($this->_aSystem['system_id'], $aCmt['cmt_id']);
+            foreach($aImages as $aImage)
+                $oStorage->deleteFile($aImage['image_id']);
+
+            $this->_oQuery->deleteImages($this->_aSystem['system_id'], $aCmt['cmt_id']);
+
+            $this->isRemoveAllowed(true);
+
+            $this->deleteMetaInfo ($aCmt['cmt_id']);
+
+            $oZ = new BxDolAlerts($this->_sSystem, 'commentRemoved', $iCmtObjectId, $iCmtAuthorId, array('comment_id' => $aCmt['cmt_id'], 'comment_author_id' => $aCmt['cmt_author_id']));
+            $oZ->alert();
+
+            $oZ = new BxDolAlerts('comment', 'deleted', $aCmt['cmt_id'], $iCmtAuthorId, array('object_system' => $this->_sSystem, 'object_id' => $iCmtObjectId));
+        	$oZ->alert();
+
+            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($iId)
+    {
+        $iObjectId = (int)$this->getId();
+        $iObjectAuthorId = $this->getObjectAuthorId($iObjectId);
+
+        $aComment = $this->_oQuery->getCommentSimple($iObjectId, $iId);
+        if(empty($aComment) || !is_array($aComment))
+            return false;
+
+        $iAuthorId = (int)$aComment['cmt_author_id'];
+        $oZ = new BxDolAlerts($this->_sSystem, 'commentPost', $iObjectId, $iAuthorId, array('comment_id' => $iId, 'comment_text' => $aComment['cmt_text'], 'comment_author_id' => $iAuthorId, 'object_author_id' => $iObjectAuthorId));
+        $oZ->alert();
+
+        $oZ = new BxDolAlerts('comment', 'added', $iId, $iAuthorId, array('object_system' => $this->_sSystem, 'object_id' => $iObjectId, 'object_author_id' => $iObjectAuthorId, 'comment_text' => $aComment['cmt_text']));
+        $oZ->alert();
+        
+        return true;
+    }
+
+    public function onEditAfter($iId)
+    {
+        $iObjectId = (int)$this->getId();
+    	$iObjectAuthorId = $this->getObjectAuthorId($iObjectId);
+
+    	$aComment = $this->_oQuery->getCommentSimple($iObjectId, $iId);
+        if(empty($aComment) || !is_array($aComment))
+            return false;
+
+        $iAuthorId = (int)$aComment['cmt_author_id'];
+        $oZ = new BxDolAlerts($this->_sSystem, 'commentUpdated', $iObjectId, $iAuthorId, array('comment_id' => $iId, 'comment_author_id' => $iAuthorId, 'object_author_id' => $iObjectAuthorId));
+        $oZ->alert();
+
+        $oZ = new BxDolAlerts('comment', 'edited', $iId, $iAuthorId, array('object_system' => $this->_sSystem, 'object_id' => $iObjectId, 'object_author_id' => $iObjectAuthorId));
+        $oZ->alert();
+
+        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));
+        $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' => '' 
+            )
+        );
+
+        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'] : '',
+                );
+
+        return $aResult;
+    }
+
+    public function serviceGetSearchResultExtended($aParams)
+    {
+        if(empty($aParams) || !is_array($aParams))
+            return array();
+
+        return $this->_oQuery->getCommentsBy(array('type' => 'search_ids', 'search_params' => $aParams));
+    }
+
+    /**
+     * 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()
+        );
+    }
+
+    protected function _getAuthorObject($iAuthorId = 0)
+    {
+        $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);
+    }
+
+    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->keywordsParse($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['reply_text'] = $this->_prepareTextForOutput($aCmt['cmt_text'], $iCmtId);
+        $aPlus['comment_url'] = sprintf('%scmts.php?sys=%s&id=%d&cmt_id=%d', BX_DOL_URL_ROOT, $this->_sSystem, $this->_iId, $iCmtParentId);
+
+        $aTemplate = BxDolEmailTemplates::getInstance()->parseTemplate('t_CommentReplied', $aPlus);
+        return $aTemplate && sendMail($aAccount['email'], $aTemplate['Subject'], $aTemplate['Body']);
+    }
+}
+
+/** @} */

+ 503 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolCmtsQuery.php

@@ -0,0 +1,503 @@
+<?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`";
+
+                $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;
+
+                        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;
+    }
+}
+
+/** @} */

+ 736 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolConnection.php

@@ -0,0 +1,736 @@
+<?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()) {
+            $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)
+    {
+        return $this->_oQuery->onDelete ($iIdInitiator, 'initiator');
+    }
+
+    /**
+     * 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)
+    {
+        return $this->_oQuery->onDelete ($iIdContent, 'content');
+    }
+
+
+    /**
+     * 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');
+    }
+}
+
+/** @} */

+ 286 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolConnectionQuery.php

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

+ 303 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/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_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_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_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)
+    {
+        return $this->_call('get_search_result_extended', $aParams);
+    }
+
+    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.B5-9.0.0.RC1/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)
+    {
+        return $this->_oCmts->serviceGetSearchResultExtended($aParams);
+    }
+}
+
+/** @} */

+ 154 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolCover.php

@@ -0,0 +1,154 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Cover.
+ *
+ * It displays cover area for different pages, usually it have background image, text and some buttons on it.
+ *
+ * @section example Example of usage
+ *
+ * Setting cover:
+ *
+ * @code
+ *  $oCover = BxDolCover::getInstance(); // get object instance
+ *  if ($oCover) // check if Cover is available for using
+ *      $oCover->set (array( // set cover with default template
+ *          'title' => 'Hello World!',
+ *      )); 
+ * @endcode
+ *
+ */
+class BxDolCover extends BxDolFactory implements iBxDolSingleton
+{
+    protected $_aOptions = array();
+    protected $_sTemplateName = 'cover.html';
+    protected $_sTemplateNameEmpty = 'cover_empty.html';
+    protected $_sCoverImageUrl = false;
+    protected $_aOptiondDefault = array();
+
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses']['BxDolCover']))
+            trigger_error ('Multiple instances are not allowed for the BxDolCover class.', E_USER_ERROR);
+
+        parent::__construct();
+
+        $this->_aOptiondDefault = array(
+            'class' => '',
+            'title' => '',
+            'actions' => '',
+            'bx_if:image' => array (
+                'condition' => false,
+                'content' => array(),
+            ),
+            'bx_if:icon' => array (
+                'condition' => false,
+                'content' => array(),
+            ),
+            'bx_if:bg' => array (
+                'condition' => false,
+                'content' => array(),
+            ),
+            'bx_if:logged_in' => array(
+                'condition' => isLogged(),
+                'content' => array(),
+            ),
+        );
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses']['BxDolCover']))
+            trigger_error('Clone is not allowed for the BxDolCover class.', E_USER_ERROR);
+    }
+
+    /**
+     * Get Cover object instance
+     * @return object instance or false on error
+     */
+    public static function getInstance($oTemplate = false)
+    {
+        if (isset($GLOBALS['bxDolClasses']['BxDolCover']))
+            return $GLOBALS['bxDolClasses']['BxDolCover'];
+
+        $o = new BxTemplCover($oTemplate);
+
+        return ($GLOBALS['bxDolClasses']['BxDolCover'] = $o);
+    }
+
+    /**
+     * Determine whether cover is already set for the page or not.  
+     */
+    public function isCover()
+    {
+    	return !empty($this->_sCoverImageUrl) || !empty($this->_aOptions);
+    }
+
+    /**
+     * Set custom cover options
+     * @param $aOptions - cover options, default template supports the following: title. For more info @see template/cover.html
+     * @param $sTemplateName - optional template name
+     */
+    public function set ($aOptions, $sTemplateName = false)
+    {
+        $this->_aOptions = $aOptions;
+        if (false !== $sTemplateName)
+            $this->_sTemplateName = $sTemplateName;
+
+        bx_alert('system', 'set_cover', 0, false, array('options' => &$this->_aOptions, 'template_name' => &$this->_sTemplateName, 'cover' => $this));
+    }
+
+    /**
+     * Set cover image url for default cover template
+     * @param $mixedCoverImageUrl - cover image url, or array with file id and storage or transcoder object:
+     * @code
+     *   array('id' => 12, 'object' => 'sample');
+     *   // or 
+     *   array('id' => 21, 'transcoder' => 'sample');
+     * @endcode
+     */
+    public function setCoverImageUrl ($mixedCoverImage)
+    {
+        if (is_string($mixedCoverImage)) {
+            $this->_sCoverImageUrl = $mixedCoverImage;
+        } 
+        elseif (is_array($mixedCoverImage) && isset($mixedCoverImage['id']) && (isset($mixedCoverImage['object']) || isset($mixedCoverImage['transcoder']))) {
+
+            if (!empty($mixedCoverImage['object']))
+                $o = BxDolStorage::getObjectInstance($mixedCoverImage['object']);
+            elseif (!empty($mixedCoverImage['transcoder']))
+                $o = BxDolTranscoder::getObjectInstance($mixedCoverImage['transcoder']);
+            if (!$o)
+                return false;
+
+            $this->_sCoverImageUrl = $o->getFileUrlById($mixedCoverImage['id']);
+        } 
+        else {
+            return false;
+        }
+
+        bx_alert('system', 'set_cover_image', 0, false, array('cover_image' => &$this->_sCoverImageUrl, 'cover' => $this));
+
+        return true;
+    }
+
+    /**
+     * Set cover area class
+     */
+    public function setCoverClass ($sClass)
+    {
+        $this->_aOptiondDefault['class'] = $sClass;
+    }
+}
+
+/** @} */

+ 88 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolCronAccount.php

@@ -0,0 +1,88 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolCronAccount extends BxDolCron
+{
+    protected $_aParseParams;
+
+    public function __construct()
+    {
+        parent::__construct();
+
+        $this->_aParseParams = array(
+            'site_title' => getParam('site_title')
+        );
+    }
+
+    public function processing()
+    {
+        if(getParam('enable_notification_account') != 'on')
+            return;
+
+        set_time_limit(0);
+        ignore_user_abort();
+
+        $this->processNewlyJoined();
+
+        if(empty($this->_aParseParams['account_count']) || empty($this->_aParseParams['account_output']))
+			return;
+
+        $aTemplate = BxDolEmailTemplates::getInstance()->parseTemplate('t_Account', $this->_aParseParams);
+        if(empty($aTemplate))
+        	return;
+
+        $aSent = array();
+        $aProfiles = BxDolAclQuery::getInstance()->getProfilesByMembership(array(MEMBERSHIP_ID_MODERATOR, MEMBERSHIP_ID_ADMINISTRATOR));
+        foreach($aProfiles as $aProfile) {
+        	$oProfile = BxDolProfile::getInstance($aProfile['id']);
+        	if(!$oProfile)
+        		continue;
+        	
+        	$oAccount = $oProfile->getAccountObject();
+        	if(!$oAccount || !$oAccount->isConfirmed())
+        		continue;
+
+        	$aAccount = $oAccount->getInfo();
+        	if((int)$aAccount['receive_news'] != 1)
+        		continue;
+
+        	if(in_array($aAccount['email'], $aSent))
+        		continue;
+
+            if(sendMail($aAccount['email'], $aTemplate['Subject'], $aTemplate['Body'], $aProfile['id']))
+            	$aSent[] = $aAccount['email'];
+        }
+    }
+
+    protected function processNewlyJoined()
+    {
+        $aAccounts = BxDolAccountQuery::getInstance()->getAccounts(array('type' => 'by_join_date', 'date' => time() - 86400));
+        if(empty($aAccounts) || !is_array($aAccounts))
+            return;
+
+        $iAccounts = 0;
+        $sAccounts = "";
+        foreach($aAccounts as $aAccount) {
+        	$oProfile = BxDolProfile::getInstance($aAccount['profile_id']);
+        	if(!$oProfile)
+        	    continue;
+
+        	$sAccounts .= _t('_sys_notification_account_link', $oProfile->getUrl(), $oProfile->getDisplayName(), $aAccount['email']);
+        	$iAccounts += 1;
+        }
+
+        if(!$sAccounts)
+            return;
+
+        $this->_aParseParams['account_count'] = $iAccounts;
+        $this->_aParseParams['account_output'] = _t('_sys_notification_account', $sAccounts);
+    }
+}
+
+/** @} */

+ 1127 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolDb.php

@@ -0,0 +1,1127 @@
+<?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 = 'utf8';
+        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 'utf8'");
+            $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);
+
+			self::$_aDbCacheData = array();
+    	}
+    	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 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]);
+    }
+
+    protected function cacheParams($bForceCacheInvalidate = false)
+    {
+        if ($bForceCacheInvalidate)
+            $this->cacheParamsClear();
+
+        self::$_aParams = $this->fromCache(self::$_sParamsCacheName, 'getPairs', "SELECT `name`, `value` FROM `sys_options`", "name", "value");
+
+        $aMixed = $this->fromCache(self::$_sParamsCacheNameMixed, '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`.`active`='1'", "option", "value");
+        if(!empty($aMixed))
+        	self::$_aParams = array_merge(self::$_aParams, $aMixed);
+
+        if (empty(self::$_aParams)) {
+            self::$_aParams = array ();
+            return false;
+        }
+
+        return true;
+    }
+
+    public function cacheParamsClear()
+    {
+        return $this->cleanCache(self::$_sParamsCacheName);
+    }
+
+    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))
+			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;
+    }
+
+    /**
+     * 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;
+
+    	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>';
+
+		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);
+}
+
+/** @} */

+ 156 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolEditor.php

@@ -0,0 +1,156 @@
+<?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
+ * @{
+ */
+
+/**
+ * Standard WYSIWYG editor view.
+ * @see BxDolEditor::attachEditor
+ */
+define('BX_EDITOR_STANDARD', 1);
+
+/**
+ * Full WYSIWYG editor view. If not supported by editor, standard view is used.
+ * @see BxDolEditor::attachEditor
+ */
+define('BX_EDITOR_FULL', 2);
+
+/**
+ * Mini WYSIWYG editor view. If not supported by editor, standard view is used.
+ * @see BxDolEditor::attachEditor
+ */
+define('BX_EDITOR_MINI', 3);
+
+/**
+ * WYSIWYG editors.
+ *
+ * Site owner can choose which visual editor can be user on the site.
+ *
+ * Default visual editor is stored in 'sys_editor_default' setting option.
+ *
+ * @section editor_create Creating the Editor object:
+ *
+ *
+ * Add record to 'sys_objects_editor' table:
+ *
+ * - object: name of the editor object, in the format: vendor prefix, underscore, module prefix, underscore, internal identifier or nothing; for example: bx_blogs - custom editor in blogs module.
+ * - title: title of the editor, displayed in the studio.
+ * - skin: editor skin, if editor suports custom/multiple skins.
+ * - override_class_name: user defined class name which is derived from one of base editor classes.
+ * - override_class_file: the location of the user defined class, leave it empty if class is located in system folders.
+ *
+ *
+ * @section example Example of usage
+ *
+ * Apply visual editor to textarea:
+ *
+ * @code
+ *  echo '<textarea id="my_textarea" rows="20" cols="80">some text here</textarea>'; // print text area element
+ *
+ *  $oEditor = BxDolEditor::getObjectInstance(); // get default editor object instance
+ *  if ($oEditor) // check if editor is available for using
+ *      echo $oEditor->attachEditor ('#my_textarea', BX_EDITOR_STANDARD); // output HTML which will automatically apply editor to textarea element by its id
+ * @endcode
+ *
+ */
+class BxDolEditor extends BxDolFactory implements iBxDolFactoryObject
+{
+    protected $_sObject;
+    protected $_aObject;
+
+    /**
+     * Constructor
+     * @param $aObject array of editor options
+     */
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+
+        $this->_sObject = $aObject['object'];
+        $this->_aObject = $aObject;
+    }
+
+    /**
+     * Get editor object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    static public function getObjectInstance($sObject = false, $oTemplate = false)
+    {
+        if (!$sObject)
+            $sObject = getParam('sys_editor_default');
+
+        if (isset($GLOBALS['bxDolClasses']['BxDolEditor!'.$sObject]))
+            return $GLOBALS['bxDolClasses']['BxDolEditor!'.$sObject];
+
+        $aObject = BxDolEditorQuery::getEditorObject($sObject);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        if (empty($aObject['override_class_name']))
+            return false;
+
+        $sClass = $aObject['override_class_name'];
+        if (!empty($aObject['override_class_file']))
+            require_once(BX_DIRECTORY_PATH_ROOT . $aObject['override_class_file']);
+
+        $o = new $sClass($aObject, $oTemplate);
+
+        return ($GLOBALS['bxDolClasses']['BxDolEditor!'.$sObject] = $o);
+    }
+
+    /**
+     * Get object name
+     */
+    public function getObjectName ()
+    {
+        return $this->_sObject;
+    }
+
+    /**
+     * Get minimal width which is neede for editor for the provided view mode
+     */
+    public function getWidth ($iViewMode)
+    {
+        // override this function in particular editor class
+    }
+
+    /**
+     * 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 synamically.
+     */
+    public function attachEditor ($sSelector, $iViewMode, $bDynamicMode = false)
+    {
+        // override this function in particular editor class
+    }
+
+    /**
+     * Add css/js files which are needed for editor display and functionality.
+     */
+    protected function _addJsCss ($bDynamicMode = false)
+    {
+        // override this function in particular editor class
+    }
+
+    /**
+     * Replace provided markers string.
+     * @param $s - string to replace markers in
+     * @param $a - markers array
+     * @return string with replaces markers
+     */
+    protected function _replaceMarkers ($s, $a)
+    {
+        if (empty($s) || empty($a) || !is_array($a))
+            return $s;
+        return bx_replace_markers($s, $a);
+    }
+
+}
+
+/** @} */

+ 188 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolEmailTemplates.php

@@ -0,0 +1,188 @@
+<?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' => BxDolTemplate::getInstance()->parseHtmlByName('bx_a.html', array(
+                'href' => BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=about'),
+				'title' => bx_html_attribute($sAboutUs),
+                'bx_repeat:attrs' => array(),
+                'content' => $sAboutUs
+            ))
+        );
+
+        $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;
+
+        if ($iAccountId) {
+            $oAccount = BxDolAccount::getInstance($iAccountId);
+            if ($oAccount && ($aAccountInfo = $oAccount->getInfo())) {
+                $aResultKeys = array_merge($aResultKeys, array(
+                    'account_id' => $aAccountInfo['id'],
+                    'account_name' => $aAccountInfo['name'],
+                    'account_email' => $aAccountInfo['email'],
+                ));
+            }
+        }
+
+        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 (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);
+    }
+}
+
+/** @} */

+ 234 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolFavorite.php

@@ -0,0 +1,234 @@
+<?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_FAVORITE_USAGE_BLOCK', 'block');
+define('BX_DOL_FAVORITE_USAGE_INLINE', 'inline');
+define('BX_DOL_FAVORITE_USAGE_DEFAULT', BX_DOL_FAVORITE_USAGE_BLOCK);
+
+/**
+ * Track any object favorites automatically
+ *
+ * Add record to sys_object_favorite table to track object favorites,
+ * to record favorites just create this class instance with your object id,
+ * for example:
+ * @code
+ *  BxDolFavorite::getObjectInstance('my_system', 25); // 25 - is object id
+ * @endcode
+ *
+ * Description of sys_object_favorite table fields:
+ * @code
+ *  `name` - system name, it is better to use unique module prefix here, lowercase and all spaces are underscored
+ *  `table_track` - table to track favorites
+ *  `is_on` - is the system activated
+ *  `trigger_table` - table where you need to update favorites field
+ *  `trigger_field_id` - table field id to unique determine object
+ *  `trigger_field_count` - table field where total favorites number is stored
+ *  `class_name` - your custom class name, if you overrride default class
+ *  `class_file` - your custom class path
+ * @endcode
+ *
+ * Structure of the track table is the following:
+ * @code
+ *  CREATE TABLE `my_favorites_track` (
+ *      `object_id` int(11) NOT NULL default '0', -- this field type must be exact as your object id type
+ *      `author_id` int(11) NOT NULL default '0', -- favoring user profile id
+ *      `date` int(11) NOT NULL default '0', -- timestamp of last recorded view
+ *      KEY `id` (`object_id`,`author_id`)
+ *  ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+ * @endcode
+ *
+ */
+class BxDolFavorite extends BxDolObject
+{
+    protected $_sBaseUrl;
+
+    protected function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
+    {
+        parent::__construct($sSystem, $iId, $iInit, $oTemplate);
+        if(empty($this->_sSystem))
+            return;
+
+        $this->_oQuery = new BxDolFavoriteQuery($this);
+
+        $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;
+    }
+
+   /**
+     * get favorites object instanse
+     * @param $sSys favorite object name
+     * @param $iId associated content id, where favorite is available
+     * @param $iInit perform initialization
+     * @return null on error, or ready to use class instance
+     */
+    public static function getObjectInstance($sSys, $iId, $iInit = true)
+    {
+        if(isset($GLOBALS['bxDolClasses']['BxDolFavorite!' . $sSys . $iId]))
+            return $GLOBALS['bxDolClasses']['BxDolFavorite!' . $sSys . $iId];
+
+        $aSystems = self::getSystems();
+        if(!isset($aSystems[$sSys]))
+            return null;
+
+        $sClassName = 'BxTemplFavorite';
+        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);
+        return ($GLOBALS['bxDolClasses']['BxDolFavorite!' . $sSys . $iId] = $o);
+    }
+
+    public static function &getSystems()
+    {
+        if(!isset($GLOBALS['bx_dol_favorite_systems']))
+            $GLOBALS['bx_dol_favorite_systems'] = BxDolDb::getInstance()->fromCache('sys_objects_favorite', 'getAllWithKey', '
+                SELECT
+                    `id` as `id`,
+                    `name` AS `name`,
+                    `table_track` AS `table_track`,
+                    `is_on` AS `is_on`,
+                    `is_undo` AS `is_undo`,
+                    `is_public` AS `is_public`,
+                    `base_url` AS `base_url`,
+                    `trigger_table` AS `trigger_table`,
+                    `trigger_field_id` AS `trigger_field_id`,
+                    `trigger_field_author` AS `trigger_field_author`,
+                    `trigger_field_count` AS `trigger_field_count`,
+                    `class_name` AS `class_name`,
+                    `class_file` AS `class_file`
+                FROM `sys_objects_favorite`', 'name');
+
+        return $GLOBALS['bx_dol_favorite_systems'];
+    }
+
+	/**
+     * Actions functions
+     */
+    public function actionFavorite()
+    {
+        return echoJson($this->_doFavorite());
+    }
+
+	public function actionGetFavoritedBy()
+    {
+        if (!$this->isEnabled())
+           return '';
+
+	    if(!$this->isAllowedFavoriteView(true))
+            return $this->msgErrAllowedFavoriteView();
+
+        return $this->_getFavoritedBy();
+    }
+
+    /**
+     * Permissions functions
+     */
+    public function isAllowedFavorite($isPerformAction = false)
+    {
+        if(isAdmin())
+            return true;
+
+        return $this->checkAction('favorite', $isPerformAction);
+    }
+
+	public function msgErrAllowedFavorite()
+    {
+        return $this->checkActionErrorMsg('favorite');
+    }
+
+    public function isAllowedFavoriteView($isPerformAction = false)
+    {
+        if(!$this->isPublic())
+            return false;
+
+        if(isAdmin())
+            return true;
+
+        return $this->checkAction('favorite_view', $isPerformAction);
+    }
+
+    public function msgErrAllowedFavoriteView()
+    {
+        return $this->checkActionErrorMsg('favorite_view');
+    }
+
+    /**
+     * Auxiliary functions
+     */
+    public function isUndo()
+    {
+        return (int)$this->_aSystem['is_undo'] == 1;
+    }
+
+    public function isPublic()
+    {
+        return (int)$this->_aSystem['is_public'] == 1;
+    }
+
+	/**
+     * Internal functions
+     */
+	protected function _doFavorite()
+    {
+        if (!$this->isEnabled())
+           return array('code' => 1, 'msg' => _t('_favorite_err_not_enabled'));
+
+        $iObjectId = $this->getId();
+        $iObjectAuthorId = $this->_oQuery->getObjectAuthorId($iObjectId);
+        $iAuthorId = $this->_getAuthorId();
+
+        $bUndo = $this->isUndo();
+        $bPerformed = $this->isPerformed($iObjectId, $iAuthorId);
+        $bPerformUndo = $bPerformed && $bUndo ? true : false;
+
+        if(!$bPerformUndo && !$this->isAllowedFavorite())
+            return array('code' => 2, 'msg' => $this->msgErrAllowedFavorite());
+
+        if($bPerformed && !$bUndo)
+        	return array('code' => 3, 'msg' => _t('_favorite_err_duplicate_favorite'));
+
+        if(!$this->_oQuery->{($bPerformUndo ? 'un' : '') . 'doFavorite'}($iObjectId, $iAuthorId))
+            return array('code' => 4, 'msg' => _t('_favorite_err_cannot_perform_action'));
+
+        if(!$bPerformUndo)
+            $this->isAllowedFavorite(true);
+
+        $this->_triggerValue($bPerformUndo ? -1 : 1);
+
+        bx_alert($this->_sSystem, ($bPerformUndo ? 'un' : '') . 'favorite', $iObjectId, $iAuthorId, array('favorite_author_id' => $iAuthorId, 'object_author_id' => $iObjectAuthorId));
+        bx_alert('favorite', ($bPerformUndo ? 'un' : '') . 'do', 0, $iAuthorId, array('object_system' => $this->_sSystem, 'object_id' => $iObjectId, 'object_author_id' => $iObjectAuthorId));
+
+        $aFavorite = $this->_oQuery->getFavorite($iObjectId);
+        return array(
+        	'eval' => $this->getJsObjectName() . '.onFavorite(oData, oElement)',
+        	'code' => 0, 
+        	'count' => $aFavorite['count'],
+        	'countf' => (int)$aFavorite['count'] > 0 ? $this->_getLabelCounter($aFavorite['count']) : '',
+            'label_icon' => $this->_getIconDoFavorite(!$bPerformed),
+            'label_title' => _t($this->_getTitleDoFavorite(!$bPerformed)),
+            'disabled' => !$bPerformed && !$bUndo
+        );
+    }
+
+    protected function _getIconDoFavorite($bPerformed)
+    {
+    	return $bPerformed && $this->isUndo() ?  'heart-o' : 'heart';
+    }
+
+    protected function _getTitleDoFavorite($bPerformed)
+    {
+    	return $bPerformed && $this->isUndo() ? '_favorite_do_unfavorite' : '_favorite_do_favorite';
+    }
+}
+
+/** @} */

+ 190 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolFeature.php

@@ -0,0 +1,190 @@
+<?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_FEATURED_USAGE_BLOCK', 'block');
+define('BX_DOL_FEATURED_USAGE_INLINE', 'inline');
+define('BX_DOL_FEATURED_USAGE_DEFAULT', BX_DOL_FEATURED_USAGE_BLOCK);
+
+/**
+ * Track any object feature automatically
+ *
+ * Add record to sys_object_feature table to track object feature,
+ * to record feature just create this class instance with your object id,
+ * for example:
+ * @code
+ *  BxDolFeature::getObjectInstance('my_system', 25); // 25 - is object id
+ * @endcode
+ *
+ * Description of sys_object_feature table fields:
+ * @code
+ *  `name` - system name, it is better to use unique module prefix here, lowercase and all spaces are underscored
+ *  `table_track` - table to track feature
+ *  `is_on` - is the system activated
+ *  `trigger_table` - table where you need to update feature field
+ *  `trigger_field_id` - table field id to unique determine object
+ *  `trigger_field_count` - table field where the featuring date is stored
+ *  `class_name` - your custom class name, if you overrride default class
+ *  `class_file` - your custom class path
+ * @endcode
+ *
+ */
+class BxDolFeature extends BxDolObject
+{
+    protected $_sBaseUrl;
+
+    protected function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
+    {
+        parent::__construct($sSystem, $iId, $iInit, $oTemplate);
+        if(empty($this->_sSystem))
+            return;
+
+        $this->_oQuery = new BxDolFeatureQuery($this);
+
+        $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;
+    }
+
+   /**
+     * get feature object instanse
+     * @param $sSys feature object name
+     * @param $iId associated content id, where feature is available
+     * @param $iInit perform initialization
+     * @return null on error, or ready to use class instance
+     */
+    public static function getObjectInstance($sSys, $iId, $iInit = true)
+    {
+        if(isset($GLOBALS['bxDolClasses']['BxDolFeature!' . $sSys . $iId]))
+            return $GLOBALS['bxDolClasses']['BxDolFeature!' . $sSys . $iId];
+
+        $aSystems = self::getSystems();
+        if(!isset($aSystems[$sSys]))
+            return null;
+
+        $sClassName = 'BxTemplFeature';
+        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);
+        return ($GLOBALS['bxDolClasses']['BxDolFeature!' . $sSys . $iId] = $o);
+    }
+
+    public static function &getSystems()
+    {
+        if(!isset($GLOBALS['bx_dol_feature_systems']))
+            $GLOBALS['bx_dol_feature_systems'] = BxDolDb::getInstance()->fromCache('sys_objects_feature', 'getAllWithKey', '
+                SELECT
+                    `id` as `id`,
+                    `name` AS `name`,
+                    `is_on` AS `is_on`,
+                    `is_undo` AS `is_undo`,
+                    `base_url` AS `base_url`,
+                    `trigger_table` AS `trigger_table`,
+                    `trigger_field_id` AS `trigger_field_id`,
+                    `trigger_field_author` AS `trigger_field_author`,
+                    `trigger_field_flag` AS `trigger_field_flag`,
+                    `class_name` AS `class_name`,
+                    `class_file` AS `class_file`
+                FROM `sys_objects_feature`', 'name');
+
+        return $GLOBALS['bx_dol_feature_systems'];
+    }
+
+	/**
+     * Actions functions
+     */
+    public function actionFeature()
+    {
+        return echoJson($this->_doFeature());
+    }
+
+    public function actionGetFeatureBy()
+    {
+        return '';
+    }
+
+    /**
+     * Permissions functions
+     */
+    public function isAllowedFeature($isPerformAction = false)
+    {
+        if(isAdmin())
+            return true;
+
+        return $this->checkAction('feature', $isPerformAction);
+    }
+
+	public function msgErrAllowedFeature()
+    {
+        return $this->checkActionErrorMsg('feature');
+    }
+
+    /**
+     * Auxiliary functions
+     */
+    public function isUndo()
+    {
+        return (int)$this->_aSystem['is_undo'] == 1;
+    }
+
+	/**
+     * Internal functions
+     */
+	protected function _doFeature()
+    {
+        if (!$this->isEnabled())
+           return array('code' => 1, 'msg' => _t('_feature_err_not_enabled'));
+
+        $iObjectId = $this->getId();
+        $iObjectAuthorId = $this->_oQuery->getObjectAuthorId($iObjectId);
+        $iAuthorId = $this->_getAuthorId();
+
+        $bUndo = $this->isUndo();
+        $bPerformed = $this->isPerformed($iObjectId, $iAuthorId);
+        $bPerformUndo = $bPerformed && $bUndo ? true : false;
+
+        if(!$bPerformUndo && !$this->isAllowedFeature())
+            return array('code' => 2, 'msg' => $this->msgErrAllowedFeature());
+
+        if($bPerformed && !$bUndo)
+        	return array('code' => 3, 'msg' => _t('_feature_err_duplicate_feature'));
+
+        if(!$this->_triggerValue($bPerformUndo ? 0 : time()))
+            return array('code' => 4, 'msg' => _t('_feature_err_cannot_perform_action'));
+            
+        if(!$bPerformUndo)
+            $this->isAllowedFeature(true);
+
+        bx_alert($this->_sSystem, ($bPerformUndo ? 'un' : '') . 'feature', $iObjectId, $iAuthorId, array('object_author_id' => $iObjectAuthorId));
+        bx_alert('feature', ($bPerformUndo ? 'un' : '') . 'do', 0, $iAuthorId, array('object_system' => $this->_sSystem, 'object_id' => $iObjectId, 'object_author_id' => $iObjectAuthorId));
+
+        return array(
+        	'eval' => $this->getJsObjectName() . '.onFeature(oData, oElement)',
+        	'code' => 0, 
+        	'label_icon' => $this->_getIconDoFeature(!$bPerformed),
+            'label_title' => _t($this->_getTitleDoFeature(!$bPerformed)),
+            'disabled' => !$bPerformed && !$bUndo
+        );
+    }
+
+    protected function _getIconDoFeature($bPerformed)
+    {
+    	return $bPerformed && $this->isUndo() ? 'star-o' : 'star';
+    }
+
+    protected function _getTitleDoFeature($bPerformed)
+    {
+    	return $bPerformed && $this->isUndo() ? '_feature_do_unfeature' : '_feature_do_feature';
+    }
+}
+
+/** @} */

+ 118 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolFileHandler.php

@@ -0,0 +1,118 @@
+<?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
+ * @{
+ */
+
+/**
+ * File preview object, it's used to preview different files in browser which browse usually doesn't support.
+ * 
+ *
+ * @section file_handler_add Adding new files handler:
+ *
+ *
+ * Add record to 'sys_objects_file_handlers' table:
+ *
+ * - object: name of the file handler object, in the format: vendor prefix, underscore, module prefix, underscore, internal identifier or nothing; for example: bx_abode_pdf - custom file handler from Abode to display PDF files in browser.
+ * - title: file handler title
+ * - override_class_name: user defined class name which is derived from one of base file handler class.
+ * - override_class_file: the location of the user defined class, leave it empty if class is located in system folders.
+ *
+ *
+ * @section example Example of usage
+ *
+ * Display file preview:
+ *
+ * @code
+ *  $oFileHandler = BxDolFileHandler::getObjectInstanceByFile('hello.pdf'); // get file preview class
+ *  if ($oFileHandler) // check if preview is available for gived file
+ *      echo $oFileHandler->display (); // output HTML which will automatically show file preview
+ * @endcode
+ *
+ */
+class BxDolFileHandler extends BxDolFactory implements iBxDolFactoryObject
+{
+    protected $_sObject;
+    protected $_aObject;
+
+    /**
+     * Constructor
+     * @param $aObject array of file handler options
+     */
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+
+        $this->_sObject = $aObject['object'];
+        $this->_aObject = $aObject;
+    }
+
+    /**
+     * Get file handler 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'][__CLASS__.'!'.$sObject]))
+            return $GLOBALS['bxDolClasses'][__CLASS__.'!'.$sObject];
+
+        $aObjects = BxDolFileHandlerQuery::getObjects();
+        if (!$aObjects || !isset($aObjects[$sObject]))
+            return false;
+
+        $aObject = $aObjects[$sObject];
+
+        if (empty($aObject['override_class_name']))
+            return false;
+
+        $sClass = $aObject['override_class_name'];
+        if (!empty($aObject['override_class_file']))
+            require_once(BX_DIRECTORY_PATH_ROOT . $aObject['override_class_file']);
+
+        $o = new $sClass($aObject);
+        if (!$o->isActive())
+            return false;
+
+        return ($GLOBALS['bxDolClasses'][__CLASS__.'!'.$sObject] = $o);
+    }
+
+    /**
+     * Get file handler object instance by file name
+     * @param $sFileName file name
+     * @return object instance or false on error
+     */
+    static public function getObjectInstanceByFile($sFileName)
+    {
+        if (!($aObjects = BxDolFileHandlerQuery::getObjects()))
+            return false;
+
+        $sObject = false;
+        foreach ($aObjects as $sObject => $aObject)
+            if (preg_match($aObject['preg_ext'], $sFileName))
+                return self::getObjectInstance($sObject);
+
+        return false;
+    }
+    
+    /**
+     * Display file preview
+     */
+    public function display ($sFileUrl, $aFile)
+    {
+        // override this function in particular class
+    }
+
+    /**
+     * Check if file preview is available
+     */
+    public function isActive ()
+    {
+        return $this->_aObject['active'];
+    }
+}
+
+/** @} */

+ 1668 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolForm.php

@@ -0,0 +1,1668 @@
+<?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);
+
+    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)
+    {
+        if (isset($GLOBALS['bxDolClasses']['BxDolForm!'.$sObject.'!'.$sDisplayName]))
+            return $GLOBALS['bxDolClasses']['BxDolForm!'.$sObject.'!'.$sDisplayName];
+
+        $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']['BxDolForm!'.$sObject.'!'.$sDisplayName] = $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 ()
+    {
+        return BxDolForm::getSubmittedValue($this->aParams['db']['submit_name'], $this->aFormAttrs['method'], $this->_aSpecificValues) ? true : 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;
+
+        // 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)
+                return false;
+        }
+
+        $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;
+                }
+            }
+
+            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 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)
+    {
+        $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)
+    {
+        if (!is_array($a))
+            return true;
+        if (empty($a))
+            return true;
+        foreach ($a as $k => $v)
+            if ($v)
+                return false;
+        return true;
+    }
+}
+
+/** @} */

+ 215 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolFormQuery.php

@@ -0,0 +1,215 @@
+<?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']);
+
+        $aDefaultsFormParams = array(
+            'db' => array(
+                'submit_name' => $aObject['submit_name'],
+                '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);
+    }
+
+}
+
+/** @} */

+ 565 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolGrid.php

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

+ 104 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolGridSubscribedMe.php

@@ -0,0 +1,104 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+
+class BxDolGridSubscribedMe extends BxTemplGrid
+{
+    protected $_bOwner = false;
+
+    protected $_sObjectConnections = 'sys_profiles_subscriptions';
+    protected $_oConnection;
+    protected $_sConnectionMethod = 'getConnectedInitiatorsAsSQLParts';
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        parent::__construct ($aOptions, $oTemplate);
+        $this->_sDefaultSortingOrder = 'DESC';
+
+        $this->_oConnection = BxDolConnection::getObjectInstance($this->_sObjectConnections);
+        if (!$this->_oConnection)
+            return;
+
+        $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if (!$iProfileId)
+            return;
+
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if (!$oProfile)
+            return;
+
+        if ($oProfile->id() == bx_get_logged_profile_id())
+            $this->_bOwner = true;
+
+        $aSQLParts = $this->_oConnection->{$this->_sConnectionMethod}('p', 'id', $oProfile->id());
+
+        $this->addMarkers(array(
+            'profile_id' => $oProfile->id(),
+            'join_connections' => $aSQLParts['join']
+        ));
+    }
+
+    /**
+     * 'accept' action handler
+     */
+    public function performActionSubscribe()
+    {
+        list($iId, $iViewedId) = $this->_prepareIds();
+        if(!$iId)
+            return echoJson(array('msg' => _t('_sys_txt_error_occured')));
+
+        $a = $this->_oConnection->actionAdd($iId, $iViewedId);
+        if(!empty($a['err']))
+            return echoJson(array('msg' => $a['msg']));
+
+        return echoJson(array('grid' => $this->getCode(false), 'blink' => $iId));
+    }
+
+    protected function _getCellName ($mixedValue, $sKey, $aField, $aRow)
+    {
+        $oProfile = BxDolProfile::getInstance($aRow['id']);
+        if (!$oProfile)
+            return _t('_sys_txt_error_occured');
+
+        return parent::_getCellDefault ($oProfile->getUnit(), $sKey, $aField, $aRow);
+    }
+
+    protected function _getActionSubscribe ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        $iViewerId = bx_get_logged_profile_id();
+        if(!isLogged() || $iViewerId == $aRow['id'] || $this->_oConnection->isConnected($iViewerId, $aRow['id']))
+            return '';
+
+        return parent::_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _prepareIds ()
+    {
+        $iViewedId = bx_get_logged_profile_id();
+
+        $iId = 0;
+        $aIds = bx_get('ids');
+        if($aIds && is_array($aIds))
+            $mixedId = array_pop($aIds);
+
+        if(strpos($mixedId, ':') !== false) {
+            list($iId, $iViewedId) = explode (':', $mixedId);
+
+            $iId = (int)$iId;
+            $iViewedId = (int)$iViewedId;
+        }
+        else 
+            $iId = (int)$mixedId;
+
+        return array($iId, $iViewedId);
+    }
+}
+
+/** @} */

+ 40 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolGridSubscriptions.php

@@ -0,0 +1,40 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+
+class BxDolGridSubscriptions extends BxDolGridSubscribedMe
+{
+    protected $_sConnectionMethod = 'getConnectedContentAsSQLParts';
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        parent::__construct ($aOptions, $oTemplate);
+    }
+
+    protected function _delete ($mixedId)
+    {
+        list($iId, $iViewedId) = $this->_prepareIds();
+
+        if(!$this->_oConnection->isConnected($iViewedId, $iId))
+            return true;
+
+        return $this->_oConnection->removeConnection($iViewedId, $iId);
+    }
+
+    protected function _getActionDelete ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if (!isLogged() || !$this->_bOwner)
+            return '';
+
+        return parent::_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+}
+
+/** @} */

+ 120 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolInformer.php

@@ -0,0 +1,120 @@
+<?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_INFORMER_ALERT', 1);
+define('BX_INFORMER_INFO', 2);
+define('BX_INFORMER_ERROR', 3);
+
+/**
+ * Informer.
+ *
+ * It displays alerts or information messages in clearly visible area on the page to let user know important information.
+ *
+ * @section example Example of usage
+ *
+ * Adding message to informer:
+ *
+ * @code
+ *  $oInformer = BxDolInformer::getInstance(); // get object instance
+ *  if ($oInformer) // check if Informer is available for using
+ *      echo $oInformer->add ('my_id', 'Some important information here', BX_INFORMER_ALERT); // add an alert message
+ * @endcode
+ *
+ */
+class BxDolInformer extends BxDolFactory implements iBxDolSingleton
+{
+	protected $_bEnabled = true;
+    protected $_aMessages = array();
+
+    /**
+     * Constructor
+     */
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses']['BxDolInformer']))
+            trigger_error ('Multiple instances are not allowed for the BxDolInformer class.', E_USER_ERROR);
+
+        parent::__construct();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses']['BxDolInformer']))
+            trigger_error('Clone is not allowed for the BxDolInformer class.', E_USER_ERROR);
+    }
+
+    /**
+     * Get Informer object instance
+     * @return object instance or false on error
+     */
+    public static function getInstance($oTemplate = false)
+    {
+        if (isset($GLOBALS['bxDolClasses']['BxDolInformer']))
+            return $GLOBALS['bxDolClasses']['BxDolInformer'];
+
+        $o = new BxTemplInformer($oTemplate);
+
+        return ($GLOBALS['bxDolClasses']['BxDolInformer'] = $o);
+    }
+
+    public function setEnabled($bEnabled)
+    {
+    	$this->_bEnabled = $bEnabled;
+    }
+
+    /**
+     * Add message to informer.
+     * @param $sId - message id
+     * @param $sMsg - message text
+     * @param $iType - message type: BX_INFORMER_ALERT, BX_INFORMER_INFO or BX_INFORMER_ERROR
+     */
+    public function add ($sId, $sMsg, $iType = BX_INFORMER_INFO)
+    {
+        $this->_aMessages[$sId] = array (
+            'id' => $sId,
+            'msg' => $sMsg,
+            'type' => $iType,
+        );
+    }
+
+    /**
+     * Remove message from informer.
+     * @param $sId - message id
+     */
+    public function remove ($sId)
+    {
+        unset($this->_aMessages[$sId]);
+    }
+
+    /**
+     * Add permanent messages which are displayed on every page.
+     */
+    protected function _addPermanentMessages ()
+    {
+        // add account & profile related permament messages
+        if (isLogged()) {
+            $oAccount = BxDolAccount::getInstance();
+            if ($oAccount)
+                $oAccount->addInformerPermanentMessages($this);
+                
+            $oProfile = BxDolProfile::getInstance();
+            if ($oProfile)
+                $oProfile->addInformerPermanentMessages($this);
+        }
+
+        // add permament messages from modules
+        bx_alert('system', 'informer_permament_messages', 0, false, array('informer' => $this));
+    }
+
+}
+
+/** @} */

+ 126 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolKey.php

@@ -0,0 +1,126 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Key objects - automatically generate hashed keys.
+ *
+ * @section example Example of usage
+ *
+ * Generate new hashed key:
+ *
+ * @code
+ *  $oKeys = BxDolKey::getInstance(); // get object instance
+ *  if ($oKeys) // check if object is available for using
+ *      echo $oKeys->getNewKey (false, 3600); // get new hashed key, which will be automatically deleted after 1 hour
+ * @endcode
+ *
+ * Check if hashed key exists:
+ *
+ * @code
+ *  $oKeys = BxDolKey::getInstance(); // get object instance
+ *  if ($oKeys && $oKeys->isKeyExists ($sKey)) // check key exists
+ *      echo 'key exists';
+ *  else
+ *      echo 'key is invalid';
+ * @endcode
+ */
+class BxDolKey extends BxDolFactory implements iBxDolSingleton
+{
+    protected $_oQuery;
+
+    /**
+     * Constructor
+     */
+    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->_oQuery = new BxDolKeyQuery();
+    }
+
+    /**
+     * 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 BxDolKey();
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    /**
+     * Get new key.
+     * @param $aData - some data to associate with the key
+     * @param $iExpire - number of seconds to generated key after, by default - 1 week
+     * @return newly generated key string
+     */
+    public function getNewKey ($aData = false, $iExpire = 604800)
+    {
+        $sKey = md5(time() . rand() . BX_DOL_SECRET);
+        if ($this->_oQuery->insert($sKey, $aData ? serialize($aData) : '', (int)$iExpire));
+            return $sKey;
+        return false;
+    }
+
+    /**
+     * Check if provided key exists.
+     * @param $sKey - key string
+     * @return true if key exists or false if key is missing
+     */
+    public function isKeyExists ($sKey)
+    {
+        return $this->_oQuery->get($sKey) ? true : false;
+    }
+
+    /**
+     * Get key data.
+     * @param $sKey - key string
+     * @return true if key exists or false if key is missing
+     */
+    public function getKeyData ($sKey)
+    {
+        $sData = $this->_oQuery->getData($sKey);
+        if ($sData)
+            return unserialize($sData);
+        return '';
+    }
+
+    /**
+     * Delete provided key.
+     * @param $sKey - key string
+     * @return true if key was successfully found and delete if false otherwise
+     */
+    public function removeKey ($sKey)
+    {
+        return $this->_oQuery->remove($sKey);
+    }
+
+    /**
+     * Delete expired keys.
+     */
+    public function prune ()
+    {
+        return $this->_oQuery->prune();
+    }
+
+}
+
+/** @} */

+ 244 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolLiveUpdates.php

@@ -0,0 +1,244 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolLiveUpdates extends BxDolFactory implements iBxDolSingleton
+{
+	protected $_oQuery;
+    protected $_aSystems;
+    protected $_aSystemsActive;
+
+    protected $_iProfileId;
+
+    protected $_iInterval;
+
+	protected $_iCacheTTL;  
+	protected $_sCacheKey;
+
+    protected $_sJsClass;
+    protected $_sJsObject;
+
+    protected function __construct()
+    {
+        parent::__construct();
+
+        /**
+         * Note. Currently Live Updates are associated with profiles (Profile ID) and therefore they are used for logged in members only.
+         * If it's needed Session ID can be used instead of Profile ID. In this case Live Updates can be used for visitors too. Don't forget
+         * to update BxBaseLiveUpdates::init if Session ID will be used.
+         */
+        $this->_iProfileId = (int)bx_get_logged_profile_id();
+        if(!$this->_iProfileId)
+            return;
+
+        $this->_oQuery = new BxDolLiveUpdatesQuery();
+
+        $this->_iInterval = (int)$this->_oQuery->getParam('sys_live_updates_interval');
+
+        $this->_iCacheTTL = 86400;
+        $this->_sCacheKey = 'sys_live_updates_' . $this->_iProfileId;
+
+        $this->_sJsClass = 'BxDolLiveUpdates';
+    	$this->_sJsObject = 'oLiveUpdates';
+
+    	$this->_aSystems = $this->_getCachedSystems();
+    	$this->_aSystemsActive = array_keys($this->_aSystems);
+    }
+
+    /**
+     * get live updates instanse
+     * @return ready to use class instance
+     */
+    public static function getInstance()
+    {
+		if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+			$GLOBALS['bxDolClasses'][__CLASS__] = new BxTemplLiveUpdates();
+
+		return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    /**
+     * 
+     * Is called via Ajax Requests only. 
+     */
+    public function perform()
+    {
+		$iIndex = (int)bx_get('index');
+
+		$mixedSystemsActive = bx_get('systems_active');
+		if($mixedSystemsActive !== false)
+			$this->_aSystemsActive = $mixedSystemsActive;
+
+		$this->_aSystems = $this->_getCachedSystems();
+    	if(empty($this->_aSystems) || !is_array($this->_aSystems))
+    		return array();
+
+		$aCached = $this->_getCachedData();
+		$aRequested = $this->_getRequestedData($iIndex, true, $aCached);
+
+		$aResult = array();
+		$bUpdateCache = false;
+		foreach($aRequested as $sName => $aData) {
+			$bCached = isset($aCached[$sName]); 
+			if($bCached && (int)$aCached[$sName] == (int)$aData['count'])
+				continue;
+
+			if($bCached) {
+				$aResultData = array('count_new' => $aData['count'], 'count_old' => $aCached[$sName]);
+				if(isset($aData['data']))
+					$aResultData = array_merge($aResultData, $aData['data']);
+	
+				$aResult[] = array(
+					'data' => $aResultData,
+					'method' => $aData['method']
+				);
+			}
+
+			$aCached[$sName] = $aData['count'];
+			$bUpdateCache = true;
+		}
+
+		if($bUpdateCache)
+			$this->_updateCached('data', $aCached);
+
+    	return $aResult;
+    }
+
+    /**
+     * 
+     * Add transient live update for current user on this current page. 
+     * @param string $sName - unique name.
+     * @param integer $iFrequency - call frequency.
+     * @param string $sServiceCall - serialized service call.
+     * @param boolean $bActive - add active/not active live update.
+     */
+    public function add($sName, $iFrequency, $sServiceCall, $bActive = true)
+    {
+        if(!$this->_iProfileId)
+            return false;
+
+    	if(!in_array($sName, $this->_aSystemsActive))
+    		$this->_aSystemsActive[] = $sName;
+
+    	if(empty($this->_aSystems[$sName])) {
+	    	$this->_aSystems[$sName] = array(
+	    		'name' => $sName,
+	    		'frequency' => $iFrequency,
+	    		'service_call' => $sServiceCall,
+	    		'active' => $bActive ? 1 : 0
+	    	);
+
+	    	$this->_updateCached('systems', $this->_aSystems);
+    	}
+
+    	$mixedResponce = $this->_getRequestedDataBySystem($this->_aSystems[$sName]);
+    	if($mixedResponce !== false) {
+    		$aCachedData = $this->_getCachedData();
+    		$aCachedData[$sName] = $mixedResponce['count'];
+    		$this->_updateCached('data', $aCachedData);
+    	}
+
+    	return true;
+    }
+
+    protected function _getCacheInfo()
+    {
+    	return array(
+    		$this->_oQuery->getDbCacheObject(),
+    		$this->_oQuery->genDbCacheKey($this->_sCacheKey),
+    		$this->_iCacheTTL
+    	);
+    }
+
+    protected function _getCached($sType)
+    {
+    	list($oCache, $sCacheKey, $iCacheTtl) = $this->_getCacheInfo();
+
+    	$aCached = $oCache->getData($sCacheKey, $iCacheTtl);
+    	if(empty($aCached[$sType])) {
+    		switch($sType) {
+    			case 'systems':
+    				$aCached[$sType] = $this->_oQuery->getSystems();
+    				break;
+
+    			case 'data':
+    				if(!isset($aCached[$sType]))
+    					$aCached[$sType] = array();
+
+    				$aRequested = $this->_getRequestedData();
+    				foreach($this->_aSystems as $sName => $aSystem)
+    					$aCached[$sType][$sName] = !empty($aRequested[$sName]['count']) ? (int)$aRequested[$sName]['count'] : 0;
+    				break;
+    		}
+
+    		if(!empty($aCached[$sType]))
+    			$oCache->setData($sCacheKey, $aCached, $iCacheTtl);
+    	}
+
+    	return $aCached[$sType];
+    }
+
+	protected function _getCachedSystems()
+    {
+    	$aSystems = $this->_getCached('systems');
+    	if(!empty($aSystems) && !empty($this->_aSystemsActive))
+    		$aSystems = array_intersect_key($aSystems, array_flip($this->_aSystemsActive));
+
+    	return $aSystems;
+    }
+
+    protected function _getCachedData()
+    {
+    	return $this->_getCached('data');
+    }
+
+	protected function _updateCached($sKey, $aData)
+    {
+    	list($oCache, $sCacheKey, $iCacheTtl) = $this->_getCacheInfo();
+
+    	$aCached = $oCache->getData($sCacheKey, $iCacheTtl);
+    	$aCached[$sKey] = $aData;
+
+    	$oCache->setData($sCacheKey, $aCached, $iCacheTtl);
+    }
+
+    protected function _getRequestedData($iIndex = 0, $bIndexCheck = false, $aCachedData = array())
+    {
+    	$aResult = array();
+
+    	foreach($this->_aSystems as $sName => $aSystem) {
+    		if(empty($aSystem) || !is_array($aSystem) || (int)$aSystem['active'] != 1)
+    			continue;
+
+			if($bIndexCheck && $iIndex % (int)$aSystem['frequency'] != 0)
+				continue;
+
+			$mixedResponce = $this->_getRequestedDataBySystem($aSystem, (!empty($aCachedData) && isset($aCachedData[$sName]) ? (int)$aCachedData[$sName] : 0));
+			if($mixedResponce === false)
+				continue;
+
+			$aResult[$sName] = $mixedResponce;
+    	}
+
+    	return $aResult;
+    }
+
+    protected function _getRequestedDataBySystem($aSystem, $iCachedData = 0)
+    {
+		if(!BxDolService::isSerializedService($aSystem['service_call']))
+			return false;
+
+		$aResponce = BxDolService::callSerialized($aSystem['service_call'], array('count' => (int)$iCachedData));
+		if(empty($aResponce) || !is_array($aResponce) || !isset($aResponce['count'], $aResponce['method']))
+			return false;
+
+		return $aResponce;
+    }
+}
+
+/** @} */

+ 296 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolMenu.php

@@ -0,0 +1,296 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Menus.
+ *
+ * Menu uses some set of items and the template to display it,
+ * so it is possible to have several menus which uses the same set of items but different templates.
+ *
+ * Menu is any set of some links or actions, for example menu can be links in site's footer or actions in profile view.
+ *
+ *
+ * @section menu_create Creating the Menu object:
+ *
+ * 1. Add record to 'sys_objects_menu' table:
+ *
+ * - object: name of the menu object, in the format: vendor prefix, underscore, module prefix, underscore, internal identifier or nothing; for example: bx_groups_actions - actions menu in group view.
+ * - title: name of the menu, displayed in the studio menu builder.
+ * - set_name: name of items' set.
+ * - module: the module this menu belongs to.
+ * - template_id: the template to use for menu displaying, this is id from 'sys_menu_templates' table.
+ * - deletable: it determines if menu can be deleted from the studio menu builder.
+ * - active: it is possible to disable particular menu, then it will not be displayed.
+ * - override_class_name: user defined class name which is derived from BxTemplMenu.
+ * - override_class_file: the location of the user defined class, leave it empty if class is located in system folders.
+ *
+ * Menu templates are stored in 'sys_menu_templates' table:
+ * - id: template id.
+ * - template: template file.
+ * - title: template title to display in the studio menu builder.
+ * All menu templates iterate through 'bx_repeat:menu_items' and use the following template variables for each menu item: __link__, __target__, __onclick__, __title__, __class_add__.
+ *
+ *
+ * 2. Add menu an empty menu set to 'sys_menu_sets' table (if you want to use new set of items for created menu):
+ * - set_name: the set name.
+ * - module: the module this set belongs to.
+ * - title: name of the set, displayed in studio menu builder.
+ * - deletable: it determines if the set can be deleted from menu builder.
+ *
+ *
+ * 3. Add menu items to the set by adding records to 'sys_menu_items' table:
+ * - set_name: the set name this item belogs to.
+ * - module: the module this item belongs to.
+ * - name: name of the item (not displayed to the end user), unique in the particular set.
+ * - title: menu item title to display to the end user, please note that some templates can still display menu as icons without text titles.
+ * - link: menu item URL.
+ * - onclick: menu item onclick event.
+ * - target: menu item target.
+ * - icon: menu item icon, please note that some templates can still display menu as text without icons.
+ * - addon: display additional data near menu item, only for supported menu templates, this is serialized array of service call parameters: module - module name, method - service method name, params - array of parameters.
+ * - visible_for_levels: bit field with set of member level ids. To use member level id in bit field - the level id minus 1 as power of 2 is used, for example:
+ *      - user level id = 1 -> 2^(1-1) = 1
+ *      - user level id = 2 -> 2^(2-1) = 2
+ *      - user level id = 3 -> 2^(3-1) = 4
+ *      - user level id = 4 -> 2^(4-1) = 8
+ * - active: it is possible to disable particular menu item, then it will not be displayed.
+ * - order: menu item order in the particular set.
+ *
+ *
+ * 4. Display Menu.
+ * Use the following sample code to display menu:
+ * @code
+ *     $oMenu = BxTemplMenu::getObjectInstance('sample_menu'); // 'sample_menu' is 'object' field from 'sys_objects_menu' table.
+ *     if ($oMenu)
+ *         echo $oMenu->getCode; // display menu
+ * @endcode
+ *
+ * But in most cases you don't need to use above code to display menu,
+ * menu objects are integrated into pages - there is special 'menu' page block type for it.
+ *
+ */
+class BxDolMenu extends BxDolFactory implements iBxDolFactoryObject, iBxDolReplaceable
+{
+    protected static $SEL_MODULE = '';
+    protected static $SEL_NAME = '';
+
+    protected $_bDynamicMode;
+
+    protected $_sSelModule;
+    protected $_sSelName;
+
+    protected $_sObject;
+    protected $_aObject;
+    protected $_oQuery;
+    protected $_oPermalinks;
+    protected $_aMarkers = array ();
+
+    /**
+     * Constructor
+     * @param $aObject array of menu options
+     */
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+
+        $this->_bDynamicMode = false;
+
+        $this->_sObject = isset($aObject['object']) ? $aObject['object'] : 'bx-menu-obj-' . time() . rand(0, PHP_INT_MAX);
+        $this->_aObject = $aObject;
+        $this->_oQuery = new BxDolMenuQuery($this->_aObject);
+        $this->_oPermalinks = BxDolPermalinks::getInstance();
+
+        if (isLogged()) {
+            $oProfile = BxDolProfile::getInstance();
+            if ($oProfile) {
+                $this->addMarkers(array(
+                    'member_id' => $oProfile->id(),
+                    'member_display_name' => $oProfile->getDisplayName(),
+                    'member_url' => $oProfile->getUrl(),
+                ));
+            }
+        }
+    }
+
+    /**
+     * Get menu object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    static public function getObjectInstance($sObject, $oTemplate = false)
+    {
+        if (isset($GLOBALS['bxDolClasses']['BxDolMenu!'.$sObject]))
+            return $GLOBALS['bxDolClasses']['BxDolMenu!'.$sObject];
+
+        $aObject = BxDolMenuQuery::getMenuObject($sObject);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        $sClass = 'BxTemplMenu';
+        if (!empty($aObject['override_class_name'])) {
+            $sClass = $aObject['override_class_name'];
+            if (!empty($aObject['override_class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aObject['override_class_file']);
+        }
+
+        $o = new $sClass($aObject, $oTemplate);
+
+        return ($GLOBALS['bxDolClasses']['BxDolMenu!'.$sObject] = $o);
+    }
+
+    /**
+     * Set selected menu item globally.
+     * @param $sModule menu item module to set as selected
+     * @param $sName menu item name to set as selected
+     */
+    static public function setSelectedGlobal ($sModule, $sName)
+    {
+        self::$SEL_MODULE = $sModule;
+        self::$SEL_NAME = $sName;
+    }
+
+    /**
+     * Process menu triggers.
+     * Menu triggers allow to automatically add menu items to modules with no different if dependant module was install before or after the module menu item belongs to.
+     * For example module "Notes" adds menu items to all profiles modules (Persons, Organizations, etc)
+     * with no difference if persons module was installed before or after "Notes" module was installed.
+     * @param $sMenuTriggerName trigger name to process, usually specified in module installer class - @see BxBaseModGeneralInstaller
+     * @return always true, always success
+     */
+    static public function processMenuTrigger ($sMenuTriggerName)
+    {
+        // get list of active modules
+        $aModules = BxDolModuleQuery::getInstance()->getModulesBy(array(
+            'type' => 'modules',
+            'active' => 1,
+        ));
+
+        // get list of menu triggers
+        $aMenuItems = BxDolMenuQuery::getMenuTriggers($sMenuTriggerName);
+
+        // check each menu item trigger for all modules
+        foreach ($aMenuItems as $aMenuItem) {
+            foreach ($aModules as $aModule) {
+                if (!BxDolRequest::serviceExists($aModule['name'], 'get_menu_set_name_for_menu_trigger'))
+                    continue;
+
+                if (!($sMenuSet = BxDolService::call($aModule['name'], 'get_menu_set_name_for_menu_trigger', array($sMenuTriggerName))))
+                    continue;
+
+                $aMenuItem['set_name'] = $sMenuSet;
+                BxDolMenuQuery::addMenuItemToSet($aMenuItem);
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Check if the menu is visible. The menu is visible if at least one menu item is visible.
+     * @return boolean
+     */
+    public function isVisible()
+    {
+        if((int)$this->_aObject['active'] == 0)
+            return false;
+
+    	if(!isset($this->_aObject['menu_items']))
+			$this->_aObject['menu_items'] = $this->_oQuery->getMenuItems();
+
+    	$bVisible = false;
+    	foreach ($this->_aObject['menu_items'] as $a) {
+    		if((isset($a['active']) && !$a['active']) || (isset($a['visible_for_levels']) && !$this->_isVisible($a)))
+				continue;
+			
+			$bVisible = true;
+			break;
+    	}
+
+    	return $bVisible;
+    }
+
+    /**
+     * Set selected menu item for current menu object only.
+     * @param $sModule menu item module to set as selected
+     * @param $sName menu item name to set as selected
+     */
+    public function setSelected ($sModule, $sName)
+    {
+        $this->_sSelModule = $sModule;
+        $this->_sSelName = $sName;
+    }
+
+    public function setDynamicMode ($bDynamicMode)
+    {
+        $this->_bDynamicMode = $bDynamicMode;
+    }
+
+    /**
+     * Add replace markers.
+     * @param $a array of markers as key => value
+     * @return true on success or false on error
+     */
+    public function addMarkers ($a)
+    {
+        if (empty($a) || !is_array($a))
+            return false;
+        $this->_aMarkers = array_merge ($this->_aMarkers, $a);
+        return true;
+    }
+
+    /**
+     * Remove marker
+     * @param $s marker key
+     */
+    public function removeMarker ($s) 
+    {
+        unset($this->_aMarkers[$s]);
+    }
+
+    /**
+     * Check if menu items is selected.
+     * @param $a menu item array
+     * @return boolean
+     */
+    protected function _isSelected ($a)
+    {
+        if ($this->_sSelModule || $this->_sSelName)
+            return (!isset($a['module']) || $a['module'] == $this->_sSelModule) && (isset($a['name']) && $a['name'] == $this->_sSelName) ? true : false;
+        return (!isset($a['module']) || $a['module'] == self::$SEL_MODULE) && (isset($a['name']) && $a['name'] == self::$SEL_NAME) ? true : false;
+    }
+
+    /**
+     * Check if menu items is visible.
+     * @param $a menu item array
+     * @return boolean
+     */
+    protected function _isVisible ($a)
+    {
+        return BxDolAcl::getInstance()->isMemberLevelInSet($a['visible_for_levels']);
+    }
+
+    /**
+     * Replace provided markers in menu item array, curently markers are replaced in title, link and onclick fields.
+     * @param $a menu item array
+     * @return array where markes are replaced with real values
+     */
+    protected function _replaceMarkers ($a)
+    {
+        if (empty($this->_aMarkers))
+            return $a;
+        $aReplacebleFields = array ('title', 'link', 'onclick');
+        foreach ($aReplacebleFields as $sField)
+            if (isset($a[$sField]))
+                $a[$sField] = bx_replace_markers($a[$sField], $this->_aMarkers);
+        return $a;
+    }
+
+}
+
+/** @} */

+ 592 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolMetatags.php

@@ -0,0 +1,592 @@
+<?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);
+
+// TODO: client side controls: some js to help to enter \@mentions and \#keywords(optional)
+// TODO: integrate to comments, try to integrate to one metatags object - maybe specify metatags object in comments object and make appropriate changes in comments, 
+//       comments content should be treated as main conetent and main content should be shown in search results
+// 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::metaAdd to add location (and all other, including content image) meta information to the page header:
+ * @code
+ *   $o = BxDolMetatags::getObjectInstance('object_name');
+ *   $o->metaAdd($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::metaAdd 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 metaAdd($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;
+    }
+
+
+    /**
+     * 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) 
+    {
+        $a = array();
+        if (!preg_match_all('/[^&](\#[\pL\pN_]+)/u', ' ' . strip_tags($s), $a)) {
+            $this->_oQuery->keywordsDelete($iId);
+            return 0;
+        }
+
+        $aTags = array_unique($a[1]);
+        $aTags = array_slice($aTags, 0, BX_METATAGS_KEYWORDS_MAX);
+
+        return $this->_oQuery->keywordsAdd($iId, $aTags);
+    }
+
+    /**
+     * 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 $s string with \#keywords
+     * @return number of found keywords
+     */
+    public function keywordsAddAuto($iId, $aContentInfo, $CNF, $sFormDisplay) 
+    {
+        $aFields = $this->keywordsFields($aContentInfo, $CNF, $sFormDisplay); 
+        $sTextWithKeywords = '';
+        foreach ($aFields as $sField)
+            $sTextWithKeywords .= $aContentInfo[$sField];
+
+        return $this->keywordsAdd($iId, $sTextWithKeywords);
+    }
+
+    /**
+     * Get field names which are subject to parse keywords
+     */
+    public function keywordsFields($aContentInfo, $CNF, $sFormDisplay)
+    {
+        $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'])
+                    $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)
+    {
+        $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 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);
+    }
+
+    /**
+     * 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;
+
+        if ($sPrefix)
+            $sPrefix .= '_';
+        if (!$oForm)
+            $oForm = new BxDolForm(array(), false);
+        $this->locationsAdd($iId, $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'));
+    }
+
+    /**
+     * Get locations string with links
+     * @param $iId content id
+     * @return string with links to country and city
+     */
+    public function locationsString($iId, $bHTML = true)
+    {
+        bx_import('BxDolForm');
+        $aCountries = BxDolFormQuery::getDataItems('Country');
+        $aLocation = $this->locationGet($iId);
+        if(!$aLocation || !$aLocation['country'] || !isset($aCountries[$aLocation['country']]))
+            return '';
+
+        $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']))
+            return _t('_sys_location_country', $sCountryUrl);
+
+        $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']))
+        	$s = _t('_sys_location_country_city', $sCountryUrl, $sStateUrl, $sCityUrl);
+        else
+            $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 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) 
+    {
+        // TODO:        
+    }
+
+    /**
+     * 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) 
+    {
+        // TODO:
+    }
+
+    /**
+     * 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, $sMention)
+    {
+        // TODO:
+    }
+
+    /**
+     * 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;
+    }
+
+}
+
+/** @} */

+ 102 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolModuleTemplate.php

@@ -0,0 +1,102 @@
+<?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 BxDolModuleTemplate extends BxDolTemplate
+{
+    protected $_oDb;
+    protected $_oConfig;
+    protected $_bObStarted = 0;
+
+    /*
+     * Constructor.
+     */
+    public function __construct(&$oConfig, &$oDb, $sRootPath = BX_DIRECTORY_PATH_ROOT, $sRootUrl = BX_DOL_URL_ROOT)
+    {
+        parent::__construct($sRootPath, $sRootUrl);
+
+        $this->_oDb = &$oDb;
+        $this->_oConfig = &$oConfig;
+
+        $sName = $oConfig->getName();
+        $sHomePath = $oConfig->getHomePath();
+        $sHomeUrl = $oConfig->getHomeUrl();
+
+        $this->addLocation($sName, $sHomePath, $sHomeUrl);
+        $this->addLocationJs($sName, $sHomePath . 'js/', $sHomeUrl . 'js/');
+    }
+
+    function addCss($mixedFiles, $bDynamic = false)
+    {
+        return $this->_addFiles(BxDolTemplate::getInstance(), 'addCss', 'addDynamicLocation', 'removeLocation', '', $mixedFiles, $bDynamic, true);
+    }
+
+    function addJs($mixedFiles, $bDynamic = false)
+    {
+        return $this->_addFiles(BxDolTemplate::getInstance(), 'addJs', 'addDynamicLocationJs', 'removeLocationJs', 'js/', $mixedFiles, $bDynamic, true);
+    }
+
+    function addJsTranslation($mixedKey)
+    {
+        BxDolTemplate::getInstance()->addJsTranslation($mixedKey);
+    }
+
+    function addStudioCss($mixedFiles, $bDynamic = false, $bSearchInModule = true)
+    {
+        return $this->_addFiles(BxDolStudioTemplate::getInstance(), 'addCss', 'addDynamicLocation', 'removeLocation', '', $mixedFiles, $bDynamic, $bSearchInModule);
+    }
+
+    function addStudioJs($mixedFiles, $bDynamic = false, $bSearchInModule = true)
+    {
+        return $this->_addFiles(BxDolStudioTemplate::getInstance(), 'addJs', 'addDynamicLocationJs', 'removeLocationJs', 'js/', $mixedFiles, $bDynamic, $bSearchInModule);
+    }
+
+    function _addFiles($oTemplate, $sFuncAddFiles, $sFuncAddLocation, $sFuncRemoveLocation, $sPath, $mixedFiles, $bDynamic = false, $bSearchInModule = true)
+    {
+        $sLocationKey = $bSearchInModule ? $oTemplate->$sFuncAddLocation($this->_oConfig->getHomePath() . $sPath, $this->_oConfig->getHomeUrl() . $sPath) : '';
+        $mixedResult = $oTemplate->$sFuncAddFiles($mixedFiles, $bDynamic);
+        if($sLocationKey != '')
+            $oTemplate->$sFuncRemoveLocation($sLocationKey);
+
+        return $mixedResult;
+    }
+
+    function addStudioInjection($sKey, $sType, $sData, $iReplace = 0)
+    {
+        BxDolStudioTemplate::getInstance()->aPage['injections']['page_0'][$sKey][] = array(
+            'page_index' => 0,
+            'key' => $sKey,
+            'type' => $sType,
+            'data' => $sData,
+            'replace' => $iReplace
+        );
+    }
+
+    function pageStart ()
+    {
+        if (0 == $this->_bObStarted)  {
+            ob_start ();
+            $this->_bObStarted = 1;
+        }
+    }
+
+    function pageEnd ($isGetContent = true)
+    {
+        if (1 == $this->_bObStarted)  {
+            $sRet = '';
+            if ($isGetContent)
+                $sRet = ob_get_clean();
+            else
+                ob_end_clean();
+            $this->_bObStarted = 0;
+            return $sRet;
+        }
+    }
+}
+
+/** @} */

+ 259 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolPage.php

@@ -0,0 +1,259 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Pages.
+ *
+ * It allows to display pages which are built from studio.
+ *
+ * The new system has the following page features:
+ * - Layouts: page can have any structure, not just columns.
+ * - SEO: page can have own SEO options, like meta tags and meta keywords, as well as instructions for search bots.
+ * - Cache: page can be cached on the server.
+ * - Access control: page access can be controlled using member levels.
+ *
+ *
+ *
+ * @section page_create Creating the Page object:
+ *
+ * 1. add record to 'sys_objects_page' table:
+ *
+ * - object: name of the page object, in the format: vendor prefix, underscore, module prefix, underscore, internal identifier or nothing; for example: bx_profiles_view - profile view page.
+ * - title: name to display as page title.
+ * - module: the module this page belongs to.
+ * - layout_id: page layout to use, this is id of the record from 'sys_pages_layouts' table.
+ * - visible_for_levels: bit field with set of member level ids. To use member level id in bit field - the level id minus 1 as power of 2 is used, for example:
+ *      - user level id = 1 -> 2^(1-1) = 1
+ *      - user level id = 2 -> 2^(2-1) = 2
+ *      - user level id = 3 -> 2^(3-1) = 4
+ *      - user level id = 4 -> 2^(4-1) = 8
+ * - visible_for_levels_editable: it determines if 'visible_for_levels' field is editable from page builder, visibility options can be overriden by custom class and shouldn't be editable in this case.
+ * - url: the page url, if it is static page.
+ * - meta_description: meta description of the page.
+ * - meta_keywords: meta keywords of the page.
+ * - meta_robots: instructions for search bots.
+ * - cache_lifetime: number of seconds to store cache for.
+ * - cache_editable: it determines if cache can be edited from page builder.
+ * - deletable: it determines if page can be deleted from page builder.
+ * - override_class_name: user defined class name which is derived from BxTemplPage.
+ * - override_class_file: the location of the user defined class, leave it empty if class is located in system folders.
+ *
+ * Page can select appropriate menu automatically if 'module' and 'object' fields in 'sys_objects_page' table are matched with 'module' and 'name' fields in 'sys_menu_items' table.
+ *
+ *
+ * Page layout are stored in 'sys_pages_layouts' table:
+ * - name: inner unique layout name.
+ * - icon: layout icon to display in page builder, it should represent basic view of the layout to help studio operator determine the layout structure.
+ * - title: layout name to display in page builder.
+ * - template: template name to use to display page with certain layout.
+ * - cells_number: number of areas in the layout, page blocks can be places into this areas(cells).
+ * To define areas in the layout they should be named as '__cell_N__', where N is cell number, starting from 1.
+ *
+ *
+ * 2. Add page blocks to 'sys_pages_blocks' table:
+ * - object: page object name this block belongs to.
+ * - cell_id: cell number in page layout to place block to.
+ * - module: module name this block belongs to.
+ * - title: block title.
+ * - designbox_id: design box to use to diplay page block, it is id of the record from 'sys_pages_design_boxes' table.
+ * - visible_for_levels: bit field with set of member level ids. To use member level id in bit field - the level id minus 1 as power of 2 is used, for example:
+ *      - user level id = 1 -> 2^(1-1) = 1
+ *      - user level id = 2 -> 2^(2-1) = 2
+ *      - user level id = 3 -> 2^(3-1) = 4
+ *      - user level id = 4 -> 2^(4-1) = 8
+ * - type: block type
+ *      - raw: HTML block, displayed in page builder as HTML textarea.
+ *      - html: HTML block, displayed in page builder as visual editor, like TinyMCE.
+ *      - lang: translatable language string, displayed in page builder as editable language key.
+ *      - image: just an image, displayed in page builder as HTML upload form.
+ *      - rss: RSS block, displayed in page builder as editable URL to RSS resource, along with number of displayed items.
+ *      - menu: menu block, displayed as menu selector.
+ *      - service: to display block content, the provided service method is used.
+ * - content: depending on 'type' field:
+ *      - raw: HTML string.
+ *      - html: HTML string.
+ *      - lang: language key.
+ *      - image: image id in the storage and alignment (left, center, right) for example: 36\#center
+ *      - rss: URL to RSS with number of displayed items, for example: http://www.example.com/rss#4
+ *      - menu: menu object name.
+ *      - service: serialized array of service call parameters: module - module name, method - service method name, params - array of parameters.
+ * - deletable: is block deletable from page builder
+ * - copyable: is block can be copied to any other page from page builder.
+ * - order: block order in particular cell.
+ *
+ * Block design boxes are stored in 'sys_pages_design_boxes' table:
+ * - id: consistent id, there are the following defines can be used in the code for each system block style:
+ *      - 0 - BX_DB_CONTENT_ONLY: design box with content only - no borders, no background, no caption.
+ *      - 1 - BX_DB_DEF: default design box with content, borders and caption.
+ *      - 2 - BX_DB_EMPTY: just empty design box, without anything.
+ *      - 3 - BX_DB_NO_CAPTION: design box with content, like BX_DB_DEF but without caption.
+ *      - 10 - BX_DB_PADDING_CONTENT_ONLY: design box with content only wrapped with default padding - no borders, no background, no caption; it can be used to just wrap content with default padding.
+ *      - 11 - BX_DB_PADDING_DEF: default design box with content wrapped with default padding, borders and caption.
+ *      - 13 - BX_DB_PADDING_NO_CAPTION: design box with content wrapped with default padding, like BX_DB_DEF but without caption.
+ * - title: block name which is displayed in studio, describes block styles.
+ * - template: template name to use to display page block.
+ *
+ *
+ * 3. Display Page.
+ * Use the following sample code to display page:
+ * @code
+ *     $oPage = BxDolPage::getObjectInstance('sample'); // 'sample' is 'object' field from 'sys_objects_page' table, it automatically creates instance of default or custom class by object name
+ *     if ($oPage)
+ *         echo $oPage->getCode(); // print page
+ * @endcode
+ *
+ */
+class BxDolPage extends BxDolFactory implements iBxDolFactoryObject, iBxDolReplaceable
+{
+    protected $_sObject;
+    protected $_aObject;
+    protected $_oQuery;
+    protected $_aMarkers = array ();
+
+    /**
+     * Constructor
+     * @param $aObject array of page options
+     */
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+
+        $this->_sObject = $aObject['object'];
+        $this->_aObject = $aObject;
+        $this->_oQuery = new BxDolPageQuery($this->_aObject);
+    }
+
+    /**
+     * Get page object instance by page URI
+     * @param $sURI unique page URI
+     * @return object instance or false on error
+     */
+    static public function getObjectInstanceByURI($sURI = '')
+    {
+    	if(empty($sURI) && bx_get('i') !== false)
+    		$sURI = bx_process_input(bx_get('i'));
+
+    	if(empty($sURI))
+			return false;
+
+        $sObject = BxDolPageQuery::getPageObjectNameByURI($sURI);
+        return $sObject ? self::getObjectInstance($sObject) : false;
+    }
+
+    /**
+     * Get page 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']['BxDolPage!'.$sObject]))
+            return $GLOBALS['bxDolClasses']['BxDolPage!'.$sObject];
+
+        $aObject = BxDolPageQuery::getPageObject($sObject);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        $sClass = 'BxTemplPage';
+        if (!empty($aObject['override_class_name'])) {
+            $sClass = $aObject['override_class_name'];
+            if (!empty($aObject['override_class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aObject['override_class_file']);
+        }
+
+        $o = new $sClass($aObject);
+
+        return ($GLOBALS['bxDolClasses']['BxDolPage!'.$sObject] = $o);
+    }
+
+	/**
+     * Process page triggers.
+     * Page triggers allow to automatically add page blocks to modules with no different if dependant module was install before or after the module page block belongs to.
+     * For example module "Notes" adds page blocks to all profiles modules (Persons, Organizations, etc)
+     * with no difference if persons module was installed before or after "Notes" module was installed.
+     * @param $sPageTriggerName trigger name to process, usually specified in module installer class - @see BxBaseModGeneralInstaller
+     * @return always true, always success
+     */
+    static public function processPageTrigger ($sPageTriggerName)
+    {
+        // get list of active modules
+        $aModules = BxDolModuleQuery::getInstance()->getModulesBy(array(
+            'type' => 'modules',
+            'active' => 1,
+        ));
+
+        // get list of page block triggers
+        $aPageBlocks = BxDolPageQuery::getPageTriggers($sPageTriggerName);
+
+        // check each page block trigger for all modules
+        foreach ($aPageBlocks as $aPageBlock) {
+            foreach ($aModules as $aModule) {
+                if (!BxDolRequest::serviceExists($aModule['name'], 'get_page_object_for_page_trigger'))
+                    continue;
+
+				$mixedPageObject = BxDolService::call($aModule['name'], 'get_page_object_for_page_trigger', array($sPageTriggerName));
+                if (!$mixedPageObject)
+                    continue;
+
+                $aPageBlockRow = $aPageBlock;
+
+                if (is_array($mixedPageObject))
+                    $aPageBlockRow = array_merge($aPageBlockRow, $mixedPageObject);
+                else
+                    $aPageBlockRow['object'] = $mixedPageObject;
+                
+                BxDolPageQuery::addPageBlockToPage($aPageBlockRow);
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Add replace markers. Markers are replaced in raw, html, lang blocks and page title, description, keywords and block titles.
+     * @param $a array of markers as key => value
+     * @return true on success or false on error
+     */
+    public function addMarkers ($a)
+    {
+        if (empty($a) || !is_array($a))
+            return false;
+        $this->_aMarkers = array_merge ($this->_aMarkers, $a);
+        return true;
+    }
+
+    /**
+     * 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);
+    }
+
+    /**
+     * Check if page block is visible.
+     */
+    protected function _isVisibleBlock ($a)
+    {
+        return BxDolAcl::getInstance()->isMemberLevelInSet($a['visible_for_levels']);
+    }
+
+    /**
+     * Check if page is visible.
+     */
+    protected function _isVisiblePage ($a)
+    {
+        return BxDolAcl::getInstance()->isMemberLevelInSet($a['visible_for_levels']);
+    }
+
+}
+
+/** @} */

+ 302 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolPayments.php

@@ -0,0 +1,302 @@
+<?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 BxDolPayments extends BxDolFactory implements iBxDolSingleton
+{
+	protected $_oDb;
+
+	protected $_aObjects;
+	protected $_sActive;
+
+    protected function __construct()
+    {
+        parent::__construct();
+
+        $this->_oDb = new BxDolPaymentsQuery();
+
+        $this->_aObjects = $this->_oDb->getObjects();
+        $this->_sActive = getParam('sys_default_payment');
+    }
+
+	static public function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses']['BxDolPayments']))
+        	$GLOBALS['bxDolClasses']['BxDolPayments'] = new BxDolPayments();
+
+		return $GLOBALS['bxDolClasses']['BxDolPayments'];
+    }
+
+	public function setActive($sActive)
+    {
+		$this->_sActive = $sActive;
+    }
+
+    public function getActive()
+    {
+    	return $this->_sActive;
+    }
+
+    public function isActive()
+    {
+    	if(empty($this->_sActive))
+    		return false;
+
+    	if(!BxDolModuleQuery::getInstance()->isModuleByName($this->_sActive))
+    		return false;
+
+    	return true;
+    }
+
+	public function getDetailsUrl()
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_details_url', 'Details'))
+    		return '';
+
+    	return BxDolService::call($this->_sActive, 'get_details_url', array(), 'Details');
+    }
+
+    public function isAcceptingPayments($iVendorId, $sPaymentType = '')
+    {
+    	if(empty($this->_sActive) || !BxDolRequest::serviceExists($this->_sActive, 'is_accepting_payments'))
+    		return false;
+
+    	$aSrvParams = array($iVendorId, $sPaymentType);
+        return BxDolService::call($this->_sActive, 'is_accepting_payments', $aSrvParams);
+    }
+
+	public function isPaymentProvider($iVendorId, $sVendorProvider, $sPaymentType = '')
+    {
+    	if(empty($this->_sActive) || !BxDolRequest::serviceExists($this->_sActive, 'is_payment_provider'))
+    		return false;
+
+    	$aSrvParams = array($iVendorId, $sVendorProvider, $sPaymentType);
+        return BxDolService::call($this->_sActive, 'is_payment_provider', $aSrvParams);
+    }
+
+	public function getPaymentProvider($iVendorId, $sVendorProvider, $sPaymentType = '')
+    {
+    	if(empty($this->_sActive) || !BxDolRequest::serviceExists($this->_sActive, 'get_payment_provider'))
+    		return false;
+
+    	$aSrvParams = array($iVendorId, $sVendorProvider, $sPaymentType);
+        return BxDolService::call($this->_sActive, 'get_payment_provider', $aSrvParams);
+    }
+
+	public function getPayments()
+    {
+        $aPayments = array(
+			'' => _t('_Select_one')
+        );
+		foreach($this->_aObjects as $aObject) {
+			if(empty($aObject) || !is_array($aObject))
+				continue;
+
+			$aPayments[$aObject['object']] = _t($aObject['title']);
+		}
+
+        return $aPayments;
+    }
+
+    public function updateDependentModules($sModule = 'all', $bInstall = true)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'update_dependent_modules'))
+    		return;
+
+		BxDolService::call($this->_sActive, 'update_dependent_modules', array($sModule, $bInstall));
+    }
+
+    public function getProvidersCart($iVendorId)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_providers_cart'))
+    		return array();
+
+    	$aSrvParams = array($iVendorId);
+        return BxDolService::call($this->_sActive, 'get_providers_cart', $aSrvParams);
+    }
+
+	public function getOption($sOption)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_option'))
+    		return '';
+
+    	return BxDolService::call($this->_sActive, 'get_option', array($sOption));
+    }
+
+    public function getOrdersUrl()
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_orders_url', 'Orders'))
+    		return '';
+
+    	return BxDolService::call($this->_sActive, 'get_orders_url', array(), 'Orders');
+    }
+
+    public function getOrdersCount($sType)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_orders_count', 'Orders'))
+    		return array();
+
+		$aSrvParams = array($sType);
+		return BxDolService::call($this->_sActive, 'get_orders_count', $aSrvParams, 'Orders');
+    }
+
+    public function getOrdersInfo($aConditions)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_orders_info', 'Orders'))
+    		return array();
+
+		$aSrvParams = array($aConditions);
+		return BxDolService::call($this->_sActive, 'get_orders_info', $aSrvParams, 'Orders');
+    }
+
+    public function getPendingOrdersInfo($aConditions)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_pending_orders_info', 'Orders'))
+    		return array();
+
+		$aSrvParams = array($aConditions);
+		return BxDolService::call($this->_sActive, 'get_pending_orders_info', $aSrvParams, 'Orders');
+    }
+
+    public function getCartUrl($iVendor = 0)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_cart_url', 'Cart'))
+    		return '';
+
+    	return BxDolService::call($this->_sActive, 'get_cart_url', array($iVendor), 'Cart');
+    }
+
+    public function getCartItemsCount()
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_cart_items_count', 'Cart'))
+    		return array();
+
+		$aSrvParams = array();
+		return BxDolService::call($this->_sActive, 'get_cart_items_count', $aSrvParams, 'Cart');
+    }
+
+    public function getCartItemDescriptor($iVendorId, $iModuleId, $iItemId, $iItemCount)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_cart_item_descriptor', 'Cart'))
+    		return '';
+
+    	$aSrvParams = array($iVendorId, $iModuleId, $iItemId, $iItemCount);
+		return BxDolService::call($this->_sActive, 'get_cart_item_descriptor', $aSrvParams, 'Cart');
+    }
+
+    /**
+     * Returns cart JavaScript code which should be included in the page to make "Add To Cart" and "Subscribe" buttons work properly.
+     */
+    public function getCartJs($sType = '')
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_cart_js', 'Cart'))
+			return '';
+
+		$aSrvParams = array($sType);
+		return BxDolService::call($this->_sActive, 'get_cart_js', $aSrvParams, 'Cart');
+    }
+
+    /**
+     * Adds an item to cart in background mode.
+     */
+	public function addToCart($iVendorId, $mixedModuleId, $iItemId, $iItemCount)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'add_to_cart', 'Cart'))
+			return array();
+
+		$aSrvParams = array($iVendorId, $mixedModuleId, $iItemId, $iItemCount);
+		return BxDolService::call($this->_sActive, 'add_to_cart', $aSrvParams, 'Cart');
+    }
+
+    /**
+     * Returns "Add To Cart" JavaScript code to use in onclick attribute.
+     */
+    public function getAddToCartJs($iVendorId, $mixedModuleId, $iItemId, $iItemCount, $bNeedRedirect = false)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_add_to_cart_js', 'Cart'))
+			return array();
+
+		$aSrvParams = array($iVendorId, $mixedModuleId, $iItemId, $iItemCount, $bNeedRedirect);
+		return BxDolService::call($this->_sActive, 'get_add_to_cart_js', $aSrvParams, 'Cart');
+    }
+
+    /**
+     * Returns the whole "Add To Cart" link including HTML tag BUTTON and cart JavaScript code.
+     */
+    public function getAddToCartLink($iVendorId, $mixedModuleId, $iItemId, $iItemCount, $bNeedRedirect = false)
+    {
+		if(!BxDolRequest::serviceExists($this->_sActive, 'get_add_to_cart_link', 'Cart'))
+			return '';
+
+		$aSrvParams = array($iVendorId, $mixedModuleId, $iItemId, $iItemCount, $bNeedRedirect);
+		return BxDolService::call($this->_sActive, 'get_add_to_cart_link', $aSrvParams, 'Cart');
+    }
+
+    public function getSubscriptionsUrl()
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_subscriptions_url', 'Subscriptions'))
+    		return '';
+
+    	return BxDolService::call($this->_sActive, 'get_subscriptions_url', array(), 'Subscriptions');
+    }
+
+	public function getSubscribeUrl($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount = 1)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_subscribe_url', 'Cart'))
+    		return '';
+
+    	return BxDolService::call($this->_sActive, 'get_subscribe_url', array($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount), 'Cart');
+    }
+
+	/**
+     * Returns "Subscribe" JavaScript code to use in onclick attribute.
+     */
+    public function getSubscribeJs($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount = 1)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'get_subscribe_js', 'Subscriptions'))
+			return array();
+
+		$aSrvParams = array($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount);
+		return BxDolService::call($this->_sActive, 'get_subscribe_js', $aSrvParams, 'Subscriptions');
+    }
+
+    /**
+     * Returns the whole "Subscribe" link including HTML tag BUTTON and cart JavaScript code.
+     */
+    public function getSubscribeLink($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount = 1)
+    {
+		if(!BxDolRequest::serviceExists($this->_sActive, 'get_subscribe_link', 'Subscriptions'))
+			return '';
+
+		$aSrvParams = array($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount);
+		return BxDolService::call($this->_sActive, 'get_subscribe_link', $aSrvParams, 'Subscriptions');
+    }
+
+    public function initializeCheckout($iVendorId, $sProvider, $aItems = array())
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'initialize_checkout'))
+			return '';
+
+		$aSrvParams = array($iVendorId, $sProvider, $aItems);
+		return BxDolService::call($this->_sActive, 'initialize_checkout', $aSrvParams);
+    }
+
+    /**
+     * Isn't used anywhere for now. Is needed when license 'prolong' is requested from license keeping module like Market and ACL.  
+     */
+	public function prolongSubscription($sOrderId)
+    {
+    	if(!BxDolRequest::serviceExists($this->_sActive, 'prolong_subscription'))
+			return '';
+
+		$aSrvParams = array($sOrderId);
+		return BxDolService::call($this->_sActive, 'prolong_subscription', $aSrvParams);
+    }
+}
+
+/** @} */

+ 265 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolPermalinks.php

@@ -0,0 +1,265 @@
+<?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
+ * @{
+ */
+
+/**
+ * Permalinks for any content.
+ *
+ * An object of the class allows to check whether permalink is enabled
+ * and get it for specified standard URI.
+ *
+ *
+ * All permalinks must match the whole URL, to be automatially determined.
+ * There are only 2 permlinks which are replaced by prefix:
+ * - modules/?r= - for module URLs
+ * - page.php?i= - for pages URLs
+ *
+ *
+ * @section example Example of usage:
+ *
+ * 1. Register permalink in database by adding necessary info in the `sys_permalinks` table.
+ * 2. Create an object and process the URI
+ * @code
+ *   $oPermalinks = new BxDolPermalinks();
+ *   $oPermalinks->permalink('modules/?r=news/home');
+ * @endcode
+ *
+ *
+ * @section acl Memberships/ACL:
+ * Doesn't depend on user's membership.
+ *
+ *
+ * @section alerts Alerts:
+ *
+ * The following alert can be used when permalink call is performed:
+ * - $sUnit - system
+ * - $sAction - permalink | unpermalink
+ * - $iObjectId - 0 (not used)
+ * - $iSenderId - 0 (not used)
+ * - $aExtra['link'] - the link to check permalink for
+ * - $aExtra['return_data'] - it is possible to override default behavior, by setting 'return_data' to non NULL value.
+ *
+ */
+class BxDolPermalinks extends BxDolDb implements iBxDolSingleton
+{
+    protected $sCacheFile;
+    protected $aLinksStandard;
+    protected $aLinksPermalink;
+    protected $aPrefixesStandard;
+    protected $aPrefixesPermalink;
+
+    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();
+
+        if (getParam('sys_db_cache_enable')) {
+            $oCache = $this->getDbCacheObject();
+            $sCacheKey = $this->genDbCacheKey('sys_permalinks');
+            $aPermalinksData = $oCache->getData($sCacheKey);
+            if (null === $aPermalinksData) {
+                $aPermalinksData = $this->getPermalinksData();
+                $oCache->setData ($sCacheKey, $aPermalinksData);
+            }
+        } else {
+            $aPermalinksData = $this->getPermalinksData();
+        }
+
+        $this->aLinksStandard = $aPermalinksData['standard'];
+        $this->aLinksPermalink = $aPermalinksData['permalink'];
+        $this->aPrefixesStandard = $aPermalinksData['prefixes_standard'];
+        $this->aPrefixesPermalink = $aPermalinksData['prefixes_permalink'];
+    }
+
+    public function cacheInvalidate()
+    {
+        return BxDolDb::getInstance()->cleanCache ('sys_permalinks');
+    }
+
+    /**
+     * 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 BxDolPermalinks();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    function getPermalinksData()
+    {
+        $aLinksStandard = $this->getAll("SELECT * FROM `sys_permalinks`");
+
+        $aResult = array(
+            'standard' => array(),
+            'permalink' => array(),
+            'prefixes_standard' => array(),
+            'prefixes_permalink' => array(),
+        );
+
+        foreach ($aLinksStandard as $aLink) {
+            $a = array(
+                'permalink' => $aLink['permalink'],
+                'standard' => $aLink['standard'],
+                'check' => $aLink['check'],
+                'enabled' => getParam($aLink['check']) == 'on',
+            );
+            $aResult['standard'][$aLink['standard']] = $a;
+            $aResult['permalink'][$aLink['permalink']] = $a;
+            if ($aLink['compare_by_prefix']) {
+                $aResult['prefixes_standard'][$aLink['standard']] = strlen($aLink['standard']);
+                $aResult['prefixes_permalink'][$aLink['permalink']] = strlen($aLink['permalink']);
+            }
+        }
+
+        return $aResult;
+    }
+
+    /**
+     * Get page name (changeable page of URL) from the URL.
+     * @param $sLink - relative UNpermalinked, it is better to use unpermalink method to make sure that correct url is passed here
+     * @param $iLength - position of the page name, or false to detect automatically
+     * @return page name (changeable part of URL) or empty string if page name is not detectable.
+     */
+    function getPageNameFromLink($sLink, $iLength = false)
+    {
+        if (false == $iLength) {
+            $sLink = $this->_fixUrl($sLink);
+            foreach ($this->aPrefixesStandard as $sKey => $iLen) {
+                if (strncmp($sLink, $sKey, $iLen) === 0) {
+                    $iLength = $iLen;
+                    break;
+                }
+            }
+        }
+        return false === $iLength ? '' : substr($sLink, $iLength);
+    }
+
+    /**
+     * Make permalink from link.
+     * @param $sLink - relative URL.
+     * @param $aParams - params to add to the url.
+     * @return - relative permalinked URL if it was detected and permalinks are ON or unchanged URL otherwise.
+     */
+    function permalink($sLink, $aParams = array())
+    {
+        $sRet = null;
+        bx_alert('system', 'permalink', 0, 0, array('link' => $sLink, 'params' => &$aParams, 'return_data' => &$sRet));
+        if (null !== $sRet)
+            return $sRet;
+
+        $sLink = $this->_fixUrl($sLink);
+
+        foreach ($this->aPrefixesStandard as $sKey => $iLength) {
+
+            if (strncmp($sLink, $sKey, $iLength) !== 0)
+                continue;
+
+            $sPage = $this->getPageNameFromLink($sLink, $iLength);
+
+            if (!$this->_isEnabled($sKey))
+                return $sLink;
+
+            $sUrl = $this->_fixUrlAmpersand($this->aLinksStandard[$sKey]['permalink'] . $sPage);
+
+            return bx_append_url_params($sUrl, $aParams);
+
+        }
+
+        return bx_append_url_params($this->_isEnabled($sLink) ? $this->aLinksStandard[$sLink]['permalink'] : $sLink, $aParams);
+    }
+
+    /**
+     * Remove permalink from link.
+     * @param $sLink - relative or absoulte URL.
+     * @param $isStripBaseUrl - strip site prefix (absolute URL) automatically (enabled by default)
+     * @return - relative UNpermalinked URL if it was detected or relative URL if URL withing the site or unchanged URL otherwise.
+     */
+    function unpermalink($sLink, $isStripBaseUrl = true)
+    {
+        if ($isStripBaseUrl && 0 === strncmp($sLink, BX_DOL_URL_ROOT, strlen(BX_DOL_URL_ROOT)))
+            $sLink = substr($sLink, strlen(BX_DOL_URL_ROOT));
+
+        $sRet = null;
+        bx_alert('system', 'unpermalink', 0, 0, array('link' => $sLink, 'return_data' => &$sRet));
+        if (null !== $sRet)
+            return $sRet;
+
+        foreach ($this->aPrefixesPermalink as $sKey => $iLength) {
+
+            if (strncmp($sLink, $sKey, $iLength) !== 0)
+                continue;
+
+            $sPage = substr($sLink, $iLength);
+
+            return $this->aLinksPermalink[$sKey]['standard'] . $sPage;
+
+        }
+
+        return isset($this->aLinksPermalink[$sLink]) ? $this->aLinksPermalink[$sLink]['standard'] : $sLink;
+    }
+
+    function _isEnabled($sLink)
+    {
+        return array_key_exists($sLink, $this->aLinksStandard) && $this->aLinksStandard[$sLink]['enabled'];
+    }
+
+    /**
+     * redirect to the correct url after switching skin or language
+     * only correct modules urls are supported
+     */
+    function redirectIfNecessary ($aSkip = array())
+    {
+        $sCurrentUrl = $_SERVER['PHP_SELF'] . '?' . bx_encode_url_params($_GET, $aSkip);
+
+        if (!preg_match('/modules\/index.php\?r=(\w+)(.*)/', $sCurrentUrl, $m))
+            return false;
+
+        $sStandardLink = 'modules/?r=' . $m[1] . '/';
+        $sPermalink = $this->permalink ($sStandardLink);
+
+        if (false !== strpos($sCurrentUrl, $sPermalink))
+            return false;
+
+        header("HTTP/1.1 301 Moved Permanently");
+        header ('Location:' . BX_DOL_URL_ROOT . $sPermalink . rtrim(trim(urldecode($m[2]), '/'), '&'));
+
+        return true;
+    }
+
+    protected function _fixUrlAmpersand($sLink)
+    {
+        $iPosQuestion = strpos($sLink, '?');
+
+        if (false === $iPosQuestion && false !== ($iPosAmpersand = strpos($sLink, '&')))
+            $sLink = substr_replace($sLink, '?', $iPosAmpersand, 1);
+
+        return $sLink;
+    }
+
+    protected function _fixUrl($sLink)
+    {
+        if (strncmp($sLink, 'modules/index.php?r=', 20) === 0)
+            $sLink = str_replace('modules/index.php?r=', 'modules/?r=', $sLink);
+        return $sLink;
+    }
+}
+
+/** @} */

+ 350 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolPrivacy.php

@@ -0,0 +1,350 @@
+<?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_PG_HIDDEN', '1');
+define('BX_DOL_PG_MEONLY', '2');
+define('BX_DOL_PG_ALL', '3');
+define('BX_DOL_PG_MEMBERS', '4');
+define('BX_DOL_PG_FRIENDS', '5');
+
+define('BX_DOL_PG_DEFAULT', BX_DOL_PG_ALL);
+
+/**
+ * Privacy settings for any content.
+ *
+ * Integration of the content with privacy engine allows site member
+ * to organize the access to his content.
+ *
+ * Related classes:
+ *  BxDolPrivacyQuery - database queries.
+ *
+ * Example of usage:
+ * 1. Register your privacy actions in `sys_privacy_actions` database table.
+ * 2. Add one privacy field(with INT type) in the table with your items for each action.
+ *    For example, for action 'comment', the field name should be 'allow_comment_to'.
+ * 3. Add group choosers for necessary actions in the form, which is used to add new items.
+ * @code
+ *    $oPrivacy = new BxDolPrivacy();
+ *    $oPrivacy->getGroupChooser($iItemOwnerId, $sModuleUri, $sModuleAction);
+ * @endcode
+ *
+ * 4. Check privacy when any user tries to view an item.
+ * @code
+ *    $oPrivacy = new BxDolPrivacy($sTable, $sFieldId, $sFieldOwnerId);
+ *    if($oPrivacy->check($sAction, $iObjectId, $iViewerId)) {
+ *     //show necessary content
+ *    }
+ * @endcode
+ *
+ *    @see an example of integration in BoonEx modules, for example: Posts
+ *
+ *
+ * Memberships/ACL:
+ * Doesn't depend on user's membership.
+ *
+ *
+ * Alerts:
+ * no alerts available
+ *
+ */
+class BxDolPrivacy extends BxDolFactory implements iBxDolFactoryObject
+{
+    protected $_oDb;
+    protected $_sObject;
+    protected $_aObject;
+
+    /**
+     * Constructor
+     * @param $aObject array of grid options
+     */
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+
+        $this->_aObject = $aObject;
+        $this->_sObject = $aObject['object'];
+
+        $this->_oDb = new BxDolPrivacyQuery();
+        $this->_oDb->init($this->_aObject);
+    }
+
+    /**
+     * Get privacy object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    public static function getObjectInstance($sObject)
+    {
+        if(isset($GLOBALS['bxDolClasses']['BxDolPrivacy!' . $sObject]))
+            return $GLOBALS['bxDolClasses']['BxDolPrivacy!' . $sObject];
+
+        $aObject = BxDolPrivacyQuery::getPrivacyObject($sObject);
+        if(!$aObject || !is_array($aObject))
+            return false;
+
+        $sClass = 'BxTemplPrivacy';
+        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']['BxDolPrivacy!' . $sObject] = $o);
+    }
+
+    /**
+     * Get Select element with available groups.
+     *
+     * @param  string  $sObject  privacy object name.
+     * @param  integer $iOwnerId object's owner ID.
+     * @param  array   $aParams  an array of custom selector's params (dynamic_groups - an array of arrays('key' => group_id, 'value' => group_title), title - the title to be used for generated field).
+     * @return an      array with Select element description.
+     */
+    public static function getGroupChooser($sObject, $iOwnerId = 0, $aParams = array())
+    {
+        $oPrivacy = BxDolPrivacy::getObjectInstance($sObject);
+        if(empty($oPrivacy))
+            return array();
+
+        $sModule = $oPrivacy->_aObject['module'];
+        $sAction = $oPrivacy->_aObject['action'];
+
+        if($iOwnerId == 0)
+            $iOwnerId = bx_get_logged_profile_id();
+
+        $sValue = $oPrivacy->_oDb->getDefaultGroupByUser($sModule, $sAction, $iOwnerId);
+        if(empty($sValue))
+            $sValue = $oPrivacy->_oDb->getDefaultGroup($sModule, $sAction);
+
+        $aValues = $oPrivacy->getGroups();
+
+        if(isset($aParams['dynamic_groups']) && is_array($aParams['dynamic_groups']))
+            $aValues = array_merge($aValues, $aParams['dynamic_groups']);
+
+        $sName = $oPrivacy->convertActionToField($sAction);
+
+        $sTitle = isset($aParams['title']) && !empty($aParams['title']) ? $aParams['title'] : '';
+        if(empty($sTitle)) {
+            $sTitle = $oPrivacy->_oDb->getTitle($sModule, $sAction);
+            $sTitle = _t(!empty($sTitle) ? $sTitle : '_' . $sName);
+        }
+
+        return array(
+            'type' => 'select',
+            'name' => $sName,
+            'caption' => $sTitle,
+            'value' => $sValue,
+            'values' => $aValues,
+            'checker' => array(
+                'func' => 'avail',
+                'error' => _t('_ps_ferr_incorrect_select')
+            ),
+            'db' => array(
+                'pass' => 'Int'
+            )
+        );
+    }
+
+    /**
+     * Get database field name for action.
+     *
+     * @param  string $sObject privacy object name.
+     * @param  string $sAction action name.
+     * @return string with field name.
+     */
+    public static function getFieldName($sObject, $sAction = '')
+    {
+    	$oPrivacy = BxDolPrivacy::getObjectInstance($sObject);
+        if(empty($oPrivacy))
+            return '';
+
+		if(empty($sAction))
+			$sAction = $oPrivacy->_aObject['action'];
+
+        return $oPrivacy->convertActionToField($sAction);
+    }
+
+    /**
+     * Get necessary condition array to use privacy in search classes
+     * @param $mixedGroupId group ID or array of group IDs
+     * @return array of conditions, for now with 'restriction' part only is returned
+     */
+    public function getContentByGroupAsCondition($mixedGroupId)
+    {
+        return array(
+            'restriction' => array (
+                'privacy_' . $this->_sObject => array(
+                    'value' => $mixedGroupId,
+                    'field' => $this->convertActionToField($this->_aObject['action']),
+                    'operator' => is_array($mixedGroupId) ? 'in' : '=',
+                    'table' => $this->_aObject['table'],
+                ),
+            ),
+        );
+    }
+
+    /**
+     * Get necessary condition array to use privacy in search classes
+     * @param $iProfileIdOwner owner profile ID
+     * @return array of conditions, for now with 'restriction' part only is returned
+     */
+    public function getContentPublicAsCondition($iProfileIdOwner = 0, $aCustomGroups = array())
+    {
+        $mixedPrivacyGroups = $this->getPrivacyGroupsForContentPublic($iProfileIdOwner, $aCustomGroups);
+        if($mixedPrivacyGroups === true)
+        	return array();
+
+        return $this->getContentByGroupAsCondition($mixedPrivacyGroups);
+    }
+
+    /**
+     * Get necessary parts of SQL query to use privacy in other queries
+     * @param $mixedGroupId group ID or array of group IDs
+     * @return array of SQL string parts, for now 'where' part only is returned
+     */
+    public function getContentByGroupAsSQLPart($mixedGroupId)
+    {
+        $sField = $this->convertActionToField($this->_aObject['action']);
+        return $this->_oDb->getContentByGroupAsSQLPart($sField, $mixedGroupId);
+    }
+
+	/**
+     * Get necessary parts of SQL query to use privacy in other queries
+     * @param $iProfileIdOwner owner profile ID
+     * @return array of SQL string parts, for now 'where' part only is returned
+     */
+    public function getContentPublicAsSQLPart($iProfileIdOwner = 0, $aCustomGroups = array())
+    {
+        $mixedPrivacyGroups = $this->getPrivacyGroupsForContentPublic($iProfileIdOwner, $aCustomGroups);
+		if($mixedPrivacyGroups === true)
+        	return array();
+
+        return $this->getContentByGroupAsSQLPart($mixedPrivacyGroups);
+    }
+
+    /**
+     * Check whether the viewer can make requested action.
+     *
+     * @param  integer $iObjectId object ID the action to be performed with.
+     * @param  integer $iViewerId viewer ID.
+     * @return boolean result of operation.
+     */
+    function check($iObjectId, $iViewerId = 0)
+    {
+        if(empty($iViewerId))
+            $iViewerId = (int)bx_get_logged_profile_id();
+
+        $aObject = $this->_oDb->getObjectInfo($this->convertActionToField($this->_aObject['action']), $iObjectId);
+        if(empty($aObject) || !is_array($aObject))
+            return false;
+
+        if($aObject['group_id'] == BX_DOL_PG_HIDDEN)
+            return false;
+
+        if(isAdmin() || $iViewerId == $aObject['owner_id'])
+            return true;
+
+        $aGroup = $this->_oDb->getGroupsBy(array('type' => 'id', 'id' => $aObject['group_id']));
+        if(!empty($aGroup) && is_array($aGroup) && (int)$aGroup['active'] == 1 && !empty($aGroup['check'])) {
+            $sCheckMethod = $this->getCheckMethod($aGroup['check']);
+            if(method_exists($this, $sCheckMethod) && $this->$sCheckMethod($aObject['owner_id'], $iViewerId))
+                return true;
+        }
+
+        return $this->isDynamicGroupMember($aObject['group_id'], $aObject['owner_id'], $iViewerId, $iObjectId);
+    }
+
+    public function checkMeOnly($iOwnerId, $iViewerId)
+    {
+        return false;
+    }
+
+    public function checkPublic($iOwnerId, $iViewerId)
+    {
+        return true;
+    }
+
+    public function checkMembers($iOwnerId, $iViewerId)
+    {
+        return isMember();
+    }
+
+    public function checkFriends($iOwnerId, $iViewerId)
+    {
+        return BxDolConnection::getObjectInstance('sys_profiles_friends')->isConnected($iOwnerId, $iViewerId, true);
+    }
+
+    public function setTableFieldAuthor($sValue)
+    {
+        $this->_aObject['table_field_author'] = $sValue;
+
+        $this->_oDb->init($this->_aObject);
+    }
+
+	protected function getPrivacyGroupsForContentPublic($iProfileIdOwner = 0, $aCustomGroups = array())
+    {
+    	$aGroups = array(BX_DOL_PG_ALL);
+        if(isLogged()) {
+            $iProfileIdLogged = bx_get_logged_profile_id();
+            if($iProfileIdLogged == $iProfileIdOwner)
+                return true;
+
+			$aGroups[] = BX_DOL_PG_MEMBERS;
+            if($iProfileIdOwner && $this->checkFriends($iProfileIdOwner, $iProfileIdLogged))
+                $aGroups[] = BX_DOL_PG_FRIENDS;
+        }
+
+        return array_merge($aGroups, $aCustomGroups);
+    }
+
+    protected function getCheckMethod($s)
+    {
+        if(substr($s, 0, 1) != '@')
+            return false;
+
+        return bx_gen_method_name(str_replace('@', 'check_', $s));
+    }
+
+    protected function convertActionToField($sAction)
+    {
+    	return 'allow_' . strtolower(str_replace(' ', '-', $sAction)) . '_to';
+    }
+
+    /**
+     * 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.
+     */
+    protected function isDynamicGroupMember($mixedGroupId, $iObjectOwnerId, $iViewerId, $iObjectId)
+    {
+        return false;
+    }
+
+    /**
+     * get privacy groups for getGroupChooser
+     */ 
+    protected function getGroups() 
+    {
+        $aValues = array();
+        $aGroups = $this->_oDb->getGroupsBy(array('type' => 'active'));
+        foreach($aGroups as $aGroup) {
+            if((int)$aGroup['active'] == 0)
+               continue;
+
+            $aValues[] = array('key' => $aGroup['id'], 'value' => _t($aGroup['title']));
+        }
+        return $aValues;
+    }
+}
+
+/** @} */

+ 192 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolPrivacyQuery.php

@@ -0,0 +1,192 @@
+<?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 BxDolPrivacyQuery extends BxDolDb
+{
+    protected $_aObject;
+
+    protected $_sTable;
+    protected $_sFieldId;
+    protected $_sFieldOwnerId;
+
+    protected $_sCachePrivacyObject;
+    protected $_sCachePrivacyObjectDefault;
+
+    protected $_sCacheGroup;
+    protected $_sCacheGroupFriends;
+    protected $_sCacheGroupsActive;
+
+    protected $_sCacheTestedObject;
+
+    public function __construct()
+    {
+        parent::__construct();
+        $this->_aObject = array();
+
+        $this->_sCachePrivacyObject = 'sys_privacy_object_';
+        $this->_sCachePrivacyObjectDefault = 'sys_privacy_object_default_';
+
+        $this->_sCacheGroup = 'sys_privacy_group_';
+        $this->_sCacheGroupFriends = 'sys_privacy_group_friends_';
+        $this->_sCacheGroupsActVis = 'sys_privacy_group_act_vis';
+
+        $this->_sCacheTestedObject = 'sys_privacy_tested_object_';
+    }
+
+    static public function getPrivacyObject($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_privacy` WHERE `object` = ?", $sObject);
+
+        $aObject = $oDb->getRow($sQuery);
+        if(!$aObject || !is_array($aObject))
+            return false;
+
+        return $aObject;
+    }
+
+    public function init($aObject = array())
+    {
+        if(empty($aObject) || !is_array($aObject)) 
+            return;
+
+        $this->_aObject = $aObject;
+
+        $this->_sTable = $this->_aObject['table'];
+        $this->_sFieldId = $this->_aObject['table_field_id'];
+        $this->_sFieldOwnerId = $this->_aObject['table_field_author'];
+    }
+
+    function getObjectInfo($sAction, $iObjectId)
+    {
+        if(empty($this->_sTable) || empty($this->_sFieldId) || empty($this->_sFieldOwnerId))
+            return array();
+
+        $sQuery = $this->prepare("SELECT `" . $this->_sFieldOwnerId . "` AS `owner_id`, `" . $sAction . "` AS `group_id` FROM `" . $this->_sTable . "` WHERE `" . $this->_sFieldId . "`=? LIMIT 1", $iObjectId);
+
+        $sCacheKey = $this->_sCacheTestedObject . $this->_sTable . '_' . $this->_aObject['action'] . '_' . $iObjectId;
+        return $this->fromMemory($sCacheKey, 'getRow', $sQuery);
+    }
+
+    function getGroupsBy($aParams)
+    {
+        switch($aParams['type']) {
+            case 'id':
+                $sCacheFunction = 'fromCache';
+                $sCacheName = $this->_sCacheGroup . $aParams['id'];
+                $sMethod = 'getRow';
+                $sWhereClause = $this->prepareAsString("`id`=?", (int)$aParams['id']);
+                break;
+
+            case 'active':
+                $sCacheFunction = 'fromMemory';
+                $sCacheName = $this->_sCacheGroupsActVis;
+                $sMethod = 'getAll';
+                $sWhereClause = "`active`='1' AND `visible`='1'";
+                break;
+        }
+
+        $sSql = "SELECT
+                   `id`,
+                   `title`,
+                   `check`,
+                   `active`
+                FROM `sys_privacy_groups`
+                WHERE " . $sWhereClause;
+
+        return !empty($sCacheFunction) && !empty($sCacheName) ? $this->$sCacheFunction($sCacheName, $sMethod, $sSql) : $this->$sMethod($sSql);
+    }
+
+    function getActions($iOwnerId)
+    {
+        $sQuery = $this->prepare("SELECT
+                    `tm`.`uri` AS `module_uri`,
+                    `tm`.`title` AS `module_title`,
+                    `ta`.`id` AS `action_id`,
+                    `ta`.`title` AS `action_title`,
+                    `ta`.`default_group` AS `action_default_value`,
+                    `td`.`group_id` AS `default_value`
+                FROM `sys_objects_privacy` AS `ta`
+                LEFT JOIN `sys_privacy_defaults` AS `td` ON `ta`.`id`=`td`.`action_id` AND `td`.`owner_id`=?
+                INNER JOIN `sys_modules` AS `tm` ON `ta`.`module`=`tm`.`uri`
+                WHERE 1
+                ORDER BY `tm`.`title`", $iOwnerId);
+
+        return $this->getAll($sQuery);
+    }
+
+    function getTitle($sModule, $sAction)
+    {
+        $aAction = $this->_getAction($sModule, $sAction);
+        return !empty($aAction) && isset($aAction['title']) ? $aAction['title'] : '';
+    }
+
+    function getDefaultGroup($sModule, $sAction)
+    {
+        $aAction = $this->_getAction($sModule, $sAction);
+        return !empty($aAction) && isset($aAction['default_group']) ? $aAction['default_group'] : BX_DOL_PG_DEFAULT;
+    }
+
+    function getDefaultGroupByUser($sModule, $sAction, $iOwnerId)
+    {
+        $sQuery = $this->prepare("SELECT
+               `td`.`group_id`
+            FROM `sys_objects_privacy` AS `ta`
+            LEFT JOIN `sys_privacy_defaults` AS `td` ON `ta`.`id`=`td`.`action_id` AND `td`.`owner_id`=?
+            WHERE
+                `ta`.`module`=? AND `ta`.`action`=?
+            LIMIT 1", $iOwnerId, $sModule, $sAction);
+
+        $sCacheKey = $this->_sCachePrivacyObjectDefault . $sModule . '_' . $sAction . '_' . $iOwnerId;
+        return $this->fromMemory($sCacheKey, 'getOne', $sQuery);
+    }
+
+    function replaceDefaulfGroup($iActionId, $iOwnerId, $iGroupId)
+    {
+        $sSql = $this->prepare("REPLACE INTO
+                `sys_privacy_defaults`
+            SET
+                `owner_id`=?,
+                `action_id`=?,
+                `group_id`=?", $iOwnerId, $iActionId, $iGroupId);
+
+        return $this->query($sSql);
+    }
+
+    function getContentByGroupAsSQLPart($sField, $mixedGroupId)
+    {
+        if(is_array($mixedGroupId))
+            $sWhere = " AND `" . $this->_sTable . "`.`" . $sField . "` IN (" . $this->implode_escape($mixedGroupId) . ")";
+        else
+            $sWhere = $this->prepareAsString(" AND `" . $this->_sTable . "`.`" . $sField . "` = ?", $mixedGroupId);
+
+        return array(
+            'where' => $sWhere
+        );
+    }
+
+    protected function _getAction($sModule, $sAction)
+    {
+        $sQuery = $this->prepare("SELECT
+                `id`,
+                `module`,
+                `action`,
+                `title`,
+                `default_group`
+            FROM `sys_objects_privacy` AS `ta`
+            WHERE `module`=? AND `action`=?
+            LIMIT 1", $sModule, $sAction);
+
+        $sCacheKey = $this->_sCachePrivacyObject . $sModule . '_' . $sAction;
+        return $this->fromCache($sCacheKey, 'getRow', $sQuery);
+    }
+}
+
+/** @} */

+ 502 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolProfile.php

@@ -0,0 +1,502 @@
+<?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 $_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)
+    {
+        if (!$iAccountId)
+            $iAccountId = getLoggedId();
+        $oQuery = BxDolProfileQuery::getInstance();
+        $aProfile = $oQuery->getProfileByContentTypeAccount($iAccountId, 'system', $iAccountId);
+        if (!$aProfile)
+            return false;
+        return self::getInstance($aProfile['id']);
+    }
+
+    /**
+     * 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)
+    {
+        $oQuery = BxDolProfileQuery::getInstance();
+        $aProfile = $oQuery->getProfileByContentAndType($iContent, $sType);
+        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)
+    {
+        $oQuery = BxDolProfileQuery::getInstance();
+        $mixedProfileId = $oQuery->getCurrentProfileByAccount($iAccountId);
+
+        return self::getInstance($mixedProfileId);
+    }
+
+    /**
+     * Get singleton instance of Profile by profile id
+     */
+    public static function getInstance($mixedProfileId = false)
+    {
+        if (!$mixedProfileId) {
+            $oQuery = BxDolProfileQuery::getInstance();
+            $mixedProfileId = $oQuery->getCurrentProfileByAccount(getLoggedId());
+        }
+
+        $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()
+    {
+        return $this->_oQuery->getIdById($this->_iProfileID);
+    }
+
+    /**
+     * 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)
+    {
+        return $this->_oQuery->getInfoById((int)$iProfileId ? $iProfileId : $this->_iProfileID);
+    }
+
+    /**
+     * 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)
+    {
+        $aInfo = $this->getInfo($iProfileId);
+        return BxDolService::call($aInfo['type'], 'profile_unit', 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']) {
+
+            $aProfiles = $oAccount->getProfiles();            
+            $oProfileAccount = BxDolProfile::getInstanceAccountProfile($aProfileInfo['account_id']);
+
+            // unset deleted profile and account profile
+            unset($aProfiles[$ID]);
+            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();
+            }
+
+            $oAccount->updateProfileContext($iProfileId);
+        }
+
+        // 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 comments
+        if($bDeleteWithContent)
+	        BxDolCmts::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;
+
+        // 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;
+
+        if ($iViewerAccountId != $this->getAccountId() ||  $iViewerProfileId == $this->id())
+            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()))), 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);
+        }
+    }
+}
+
+/** @} */

+ 161 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolProfileUndefined.php

@@ -0,0 +1,161 @@
+<?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
+ * @{
+ */
+
+/**
+ * When profile is not available (for example profile is deleted) then use this special class.
+ *
+ * @section example Example of usage
+ *
+ * @code
+ * $oProfile = BxDolProfile::getInstance($iId);
+ * if (!$oProfile)
+ *     $oProfile = BxDolProfileUndefined::getInstance();
+ * @endcode
+ */
+class BxDolProfileUndefined extends BxDolFactory implements iBxDolSingleton, iBxDolProfile
+{
+    /**
+     * Constructor
+     */
+    protected function __construct ()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        $sClass = get_class($this);
+        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()
+    {
+        $sClass = __CLASS__;
+        if(!isset($GLOBALS['bxDolClasses'][$sClass]))
+            $GLOBALS['bxDolClasses'][$sClass] = new $sClass();
+
+        return $GLOBALS['bxDolClasses'][$sClass];
+    }
+
+    /**
+     * Get profile id
+     */
+    public function id()
+    {
+        return 0;
+    }
+
+    /**
+     * Get profile display name
+     */
+    public function getDisplayName()
+    {
+        return _t('_uknown');
+    }
+
+    /**
+     * Get profile url
+     */
+    public function getUrl()
+    {
+        return 'javascript:void(0);';
+    }
+
+    /**
+     * Get profile unit
+     */
+    public function getUnit()
+    {
+		return BxDolTemplate::getInstance()->parseHtmlByName('profile_unit.html', array(
+        	'thumb_url' => $this->getThumb(),
+        	'title' => $this->getDisplayName()
+        ));
+    }
+
+    /**
+     * Get picture url
+     */
+    public function getPicture()
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account-picture.png');
+    }
+
+    /**
+     * Get profile avatar
+     */
+    public function getAvatar()
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account-avatar.png');
+    }
+
+    /**
+     * Get profile thumb
+     */
+    public function getThumb()
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account-thumb.png');
+    }
+
+    /**
+     * Get profile icon
+     */
+    public function getIcon()
+    {
+        return BxDolTemplate::getInstance()->getImageUrl('account-icon.png');
+    }
+
+    /**
+     * Get module icon
+     */
+    public function getIconModule($iProfileId = 0)
+    {
+        return 'user';
+    }
+
+    /**
+     * Get profile edit url
+     */
+    public function getEditUrl()
+    {
+        return '';
+    }
+
+    /**
+     * @see iBxDolProfile::checkAllowedProfileView
+     */
+    public function checkAllowedProfileView($iProfileId = 0)
+    {
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+    /**
+     * @see iBxDolProfile::checkAllowedPostInProfile
+     */
+    public function checkAllowedPostInProfile($iProfileId = 0)
+    {
+        return _t('_sys_txt_access_denied');
+    }
+
+    /**
+     * Check if profile is active
+     */
+    public function isActive()
+    {
+        return true;
+    }
+}
+
+/** @} */

+ 233 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolReport.php

@@ -0,0 +1,233 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+bx_import('BxDolAcl');
+
+define('BX_DOL_REPORT_USAGE_BLOCK', 'block');
+define('BX_DOL_REPORT_USAGE_INLINE', 'inline');
+define('BX_DOL_REPORT_USAGE_DEFAULT', BX_DOL_REPORT_USAGE_BLOCK);
+
+/**
+ * Report any content
+ *
+ * Related classes:
+ * - BxDolReportQuery - report database queries
+ * - BxBaseReport - report base representation
+ * - BxTemplReport - custom template representation
+ *
+ * AJAX report for any content.
+ *
+ * To add report section to your feature you need to add a record to 'sys_objects_report' table:
+ *
+ * - id - autoincremented id for internal usage
+ * - name - your unique module name, with vendor prefix, lowercase and spaces are underscored
+ * - table_main - table name where summary reports are stored
+ * - table_track - table name where each report is stored
+ * - is_on - is this report object enabled
+ * - base_url - base URL for reported object
+ * - trigger_table - table to be updated upon each report
+ * - trigger_field_id - trigger_table table field with unique record id, primary key
+ * - trigger_field_author - trigger_table table field with author
+ * - trigger_field_count - trigger_table table field with reports count
+ * - class_name - your custom class name, if you overrride default class
+ * - class_file - your custom class path
+ *
+ * You can refer to BoonEx modules for sample record in this table.
+ *
+ *
+ *
+ * @section example Example of usage:
+ * You can show report in any place, using the following code:
+ * @code
+ * $o = BxDolReport::getObjectInstance('system object name', $iYourEntryId);
+ * if (!$o->isEnabled()) 
+ *     return '';
+ * echo $o->getElementBlock();
+ * @endcode
+ *
+ *
+ * @section acl Memberships/ACL:
+ * - report
+ * - report view
+ *
+ *
+ * @section alerts Alerts:
+ * Alerts type/unit - every module has own type/unit, it equals to ObjectName.
+ * The following alerts are rised:
+ *
+ * - type: 'object name', action: doReport
+ * - type: report, action: do
+ *
+ */
+
+class BxDolReport extends BxDolObject
+{
+	protected $_oTemplate;
+
+	protected $_sBaseUrl;
+
+	protected $_sFormObject;
+    protected $_sFormDisplayPost;
+
+	protected $_aTypes;
+
+    protected function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
+    {
+        parent::__construct($sSystem, $iId, $iInit);
+        if(empty($this->_sSystem))
+            return;
+
+        $this->_oQuery = new BxDolReportQuery($this);
+
+        if ($oTemplate)
+            $this->_oTemplate = $oTemplate;
+        else
+            $this->_oTemplate = BxDolTemplate::getInstance();
+
+		$this->_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->_sFormObject = 'sys_report';
+		$this->_sFormDisplayPost = 'sys_report_post';
+
+		$this->_aTypes = array('spam', 'scam', 'fraud', 'nude', 'other');
+    }
+
+    /**
+     * get reports object instanse
+     * @param $sSys report object name
+     * @param $iId associated content id, where report is available
+     * @param $iInit perform initialization
+     * @return null on error, or ready to use class instance
+     */
+    public static function getObjectInstance($sSys, $iId, $iInit = true, $oTemplate = false)
+    {
+        if(isset($GLOBALS['bxDolClasses']['BxDolReport!' . $sSys . $iId]))
+            return $GLOBALS['bxDolClasses']['BxDolReport!' . $sSys . $iId];
+
+        $aSystems = self::getSystems();
+        if(!isset($aSystems[$sSys]))
+            return null;
+
+        $sClassName = 'BxTemplReport';
+        if(!empty($aSystems[$sSys]['class_name'])) {
+            $sClassName = $aSystems[$sSys]['class_name'];
+            if(!empty($aSystems[$sSys]['class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aSystems[$sSys]['class_file']);
+        }
+
+        $o = new $sClassName($sSys, $iId, $iInit, $oTemplate);
+        return ($GLOBALS['bxDolClasses']['BxDolReport!' . $sSys . $iId] = $o);
+    }
+
+    public static function &getSystems()
+    {
+        if(!isset($GLOBALS['bx_dol_report_systems']))
+            $GLOBALS['bx_dol_report_systems'] = BxDolDb::getInstance()->fromCache('sys_objects_report', 'getAllWithKey', '
+                SELECT
+                    `id` as `id`,
+                    `name` AS `name`,
+                    `table_main` AS `table_main`,
+                    `table_track` AS `table_track`,
+                    `is_on` AS `is_on`,
+                    `base_url` AS `base_url`,
+                    `trigger_table` AS `trigger_table`,
+                    `trigger_field_id` AS `trigger_field_id`,
+                    `trigger_field_author` AS `trigger_field_author`,
+                    `trigger_field_count` AS `trigger_field_count`,
+                    `class_name` AS `class_name`,
+                    `class_file` AS `class_file`
+                FROM `sys_objects_report`', 'name');
+
+        return $GLOBALS['bx_dol_report_systems'];
+    }
+
+    /**
+     * Interface functions for outer usage
+     */
+	public function getBaseUrl()
+    {
+        return $this->_replaceMarkers($this->_sBaseUrl);
+    }
+
+    public function getStatCounter()
+    {
+        $aReport = $this->_oQuery->getReport($this->getId());
+        return $aReport['count'];
+    }
+
+    /**
+     * Actions functions
+     */
+    public function actionReport()
+    {
+        return echoJson($this->_getReport());
+    }
+
+	public function actionGetReportedBy()
+    {
+        if (!$this->isEnabled())
+           return '';
+
+	    if(!$this->isAllowedReportView(true))
+            return $this->msgErrAllowedReportView();
+
+        return $this->_getReportedBy();
+    }
+
+    /**
+     * Permissions functions
+     */
+    public function isAllowedReport($isPerformAction = false)
+    {
+        if(isAdmin())
+            return true;
+
+        return $this->checkAction('report', $isPerformAction);
+    }
+
+    public function msgErrAllowedReport()
+    {
+        return $this->checkActionErrorMsg('report');
+    }
+
+    public function isAllowedReportView($isPerformAction = false)
+    {
+        if(isAdmin())
+            return true;
+
+        return $this->checkAction('report_view', $isPerformAction);
+    }
+
+    public function msgErrAllowedReportView()
+    {
+        return $this->checkActionErrorMsg('report_view');
+    }
+
+    /**
+     * Internal functions
+     */
+    protected function _getIconDoReport($bPerformed)
+    {
+    	return 'exclamation-circle';
+    }
+
+    protected function _getTitleDoReport($bPerformed)
+    {
+    	return '_report_do_report';
+    }
+
+	protected function _getFormObject()
+    {
+        return BxDolForm::getObjectInstance($this->_sFormObject, $this->_sFormDisplayPost);
+    }
+}
+
+/** @} */

+ 90 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolRss.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    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * This class unifies the usage of RSS feeds in the script.
+ *
+ *
+ * Usage.
+ *
+ * Step 1:
+ * Add record to sys_objects_rss table, like you are doing this for Comments or Voting objects:
+ * - object: your rss object name, usually it is in the following format - vendor prefix, underscore, module prefix;
+ * - class_name: user defined class name which is derived from BxTemplRss.
+ * - class_file: the location of the user defined class, leave it empty if class is located in system folders.
+ * 
+ * Step 2:
+ * Write a code for getUrl method in your class (MyModuleRss) which is derived from BxTemplRss.
+ * @code
+ * public function getUrl($mixedId) {
+ * 		if(!isset($this->aFeeds[$mixedId]))
+ * 			return false;
+ * 
+ * 		return $this->aFeeds[$mixedId];
+ * }
+ * @endcode
+ * 
+ * Step 2:
+ * Get an RSS placeholder to display somewhere on a page
+ * @code
+ *  BxDolRss::getObjectInstance('my_module_rss_object')->getHolder($mixedRssId, $iRssNum);
+ * @endcode
+ *
+ */
+
+class BxDolRss extends BxDolFactory implements iBxDolFactoryObject
+{
+	protected $_oDb;
+	protected $_sObject;
+    protected $_aObject;
+
+    public static $bInitialized = false;
+
+    /**
+     * Constructor
+     */
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+
+        $this->_aObject = $aObject;
+        $this->_sObject = $aObject['object'];
+
+        $this->_oDb = new BxDolRssQuery($this->_aObject);
+    }
+
+   /**
+     * Get rss object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    public static function getObjectInstance($sObject)
+    {
+        if(isset($GLOBALS['bxDolClasses']['BxDolRss!' . $sObject]))
+            return $GLOBALS['bxDolClasses']['BxDolRss!' . $sObject];
+
+        $aObject = BxDolRssQuery::getRssObject($sObject);
+        if(!$aObject || !is_array($aObject))
+            return false;
+
+        $sClass = 'BxTemplRss';
+        if(!empty($aObject['class_name'])) {
+            $sClass = $aObject['class_name'];
+            if(!empty($aObject['class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aObject['class_file']);
+        }
+
+        $o = new $sClass($aObject);
+        return ($GLOBALS['bxDolClasses']['BxDolRss!' . $sObject] = $o);
+    }
+
+    public function getUrl($mixedId) {}
+}
+
+/** @} */

+ 877 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolSearch.php

@@ -0,0 +1,877 @@
+<?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 $_bLiveSearch = false;
+    protected $_sMetaType = '';
+    protected $_sCategoryObject = '';
+
+    /**
+     * 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 = '';
+        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();
+            $oEx->setId($aValue['id']);
+            $oEx->setLiveSearch($this->_bLiveSearch);
+            $oEx->setMetaType($this->_sMetaType);
+            $oEx->setCategoryObject($this->_sCategoryObject);
+            $oEx->setCenterContentUnitSelector(false);
+            $sCode .= $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;
+    }
+}
+
+/*
+ * 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 $_bLiveSearch = false;
+    protected $_sMetaType = '';
+    protected $_sCategoryObject = '';
+
+    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 setLiveSearch($bLiveSearch)
+    {
+        $this->_bLiveSearch = $bLiveSearch;
+    }
+
+    public function setMetaType($s)
+    {
+        $this->_sMetaType = $s;
+    }
+
+    public function setCategoryObject($s)
+    {
+        $this->_sCategoryObject = $s;
+    }
+
+    /**
+     * 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 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(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(bx_get('state')));
+                        break;
+                    case 'location_country_city':
+                        $o->locationsSetSearchCondition($this, $sKeyword, bx_process_input(bx_get('state')), bx_process_input(bx_get('city')));
+                        break;
+                    case 'mention':
+                        // TODO:
+                        break;
+                    case 'keyword':
+                        $o->keywordsSetSearchCondition($this, $sKeyword);
+                        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 ()
+    {
+        $this->aCurrent['paginate']['perPage'] = (isset($_GET['per_page']) && (int)$_GET['per_page'] != 0) ? (int)$_GET['per_page'] : $this->aCurrent['paginate']['perPage'];
+        if (empty($this->aCurrent['paginate']['perPage']))
+            $this->aCurrent['paginate']['perPage'] = BX_DOL_SEARCH_RESULTS_PER_PAGE_DEFAULT;
+
+        $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);
+    }
+}
+
+/** @} */

+ 108 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolSearchExtended.php

@@ -0,0 +1,108 @@
+<?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 BxDolSearchExtended extends BxDolFactory implements iBxDolFactoryObject
+{
+    public static $SEARCHABLE_TYPES = array(
+        'text', 'textarea', 'number', 
+    	'select', 'radio_set', 
+    	'checkbox', 'switcher'
+    );
+
+    public static $TYPE_TO_TYPE_SEARCH = array(
+        'text' => array('text'),
+        'textarea' => array('text'),
+        'number' => array('text'),
+        'text_auto' => array('text_auto'),
+        'select' => array('checkbox_set', 'select_multiple', 'select'),
+        'radio_set' => array('checkbox_set', 'select_multiple', 'select'),
+        'checkbox' => array('checkbox', 'switcher'),
+    	'switcher' => array('checkbox', 'switcher'),
+    );
+
+    public static $TYPE_TO_OPERATOR = array(
+    	'text' => array('like', '='),
+        'textarea' => array('like', '='),
+        'number' => array('='),
+        'text_auto' => array('in'),
+        'select' => array('in'),
+        'radio_set' => array('in'),
+        'checkbox' => array('='),
+    	'switcher' => array('='),
+    );
+
+    protected $_oDb;
+
+    protected $_sObject;
+    protected $_aObject;
+
+
+    /**
+     * Constructor
+     * @param $aObject array of search options
+     */
+    protected function __construct($aObject)
+    {
+        parent::__construct();
+
+        $this->_oDb = new BxDolSearchExtendedQuery($this->_aObject);
+
+        $this->_sObject = $aObject['object'];
+        $this->_aObject = $aObject;
+    }
+
+    /**
+     * Get editor object instance by object name
+     * @param $sObject object name
+     * @return object instance or false on error
+     */
+    static public function getObjectInstance($sObject, $oTemplate = false)
+    {
+        if(isset($GLOBALS['bxDolClasses']['BxDolSearchExtended!' . $sObject]))
+            return $GLOBALS['bxDolClasses']['BxDolSearchExtended!' . $sObject];
+
+        $aObject = BxDolSearchExtendedQuery::getSearchObject($sObject);
+        if(!$aObject || !is_array($aObject))
+            return false;
+
+        $sClass = 'BxTemplSearchExtended';
+        if(!empty($aObject['class_name'])) {
+            $sClass = $aObject['class_name'];
+            if(!empty($aObject['class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aObject['class_file']);
+        }
+
+        $o = new $sClass($aObject, $oTemplate);
+        return ($GLOBALS['bxDolClasses']['BxDolSearchExtended!' . $sObject] = $o);
+    }
+
+    static public function actionGetAuthors()
+    {
+        $aResult = BxDolService::call('system', 'profiles_search', array(bx_get('term')), 'TemplServiceProfiles');
+
+        header('Content-Type:text/javascript; charset=utf-8');
+        echo json_encode($aResult);
+    }
+
+    public function isEnabled()
+    {
+        return isset($this->_aObject['active']) && (int)$this->_aObject['active'] != 0;
+    }
+
+    public function reset()
+    {
+        if($this->_oDb->deleteFields(array('object' => $this->_sObject)) === false)
+            return false;
+
+        $this->_aObject['fields'] = BxDolSearchExtendedQuery::getSearchFields($this->_aObject);
+        return true;
+    }
+}
+
+/** @} */

+ 89 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolSearchExtendedQuery.php

@@ -0,0 +1,89 @@
+<?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 BxDolSearchExtendedQuery extends BxDolDb
+{
+    public function __construct($aObject = array())
+    {
+        parent::__construct();
+    }
+
+    static public function getSearchObject($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_search_extended` WHERE `object` = ?", $sObject);
+
+        $aObject = $oDb->getRow($sQuery);
+        if(!$aObject || !is_array($aObject))
+            return false;
+
+        $aObject['fields'] = self::getSearchFields($aObject);
+        return $aObject;
+    }
+
+    static public function getSearchFields($aObject)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $sQueryFields = "SELECT * FROM `sys_search_extended_fields` WHERE `object` = :object ORDER BY `order`";
+        $aQueryFieldsBindings = array('object' => $aObject['object']);
+        $aFields = $oDb->getAll($sQueryFields, $aQueryFieldsBindings);
+
+        //--- Get fields
+        if(empty($aFields) || !is_array($aFields)) {
+            $aFields = BxDolContentInfo::getObjectInstance($aObject['object_content_info'])->getSearchableFieldsExtended();
+            if(empty($aFields) || !is_array($aFields))
+                return array();
+
+            $iOrder = 0;
+            foreach($aFields as $sField => $aField) 
+                $oDb->query("INSERT INTO `sys_search_extended_fields`(`object`, `name`, `type`, `caption`, `values`, `search_type`, `search_operator`, `active`, `order`) VALUES(:object, :name, :type, :caption, :values, :search_type, :search_operator, '1', :order)", array(
+                    'object' => $aObject['object'], 
+                    'name' => $sField,
+                    'type' => $aField['type'],
+                    'caption' => $aField['caption'],
+                    'values' => $aField['values'],
+                    'search_type' => reset(BxDolSearchExtended::$TYPE_TO_TYPE_SEARCH[$aField['type']]), 
+                    'search_operator' => reset(BxDolSearchExtended::$TYPE_TO_OPERATOR[$aField['type']]),
+                    'order' => $iOrder++
+                ));
+
+            $aFields = $oDb->getAll($sQueryFields, $aQueryFieldsBindings);
+        }
+
+        //--- Process fields
+        foreach ($aFields as $iIndex => $aField) {
+            //--- Process field Values            
+            if(!empty($aField['values'])) {
+                if(strncmp(BX_DATA_LISTS_KEY_PREFIX, $aField['values'], 2) === 0) {
+                    $sList = trim($aField['values'], BX_DATA_LISTS_KEY_PREFIX . ' ');
+                    $aFields[$iIndex] = array_merge($aField, array(
+                    	'values' => BxDolFormQuery::getDataItems($sList),
+                    	'values_list_name' => $sList,
+                    ));
+                }
+                else
+                    $aFields[$iIndex]['values'] = unserialize($aField['values']);
+            }
+        }
+
+        return $aFields;
+    }
+
+    public function deleteFields($aWhere)
+    {
+        if(empty($aWhere))
+    		return false;
+
+        return $this->query("DELETE FROM `sys_search_extended_fields` WHERE " . $this->arrayToSQL($aWhere, ' AND '));
+    }
+}
+
+/** @} */

+ 196 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolSession.php

@@ -0,0 +1,196 @@
+<?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_SESSION_LIFETIME', 3600);
+define('BX_DOL_SESSION_COOKIE', 'memberSession');
+
+class BxDolSession extends BxDolFactory implements iBxDolSingleton
+{
+    protected $oDb;
+    protected $sId;
+    protected $iUserId;
+    protected $aData;
+
+    protected $bAutoLogout;	//--- Auto logout with broken session
+
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+
+        $this->oDb = new BxDolSessionQuery();
+        $this->sId = '';
+        $this->iUserId = 0;
+        $this->aData = array();
+
+        $this->bAutoLogout = false; // when changing to 'true', it's better to increase BX_DOL_SESSION_LIFETIME to 1 month or so
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolSession();
+
+        if(!$GLOBALS['bxDolClasses'][__CLASS__]->getId())
+            $GLOBALS['bxDolClasses'][__CLASS__]->start();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    function start()
+    {
+        if (defined('BX_DOL_CRON_EXECUTE'))
+            return true;
+
+        if ($this->exists($this->sId)) {
+            if ($this->iUserId == getLoggedId())
+                return true;
+            $this->destroy(false);
+        }
+
+		/**
+		 * Force logout a logged in user if his session wasn't found and required to be automatically recreated.
+		 * It's needed to avoid problems when different users are logged in at the same time under one account. 
+		 */
+		if($this->bAutoLogout && isLogged())
+			bx_logout();
+
+        $this->sId = genRndPwd(32, true);
+
+        $aUrl = parse_url(BX_DOL_URL_ROOT);
+        $sPath = isset($aUrl['path']) && !empty($aUrl['path']) ? $aUrl['path'] : '/';
+        setcookie(BX_DOL_SESSION_COOKIE, $this->sId, time() + BX_DOL_SESSION_LIFETIME, $sPath, '', false, true);
+
+        $this->save();
+        return true;
+    }
+
+    function destroy($bDeleteCookies = true)
+    {
+        if ($bDeleteCookies) {
+            $aUrl = parse_url(BX_DOL_URL_ROOT);
+            $sPath = isset($aUrl['path']) && !empty($aUrl['path']) ? $aUrl['path'] : '/';
+            setcookie(BX_DOL_SESSION_COOKIE, '', time() - 86400, $sPath, '', false, true);
+            unset($_COOKIE[BX_DOL_SESSION_COOKIE]);
+        }
+
+        $this->oDb->delete($this->sId);
+
+        $this->sId = '';
+        $this->iUserId = 0;
+        $this->aData = array();
+    }
+
+    function exists($sId = '')
+    {
+        if(empty($sId) && isset($_COOKIE[BX_DOL_SESSION_COOKIE]))
+            $sId = bx_process_input($_COOKIE[BX_DOL_SESSION_COOKIE]);
+
+        $mixedSession = $this->oDb->exists($sId);
+        if($mixedSession === false) 
+        	return false;
+
+		$this->sId = $mixedSession['id'];
+		$this->iUserId = (int)$mixedSession['user_id'];
+		$this->aData = unserialize($mixedSession['data']);
+
+		$this->oDb->update($this->sId);		//--- update session's time
+		return true;
+    }
+
+    function getId()
+    {
+        return $this->sId;
+    }
+
+    function setUserId($iUserId)
+    {
+        $this->iUserId = $iUserId;
+        $this->save();
+    }
+
+    function setValue($sKey, $mixedValue)
+    {
+        if(empty($this->sId))
+            $this->start();
+
+        $this->aData[$sKey] = $mixedValue;
+        $this->save();
+    }
+
+    function unsetValue($sKey)
+    {
+        if(empty($this->sId))
+            $this->start();
+
+        unset($this->aData[$sKey]);
+
+        if(!empty($this->aData))
+            $this->save();
+        else
+            $this->destroy();
+    }
+
+	function isValue($sKey)
+    {
+        if(empty($this->sId))
+            $this->start();
+
+        return isset($this->aData[$sKey]);
+    }
+
+    function getValue($sKey)
+    {
+        if(empty($this->sId))
+            $this->start();
+
+        return isset($this->aData[$sKey]) ? $this->aData[$sKey] : false;
+    }
+
+    function getUnsetValue($sKey)
+    {
+        $mixedValue = $this->getValue($sKey);
+        if($mixedValue !== false)
+            $this->unsetValue($sKey);
+
+        return $mixedValue;
+    }
+
+    public function maintenance()
+    {
+        return $this->oDb->deleteExpired();
+    }
+
+    protected function save()
+    {
+        if($this->iUserId == 0)
+            $this->iUserId = getLoggedId();
+
+        $this->oDb->save($this->sId, array(
+            'user_id' => $this->iUserId,
+            'data' => serialize($this->aData)
+        ));
+    }
+}
+
+/** @} */

+ 60 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolSessionQuery.php

@@ -0,0 +1,60 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * @see BxDolSession
+ */
+class BxDolSessionQuery extends BxDolDb
+{
+    protected $sTable;
+
+    public function __construct()
+    {
+        parent::__construct();
+
+        $this->sTable = 'sys_sessions';
+    }
+    function getTableName()
+    {
+        return $this->sTable;
+    }
+    function exists($sId)
+    {
+        $sSql = $this->prepare("SELECT `id`, `user_id`, `data` FROM `" . $this->sTable . "` WHERE `id`=? LIMIT 1", $sId);
+        $aSession = $this->getRow($sSql);
+        return !empty($aSession) ? $aSession : false;
+    }
+    function save($sId, $aSet)
+    {
+    	$aSet['id'] = $sId;
+        return (int)$this->query("REPLACE INTO `" . $this->sTable . "` SET " . $this->arrayToSQL($aSet) . ", `date`=UNIX_TIMESTAMP()") > 0;
+    }
+	function update($sId, $aSet = array())
+    {
+    	$sSet = !empty($aSet) ? $this->arrayToSQL($aSet) . ", " : "";
+    	$sSet .= "`date`=UNIX_TIMESTAMP()";
+
+        return (int)$this->query("UPDATE `" . $this->sTable . "` SET " . $sSet . " WHERE `id`=:id", array('id' => $sId)) > 0;
+    }
+    function delete($sId)
+    {
+        $sSql = $this->prepare("DELETE FROM `" . $this->sTable . "` WHERE `id`=? LIMIT 1", $sId);
+        return (int)$this->query($sSql) > 0;
+    }
+    function deleteExpired()
+    {
+        $sSql = $this->prepare("DELETE FROM `" . $this->sTable . "` WHERE `date` < (UNIX_TIMESTAMP() - ?)", BX_DOL_SESSION_LIFETIME);
+        $iRet = (int)$this->query($sSql);
+        if ($iRet)
+            $this->query("OPTIMIZE TABLE `" . $this->sTable . "`");
+        return $iRet;
+    }
+}
+
+/** @} */

+ 1405 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolStorage.php

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

+ 223 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolStorageS3.php

@@ -0,0 +1,223 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+if (!defined('CURL_SSLVERSION_TLSv1'))
+    define('CURL_SSLVERSION_TLSv1', 1);
+
+require_once(BX_DIRECTORY_PATH_PLUGINS . 'amazon-s3/S3.php');
+
+/**
+ * File storage in Amazon S3.
+ * @see BxDolStorage
+ */
+class BxDolStorageS3 extends BxDolStorage
+{
+    protected $_s3;
+    protected $_sBucket;
+    protected $_sDomain;
+    protected $_bSSL;
+    protected $_bReducedRedundancy;
+
+    /**
+     * constructor
+     */
+    public function __construct($aObject)
+    {
+        parent::__construct($aObject);
+
+        $sAccessKey = getParam('sys_storage_s3_access_key');
+        $sSecretKey = getParam('sys_storage_s3_secret_key');
+        $this->_s3 = new S3($sAccessKey, $sSecretKey);
+        $this->_sBucket = getParam('sys_storage_s3_bucket');
+        $this->_sDomain = getParam('sys_storage_s3_domain');
+
+        $this->_bSSL = (isset($this->_aParams['ssl']) && $this->_aParams['ssl']) || (!isset($this->_aParams['ssl']) && strncmp(BX_DOL_URL_ROOT, 'https://', 8) === 0) ? true : false;
+        $this->_bReducedRedundancy = isset($this->_aParams['reduced_redundancy']) && $this->_aParams['reduced_redundancy'] ? true : false;
+    }
+
+    /**
+     * Get file url.
+     * @param $iFileId file
+     * @return file url
+     */
+    public function getFileUrlById($iFileId)
+    {
+        $aFile = $this->_oDb->getFileById($iFileId);
+        if (!$aFile)
+            return false;
+
+        if ($aFile['private']) {
+            $sFileLocation = $this->getObjectBaseDir($aFile['private']) . $aFile['path'];
+
+            if ($this->_sDomain)
+                return $this->_s3->getAuthenticatedURL($this->_sDomain, $sFileLocation, $this->_aObject['token_life'], true, $this->_bSSL);
+            else
+                return $this->_s3->getAuthenticatedURL($this->_sBucket, $sFileLocation, $this->_aObject['token_life'], false, $this->_bSSL);
+        }
+
+        return $this->getObjectBaseUrl($aFile['private']) . $aFile['path'];
+    }
+
+    /**
+     * Start file fownloading by remote id. If file is private then token is checked.
+     */
+    public function download ($sRemoteId, $sToken = false)
+    {
+        $this->setErrorCode(BX_DOL_STORAGE_ERR_OK);
+
+        $aFile = $this->_oDb->getFileByRemoteId($sRemoteId);
+        if (!$aFile) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_FILE_NOT_FOUND);
+            return false;
+        }
+
+        if ($aFile['private'] && !$this->_oDb->isTokenValid($aFile['id'], $sToken)) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_PERMISSION_DENIED);
+            return false;
+        }
+
+        $sUrl = $this->getFileUrlById($aFile['id']);
+        header("Location: " . $sUrl);
+
+        return parent::download($aFile);
+    }
+    
+    /**
+     * Set file private or public.
+     */
+    public function setFilePrivate($iFileId, $isPrivate = true)
+    {
+        $aFile = $this->_oDb->getFileById($iFileId);
+        $sFileLocation = $this->getObjectBaseDir($aFile['private']) . $aFile['path'];
+        if (($aACP = $this->_s3->getAccessControlPolicy($this->_sBucket, $sFileLocation)) === false) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_ENGINE_GET);
+            return false;
+        }
+
+        if (!is_array($aACP['acl']) || !$aACP['acl']) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_ENGINE_GET);
+            return false;
+        }
+
+        // check current permissions
+        $aNewACP = $aACP;
+        unset($aNewACP['acl']);
+        $aNewACP['acl'] = array();
+        $aGroupPublic = false;
+        $aGroupPrivate = false;
+        foreach ($aACP['acl'] as $r) {
+            if ('Group' == $r['type']) {
+                if (isset($r['uri']) && $r['uri'] == 'http://acs.amazonaws.com/groups/global/AllUsers')
+                    $aGroupPublic = $r;
+                elseif (isset($r['uri']) && $r['uri'] == 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers')
+                    $aGroupPrivate = $r;
+                else
+                    $aNewACP['acl'][] = $r;
+            } else {
+                $aNewACP['acl'][] = $r;
+            }
+        }
+
+        // determine permissions changing
+
+        $aGroupAdd = false;
+
+        if ($isPrivate && (!$aGroupPrivate || $aGroupPublic)) {
+
+            // make private
+            $aGroupAdd = array (
+                    'type' => 'Group',
+                    'uri' => 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers',
+                    'permission' => 'READ',
+                );
+
+        } elseif (!$isPrivate && ($aGroupPrivate || !$aGroupPublic)) {
+
+            // make public
+            $aGroupAdd = array (
+                    'type' => 'Group',
+                    'uri' => 'http://acs.amazonaws.com/groups/global/AllUsers',
+                    'permission' => 'READ',
+                );
+
+        }
+
+        // change permission if necessary
+
+        if ($aGroupAdd) {
+            $aNewACP['acl'][] = $aGroupAdd;
+            if (!$this->_s3->setAccessControlPolicy($this->_sBucket, $sFileLocation, $aNewACP)) {
+                $this->setErrorCode(BX_DOL_STORAGE_ERR_ENGINE_GET);
+                return false;
+            }
+        }
+
+        return parent::setFilePrivate($iFileId, $isPrivate);
+    }
+
+    // ----------------
+
+    protected function addFileToEngine($sTmpFile, $sLocalId, $sName, $isPrivate, $iProfileId)
+    {
+        $sMimeType = $this->getMimeTypeByFileName($sName);
+        $sExt = $this->getFileExt($sName);
+        $sPath = $this->genPath($sLocalId, $this->_aObject['levels']);
+        $sRemoteNamePath = $sPath . $sLocalId . ($sExt ? '.' . $sExt : '');
+
+        $aMetaHeaders = array();
+        $aRequestHeaders = array (
+            "Content-Type"  => $sMimeType,
+        );
+        if ($this->_iCacheControl > 0) {
+            $aRequestHeaders = array_merge ($aRequestHeaders, array (
+                "Cache-Control" => "max-age=" . ($isPrivate && $this->_iCacheControl > $this->_aObject['token_life'] ? $this->_aObject['token_life'] : $this->_iCacheControl),
+            ));
+        }
+
+        $sStorageClass = $this->_bReducedRedundancy ? S3::STORAGE_CLASS_RRS : S3::STORAGE_CLASS_STANDARD;
+        $sACL = $isPrivate ? S3::ACL_AUTHENTICATED_READ : S3::ACL_PUBLIC_READ;
+        $aInputFile = $this->_s3->inputFile($sTmpFile);
+        if (!$this->_s3->putObject($aInputFile, $this->_sBucket, $this->getObjectBaseDir($isPrivate) . $sRemoteNamePath, $sACL, $aMetaHeaders, $aRequestHeaders, $sStorageClass)) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_ENGINE_ADD);
+            return false;
+        }
+
+        return true;
+    }
+
+    protected function deleteFileFromEngine($sFilePath, $isPrivate)
+    {
+        $sFileLocation = $this->getObjectBaseDir($isPrivate) . $sFilePath;
+
+        if (!$this->_s3->deleteObject($this->_sBucket, $sFileLocation)) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_UNLINK);
+            return false;
+        }
+
+        return true;
+    }
+
+    protected function genRemoteNamePath ($sPath, $sLocalId, $sExt)
+    {
+        return $sPath . $sLocalId . ($sExt ? '.' . $sExt : '');
+    }
+
+    protected function getObjectBaseDir ($isPrivate = false)
+    {
+        return $this->_aObject['object'] . '/';
+    }
+
+    protected function getObjectBaseUrl ($isPrivate = false)
+    {
+        $sProto = $this->_bSSL ? 'https://' : 'http://';
+        return $sProto . $this->_sBucket . '.s3.amazonaws.com/' . $this->getObjectBaseDir($isPrivate);
+    }
+}
+
+/** @} */

+ 2454 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolTemplate.php

@@ -0,0 +1,2454 @@
+<?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_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_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
+{
+    /**
+     * Main fields
+     */
+    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 $_oConfigTemplate;
+
+    /**
+     * 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 = 'skin';
+
+        $sCode = getParam('template');
+        if(empty($sCode))
+            $sCode = BX_DOL_TEMPLATE_DEFAULT_CODE;
+        $this->_checkCode($sCode, false);
+
+        //--- Check selected template in COOKIE(the lowest priority) ---//
+        $sCode = !empty($_COOKIE[$this->_sCodeKey]) ? $_COOKIE[$this->_sCodeKey] : '';
+        $this->_checkCode($sCode, false);
+
+        //--- Check selected template in GET(the highest priority) ---//
+        $sCode = !empty($_GET[$this->_sCodeKey]) ? $_GET[$this->_sCodeKey] : '';
+        $this->_checkCode($sCode, true);
+
+        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();
+
+        $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__];
+    }
+
+    /**
+     * 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()
+    {
+        //--- 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',
+        ));
+
+        //--- 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',
+        ));
+
+        //--- Load Embed.ly if Key and Pattern are available ---//
+        $bEmbedly = !empty(getParam('sys_embedly_api_key')) && !empty(getParam('sys_embedly_api_pattern'));
+        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->_oConfigTemplate = BxTemplConfig::getInstance();
+    }
+
+    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 code.
+     *
+     * @return string template's code.
+     */
+    function getCode()
+    {
+        return $this->_sCode;
+    }
+
+    /**
+     * Check whether code is associated with active template.
+     *
+     * @param string  $sCode      template's unique URI.
+     * @param boolean $bSetCookie save code in COOKIE or not.
+     */
+    protected function _checkCode($sCode, $bSetCookie)
+    {
+        if(empty($sCode) || !preg_match('/^[A-Za-z0-9_-]+$/', $sCode))
+            return;
+
+        $aModule = BxDolModuleQuery::getInstance()->getModuleByUri($sCode);
+        if(empty($aModule) || !is_array($aModule) || (int)$aModule['enabled'] != 1 || !file_exists($this->_sRootPath . 'modules/' . $aModule['path'] . 'data/template/'))
+            return;
+
+        $oConfig = new BxDolModuleConfig($aModule);
+
+        $this->_sCode = $oConfig->getUri();
+        $this->_sSubPath = $oConfig->getDirectory();
+
+        if(!$bSetCookie || bx_get('preview'))
+            return;
+
+        $aUrl = parse_url(BX_DOL_URL_ROOT);
+        $sPath = isset($aUrl['path']) && !empty($aUrl['path']) ? $aUrl['path'] : '/';
+
+        setcookie( $this->_sCodeKey, $this->_sCode, time() + 60*60*24*365, $sPath);
+    }
+
+    /**
+     * 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));
+    }
+
+    /**
+     * 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;
+        }
+    }
+    /**
+     * 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'] . '" />';
+        }
+
+        $sRet .= BxTemplFunctions::getInstance()->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 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 != "") {
+        	$bAlt = isset($aParams['alt']) && !empty($aParams['alt']);
+            $bClass = isset($aParams['class']) && !empty($aParams['class']);
+
+            $aAttrs = array();
+            if($bClass)
+            	$aAttrs[] = array('key' => 'class', 'value' => $aParams['class']);
+			if($bAlt)
+				$aAttrs[] = array('key' => 'alt', 'value' => $aParams['alt']);
+
+            return $this->parseHtmlByName('bx_img.html', array(
+                'bx_if:class' => array(
+                    'condition' => $bClass,
+                    'content' => array(
+                        'content' => $bClass ? $aParams['class'] : ''
+                    )
+                ),
+                'bx_repeat:attrs' => $aAttrs,
+                'src' => $sUrl,
+                'alt' => $bAlt ? $aParams['alt'] : ''
+            ));
+        }
+
+        //--- Use iconic font.
+        return $this->parseHtmlByName('bx_icon.html', array(
+            'name' => $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_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 'popup_loading':
+                $s = $this->parsePageByName('popup_loading.html', array());
+                $sRet = BxTemplFunctions::getInstance()->transBox('bx-popup-loading', $s, true);
+
+                $oSearch = new BxTemplSearch();
+                $oSearch->setLiveSearch(true);
+                $sRet .= $this->parsePageByName('search.html', array(
+                    'search_form' => $oSearch->getForm(BX_DB_CONTENT_ONLY),
+                    'results' => $oSearch->getResultsContainer(),
+                ));
+
+                $sRet .= $this->getMenu ('sys_site');                
+                $sRet .= isLogged() ? $this->getMenu ('sys_add_content') : '';
+                $sRet .= isLogged() ? $this->getMenu ('sys_account_popup') : '';
+                break;
+            case 'lang':
+                $sRet = bx_lang_name();
+                break;
+            case 'main_logo':
+                $sRet = BxTemplFunctions::getInstance()->getMainLogo();
+                break;
+            case 'informer':
+                $oInformer = BxDolInformer::getInstance($this);
+                $sRet = $oInformer ? $oInformer->display() : '';
+                break;
+            case 'cover':
+            	$oCover = BxDolCover::getInstance($this);
+            	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;
+    }
+    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->_oConfigTemplate->aLessConfig);
+
+            return array('url' => $this->_sCachePublicFolderUrl . $sFile, 'path' => $this->_sCachePublicFolderPath . $sFile);
+        }
+
+        $oLess = new Less_Parser();
+        $oLess->ModifyVars($this->_oConfigTemplate->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/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)
+    {
+        $aKeys = array_keys($aVariables);
+        $aValues = array_values($aVariables);
+
+        $aKeyWrappers = $this->_getKeyWrappers($mixedKeyWrapperHtml);
+
+        $iCountKeys = count($aKeys);
+        for ($i = 0; $i < $iCountKeys; $i++) {
+            if (strncmp($aKeys[$i], 'bx_repeat:', 10) === 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]))
+                        foreach($aValues[$i] as $aValue)
+                            $sValue .= $this->parseHtmlByContent($aMatches[1], $aValue, $mixedKeyWrapperHtml);
+                    else if(is_string($aValues[$i]))
+                        $sValue = $aValues[$i];
+                }
+            } 
+            else if (strncmp($aKeys[$i], 'bx_if:', 6) === 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']) && isset($aValues[$i]['condition']) && $aValues[$i]['condition'])
+                        $sValue .= $this->parseHtmlByContent($aMatches[1], $aValues[$i]['content'], $mixedKeyWrapperHtml);
+            } 
+            else {
+                $sKey = "'" . $aKeyWrappers['left'] . $aKeys[$i] . $aKeyWrappers['right'] . "'s";
+                $sValue = $aValues[$i];
+            }
+
+            $aKeys[$i] = $sKey;
+            $aValues[$i] = 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 ($sTitle);
+        $oTemplate->setPageContent ('page_main_code', DesignBoxContent($sTitle, 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;
+
+        header( 'Content-type: text/html; charset=utf-8' );
+        header( 'X-Frame-Options: sameorigin' );
+        echo $oTemplate->parsePageByName('page_' . $oTemplate->getPageNameIndex() . '.html', $oTemplate->getPageContent());
+    }
+}
+
+/** @} */

+ 164 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolTranscoderImage.php

@@ -0,0 +1,164 @@
+<?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
+ * @{
+ */
+
+/**
+ * This class transcodes images on the fly.
+ * Transcoded image is saved in the specified storage engine and next time ready image is served.
+ *
+ *
+ * To add image transcoder object add record to 'sys_objects_transcoder' table:
+ * - object - name of the transcoder object, in the format: vendor prefix, underscore, module prefix, underscore, image size name; for example: bx_images_thumb.
+ * - storage_object - name of the storage object to store transcoded data, the specified storage object need to be created too, @see BxDolStorage.
+ * - source_type - type of the source, where is source image is taken from, available options 'Storage' and 'Folder' for now.
+ * - source_params - source_type params, each source_type can have own set of params, please read futher for more info about particular source_types, serialized array of params is stored here.
+ * - private - how to store transcoded data:
+ *      - no - store transcoded data publicly.
+ *      - yes - store transcoded data privately.
+ *      - auto - detect automatically, not supported for 'Folder' source type.
+ * - atime_tracking - track last access time to the transcoded data, allowed values 0 - disables or 1 - enabled.
+ * - atime_pruning - prune transcoded images by last access time, if last access time of the image is older than atime_pruning seconds - it is deleted, it works when atime_tracking is enabled
+ * - ts - unix timestamp of the last change of transcoder parameters, if transcoded image is older than this value - image is deleted and transcoded again.
+ *
+ *
+ * Then you need to add image filters to 'sys_transcoder_filters' table:
+ * - transcoder_object - name of the transcoded object to apply filter to.
+ * - filter - filter name, please read futher for available filters.
+ * - filter_params - serialized array of filter params, please read futher for particular filters params.
+ * - order - if there are several filters for one object, they will be applied in this order.
+ *
+ *
+ * 'Folder' source types:
+ * This source type is some folder with original images for the transcoding, the identifier of the image (handler) is file name.
+ * The params are the following:
+ * - path - path to the folder with original images
+ * This source type has some limitation:
+ * - automatic detection of private files is not supported
+ * - transcoded file is not automaticlaly deleted/renewed if original file is changed
+ *
+ *
+ * 'Storage' source type:
+ * The source of original files is Storage engine, the identifier of the image (handler) is file id.
+ * The params are the following:
+ * - object - name of the Storage object
+ *
+ *
+ * Available filters:
+ * - Resize - this filter resizes original image, the parameters are the following:
+ *     - w - width of resulted image.
+ *     - h - height of resulted image.
+ *     - square_resize - make resulted image square, even of original image is not square, 'w' and 'h' parameters must be the same.
+ *     - crop_resize - crop image to destination size with filling whole area of destination size.
+ *     - force_type - always change type of the image to the specified type: jpg, png, gif.
+ * - Grayscale - make image grayscale, there is no parameters for this filter
+ *
+ *
+ * Automatic deletetion of associated data is supported - in the case if original or transcoded file is deleted,
+ * but you need to register alert handlers, just call registerHandlers () function to register handler (for example, during module installation)
+ * and call unregisterHandlers () function to unregister handlers (for example, during module uninstallation)
+ *
+ *
+ * Example of usage:
+ * @code
+ * $oTranscoder = BxDolTranscoderImage::getObjectInstance('bx_images_thumb'); // change images transcode object name to your own
+ * $oTranscoder->registerHandlers(); // make sure to call it only once! before the first usage, no need to call it every time
+ * $sTranscodedImageUrl = $oTranscoder->getFileUrl('my_dog.jpg'); // the name of file, in the case of 'Folder' storage type this is file name
+ * echo 'My dog : <img src="' . $sUrl . '" />'; // transcoded(resized and/or grayscaled) image will be shown, according to the specified filters
+ * @endcode
+ *
+ */
+class BxDolTranscoderImage extends BxDolTranscoder implements iBxDolFactoryObject
+{
+    protected function __construct($aObject, $oStorage)
+    {
+        parent::__construct($aObject, $oStorage);
+        $this->_oDb = new BxDolTranscoderImageQuery($aObject, false);
+        $this->_sQueueTable = $this->_oDb->getQueueTable();
+    }
+
+    /**
+     * Get file url when file isn't transcoded yet
+     */
+    public function getFileUrlNotReady($mixedHandler)
+    {
+        return BX_DOL_URL_ROOT . 'image_transcoder.php?o=' . $this->_aObject['object'] . '&h=' . $mixedHandler . '&dpx=' . $this->getDevicePixelRatio() . '&t=' . time();
+    }
+
+    public function isFileReady ($mixedHandler, $isCheckOutdated = true)
+    {
+        if (isAdmin() && false !== $this->getFilterParams('ResizeVar')) { // only operators can apply new image size
+            $aTranscodedFileData = $this->_oDb->getTranscodedFileData ($mixedHandler);
+            $x = $this->getCustomResizeDimension ('x');
+            $y = $this->getCustomResizeDimension ('y');
+
+            // if new sizes are provided - delete old image, so new one will be created
+            if (($x && (!isset($aTranscodedFileData['x']) || $x != $aTranscodedFileData['x'])) || ($y && (!isset($aTranscodedFileData['y']) || $y != $aTranscodedFileData['y']))) {                
+                if (!($iFileId = $this->_oDb->getFileIdByHandler($mixedHandler)))
+                    return false;
+
+                if (!($aFile = $this->_oStorage->getFile($iFileId)))
+                    return false;
+
+                if (!$this->_oStorage->deleteFile($aFile['id']))
+                    return false;
+            }
+        }
+        return parent::isFileReady ($mixedHandler, $isCheckOutdated);
+    }
+
+    protected function getCustomResizeDimension ($sName)
+    {
+        $i = (int)bx_get($sName);
+        if ($i > 2048) $i = 2048;
+        if ($i && $i < 16) $i = 16;
+        return $i;
+    }
+
+    public function transcode ($mixedHandler, $iProfileId = 0)
+    {
+        if (!($bRet = parent::transcode ($mixedHandler, $iProfileId)))
+            return $bRet;
+
+        $x = $this->getCustomResizeDimension ('x');
+        $y = $this->getCustomResizeDimension ('y');
+
+        if ($x || $y)
+            $this->_oDb->updateTranscodedFileData($mixedHandler, array('x' => $x, 'y' => $y));
+
+        return $bRet;
+    }
+
+    protected function applyFilter_Grayscale ($sFile, $aParams)
+    {
+        $o = BxDolImageResize::getInstance();
+        $o->removeCropOptions ();
+
+        if (IMAGE_ERROR_SUCCESS == $o->grayscale($sFile))
+            return true;
+
+        return false;
+    }
+
+    protected function applyFilter_ResizeVar ($sFile, $aParams)
+    {
+        $aParams['w'] = $this->getCustomResizeDimension ('x');
+        $aParams['h'] = $this->getCustomResizeDimension ('y');
+
+        if (!$aParams['w'])
+            unset($aParams['w']);
+        if (!$aParams['h'])
+            unset($aParams['h']);
+
+        if (!$aParams['w'] && !$aParams['h'])
+            return true;
+
+        return $this->applyFilter_Resize ($sFile, $aParams);
+    }
+}
+
+/** @} */

+ 68 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolTranscoderProxy.php

@@ -0,0 +1,68 @@
+<?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
+ * @{
+ */
+
+/**
+ * This transcoder can be used as universal transcoder for videos and images.
+ * Depending on original file MIME type it calls the appropriate transcoder.
+ * Proxied transcoders names are specified in 'source_params' array:
+ * - object: original file storage object
+ * - image: image transcoder name
+ * - video_poster: video transcoder name for video poster image 
+ * - video: video transcoders array to force conversion for
+ */
+class BxDolTranscoderProxy extends BxDolTranscoder implements iBxDolFactoryObject
+{
+    protected function __construct($aObject, $oStorage)
+    {
+        parent::__construct($aObject, $oStorage);
+        $this->_oDb = new BxDolTranscoderImageQuery($aObject, false);
+        $this->_sQueueTable = $this->_oDb->getQueueTable();
+    }
+
+    /**
+     * Depending on original file mime type call appropriate transcoder 
+     */
+    public function getFileUrl($mixedHandler)
+    {
+        $oStorageOriginal = BxDolStorage::getObjectInstance($this->_aObject['source_params']['object']);
+        if (!$oStorageOriginal)
+            return false;
+
+        $aFile = $oStorageOriginal->getFile($mixedHandler);
+        if (!$aFile)
+            return false;
+
+        $sTranscoder = '';
+        if (0 === strncmp($aFile['mime_type'], 'image/', 6) && !empty($this->_aObject['source_params']['image'])) {
+            $sTranscoder = $this->_aObject['source_params']['image'];
+        } 
+        elseif (0 === strncmp($aFile['mime_type'], 'video/', 6) && !empty($this->_aObject['source_params']['video_poster'])) {
+            $sTranscoder = $this->_aObject['source_params']['video_poster'];
+
+            // if additional video transcoders provided call it to force video conversion
+            if (!empty($this->_aObject['source_params']['video']))
+                foreach ($this->_aObject['source_params']['video'] as $sVideoTranscoder) {
+                    if (!($oTranscoder = BxDolTranscoderVideo::getObjectInstance($sVideoTranscoder)))
+                        continue;
+                    
+                    $oTranscoder->getFileUrl($mixedHandler);                
+                }
+        }
+    
+        if (!$sTranscoder)
+            return false;
+
+        if (!($oTranscoder = BxDolTranscoderImage::getObjectInstance($sTranscoder)))
+            return false;
+        
+        return $oTranscoder->getFileUrl($mixedHandler);
+    }
+}
+
+/** @} */

+ 265 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolTranscoderVideo.php

@@ -0,0 +1,265 @@
+<?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
+ * @{
+ */
+
+/**
+ * This class transcodes videos and generates video thumbnails.
+ *
+ * To generate video which plays in all moders browsers along with video poster, 
+ * you need to create 3 different video transcoding objects which will generate .mp4, .webm videos and video poster.
+ * Video is converting upon first access, so it is probably better to force video conversion by calling @see BxDolTranscoderVideo::getFileUrl just after video uploading.
+ * Video for conversion is queued and when cron is run video conversion is performed.
+ * While video is pending for conversion or in the process then 
+ * @see BxDolTranscoderVideo::getFileUrl methods returns empty string for video and predefined image for video poster.
+ *
+ * Transcoder object and other params are the same as in @see BxDolTranscoderImage, but it is highly recommended to disable 'atime_pruning' and 'atime_tracking', 
+ * or set it to fairly big value, since video transcoding is not performed on the fly and takes some time. 
+ *
+ * Video conversion can be performed on separate server or multiple servers, to do it:
+ * - install the script on separate server(s), but connect to the same DB which your main site is using
+ * - enable 'sys_transcoder_queue_files' option (when it is enabled it takes a little longer to convert videos)
+ * - add the following code to the begining of inc/header.inc.php file on the main site, where your actual site in installed:
+ *   @code
+ *     define('BX_TRANSCODER_PROCESS_COMPLETED', '');
+ *   @endcode
+ * - if you don't want your main site to convert videos, so all conversion will be performed on the separate server, add the following code to the begining of inc/header.inc.php file on the main site:
+ *   @code
+ *     define('BX_TRANSCODER_NO_TRANSCODING', '');
+ *   @endcode
+ * - all servers must have different host name
+ * - only main server must be used as site, additional sites are just for video conversion, don't perform any action on these sites
+ *
+ *
+ * Available filters:
+ * - Mp4 - this filter convert video into .mp4 format along with resizing, the parameters are the following:
+ *     - h - height of resulted video (360px by default), for video it is highly recommended to specify only height parameter (without width parameter)
+ *     - video_bitrate - video bitrate (512k by default)
+ *     - audio_bitrate - video bitrate (128k by default)
+ *     - ffmpeg_options - additional command line options for ffmepeg, as key => value array (empty by default)
+ * - Webm - this filter convert video into .webm format along with resizing, the parameters are the same as for Mp4 filter
+ * - Poster - this filter generates video thumbnail, it tries to get poster at 0, 3 and 5 seconds from the beginning, it gets first not fully black/white thumb
+ *
+ *
+ * Example of usage:
+ * @code
+ * // transcoder objects which generate .mp4, .webm videos and image poster
+ * $oTranscoderMp4 = BxDolTranscoder::getObjectInstance('bx_video_mp4'); 
+ * $oTranscoderWebm = BxDolTranscoder::getObjectInstance('bx_video_webm');
+ * $oTranscoderPoster = BxDolTranscoder::getObjectInstance('bx_video_poster');
+ *
+ * // make sure to call it only once (for example: during module installation), before the first usage, no need to call it every time
+ * $oTranscoderMp4->registerHandlers(); 
+ * $oTranscoderWebm->registerHandlers(); 
+ * $oTranscoderPoster->registerHandlers(); 
+ *
+ * // get URLs of transcoded videos and video thumbnail, 33 is ID of original video file stored in specified storage object
+ * $sUrlMp4 = $oTranscoderMp4->getFileUrl(33);
+ * $sUrlWebM = $oTranscoderWebm->getFileUrl(33);
+ * $sUrlPoster = $oTranscoderPoster->getFileUrl(33);
+ *
+ * echo 'My cat:' . BxTemplFunctions::getInstance()->videoPlayer($sUrlPoster, $sUrlMP4, $sUrlWebM); 
+ * @endcode
+ *
+ * Also @see transcoder_videos sample for complete example.
+ */
+class BxDolTranscoderVideo extends BxDolTranscoder implements iBxDolFactoryObject
+{
+    protected function __construct($aObject, $oStorage)
+    {
+        parent::__construct($aObject, $oStorage);
+        $this->_sQueueStorage = getParam('sys_transcoder_queue_storage') ? 'sys_transcoder_queue_files' : '';
+        $this->_oDb = new BxDolTranscoderVideoQuery($aObject);
+        $this->_sQueueTable = $this->_oDb->getQueueTable();
+    }
+
+    public function getDevicePixelRatioHandlerSuffix ()
+    {
+        return '';
+    }
+
+    /**
+     * If video isn't processed yet then empty string is returned for video, or predefined image is returned for video poster
+     */
+    public function getFileUrlNotReady($mixedHandler)
+    {
+        return false === $this->getFilterParams('Poster') ? '' : BxDolTemplate::getInstance()->getImageUrl('video-na.png');
+    }
+
+    /**
+     * Convert video to .jpg format - video poster
+     */
+    protected function applyFilter_Poster ($sFile, $aParams)
+    {
+    	$oImage = BxDolImageResize::getInstance();
+
+        $sFileOut = $this->getTmpFilename('.jpg');
+        $bRet = false;
+        $aSeconds = array (0, 3, 5, 0);
+        $sSize = $this->_getOptionSizeForVideo ($sFile, $aParams);
+        foreach ($aSeconds as $iSecond) {
+            $bRet = $this->_convertVideo($sFile, $sFileOut, '.jpg', $aParams, array (
+                's' => $sSize,
+                'ss' => $iSecond,
+                'vframes' => 1,
+                'f' => 'image2',
+                'an' => ' ',
+            ));
+            if (!$bRet)
+                continue;
+
+    		$aRgb = $oImage->getAverageColor($sFileOut);
+		    $fRgb = ($aRgb['r'] + $aRgb['g'] + $aRgb['b']) / 3;
+	    	if ($fRgb > 32 && $fRgb < 224)
+    			break;
+        }
+
+        if (!$bRet) {
+            @unlink($sFileOut);
+            return false;
+        }
+
+        return rename($sFileOut, $sFile);
+    }
+
+    /**
+     * Convert video to .mp4 format
+     */
+    protected function applyFilter_Mp4 ($sFile, $aParams)
+    {
+        return $this->_convertVideo($sFile, $sFile, '.mp4', $aParams, array (
+            'strict' => 'experimental',
+            'vcodec' => 'libx264',
+            's' => $this->_getOptionSizeForVideo ($sFile, $aParams),
+            'b:v' => isset($aParams['video_bitrate']) ? $aParams['video_bitrate'] . 'k' : '512k',
+            'movflags' => '+faststart',
+            'acodec' => 'aac',
+            'ar' => '44100',
+            'b:a' => isset($aParams['audio_bitrate']) ? $aParams['audio_bitrate'] . 'k' : '128k',
+        ));
+    }
+
+    /**
+     * Convert video to .webm format
+     */
+    protected function applyFilter_Webm ($sFile, $aParams)
+    {
+        return $this->_convertVideo($sFile, $sFile, '.webm', $aParams, array (
+            's' => $this->_getOptionSizeForVideo ($sFile, $aParams),
+            'b:v' => isset($aParams['video_bitrate']) ? $aParams['video_bitrate'] . 'k' : '512k',
+            'acodec' => 'libvorbis',
+            'ar' => '44100',
+            'b:a' => isset($aParams['audio_bitrate']) ? $aParams['audio_bitrate'] . 'k' : '128k',
+        ));
+    }
+
+    /**
+     * Convert video using ffmpeg binary. Video conversion otput is written to the 'log' variable.
+     * @param $sFile input file for conversion
+     * @param $sFileOut output file for conversion, can be the same as input file
+     * @param $sExt output file extension, in the format '.ext'
+     * @param $aParams filter params
+     * @param $aOptions ffmpeg options as array
+     * @return false on error, or true on success
+     */
+    protected function _convertVideo ($sFile, $sFileOut, $sExt, $aParams, $aOptions)
+    {
+        if (!empty($aParams['ffmpeg_options']) && is_array($aParams['ffmpeg_options']))
+            $aOptions = array_merge($aOptions, $aParams['ffmpeg_options']);
+
+        $sOptions = '';
+        foreach ($aOptions as $k => $v)
+            if ($v)
+                $sOptions .= "-{$k} {$v} ";
+
+        $bRename = false;
+        if ($sFileOut == $sFile) { // if output file is the same as input - generate new tmp file
+            $sFileOut = $this->getTmpFilename($sExt);
+            $bRename = true;
+        }
+        elseif (file_exists($sFileOut)) {
+            @unlink($sFileOut);
+        }
+
+        $sCommand = escapeshellcmd(BX_SYSTEM_FFMPEG) . ' -y -i ' . escapeshellarg($sFile) . ' ' . $sOptions . ' ' . escapeshellarg($sFileOut) . ' 2>&1';
+        $sOutput = `$sCommand`;
+        $this->addToLog("\n---\n{$sCommand}\n{$sOutput}\n");
+
+        if (!file_exists($sFileOut) || 0 == filesize($sFileOut))
+            return false;
+
+        if ($bRename && !rename($sFileOut, $sFile)) // rename tmp file, if tmp file was generated
+            return false;
+
+        return true;
+    }
+
+    /**
+     * Get video size. 
+     * It generated image from the video and returns size of the image.
+     * @param $sFile path to the video file
+     * @return array where 'w' key is width and 'h' key is height
+     */
+    protected function _getVideoSize ($sFile)
+    {
+        $sFileOut = $this->getTmpFilename('.jpg');
+
+        $bRet = $this->_convertVideo($sFile, $sFileOut, '.jpg', array(), array (
+            'ss' => 0,
+            'vframes' => 1,
+            'f' => 'image2',
+            'an' => ' ',
+        ));
+
+        if (!$bRet) {
+            @unlink($sFileOut);
+            return false;
+        }
+
+    	$oImage = BxDolImageResize::getInstance();
+        $aSize = $oImage->getImageSize($sFileOut);
+
+        @unlink($sFileOut);
+
+        return $aSize;
+    }
+
+    /**
+     * Get output video size as 'WxH' string.
+     * It check filter params for 'w' and 'h' options and original video ratio and return the resulted video size.
+     * - If only height specified in params (desired behavior) or no width or height is specified (default height is 360), 
+     * then width is automatically calculated using original video size ratio.
+     * - If only width specified in params, then height is automatically calculated using original video size ratio.
+     * - If width and height specified in params then these values are returned, but the resulted video size can be unproportionate.
+     * @param $sFile original video file path (to check original video size ratio)
+     * @param $aParams filter params
+     * @return WxH string
+     */
+    protected function _getOptionSizeForVideo ($sFile, $aParams) 
+    {
+        if (isset($aParams['w']) && isset($aParams['h']))
+            return $aParams['w'] . 'x' . $aParams['h'];
+
+        if (!isset($aParams['w']) && !isset($aParams['h']))
+            $aParams['h'] = '360';
+
+        $fRatio = 16/9;
+        $aSize = $this->_getVideoSize($sFile);
+        if ($aSize)
+            $fRatio = $aSize['w'] / $aSize['h'];
+
+        if (isset($aParams['h']))
+            return round($fRatio * $aParams['h'] / 2) * 2 . 'x' . $aParams['h'];
+
+        if (isset($aParams['w']))
+            return  $aParams['w'] . 'x' . round($aParams['w'] / $fRatio / 2) * 2;
+
+        return '640x360'; // should never happen
+    }
+}
+
+/** @} */

+ 437 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolUploader.php

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

+ 181 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolView.php

@@ -0,0 +1,181 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+define('BX_DOL_VIEW_OLD_VIEWS', 3 * 86400); ///< views older than this number of seconds will be deleted automatically
+
+define('BX_DOL_VIEW_USAGE_BLOCK', 'block');
+define('BX_DOL_VIEW_USAGE_INLINE', 'inline');
+define('BX_DOL_VIEW_USAGE_DEFAULT', BX_DOL_VIEW_USAGE_BLOCK);
+
+/**
+ * Track any object views automatically
+ *
+ * Add record to sys_object_view table to track object views,
+ * to record view just create this class instance with your object id,
+ * for example:
+ * @code
+ *  BxDolView::getObjectInstance('my_system', 25); // 25 - is object id
+ * @endcode
+ *
+ * Description of sys_object_view table fields:
+ * @code
+ *  `name` - system name, it is better to use unique module prefix here, lowercase and all spaces are underscored
+ *  `table_track` - table to track views
+ *  `period` - period in secs to update next views, default is 86400(1 day)
+ *  `is_on` - is the system activated
+ *  `trigger_table` - table where you need to update views field
+ *  `trigger_field_id` - table field id to unique determine object
+ *  `trigger_field_count` - table field where total view number is stored
+ *  `class_name` - your custom class name, if you overrride default class
+ *  `class_file` - your custom class path
+ * @endcode
+ *
+ * Structure of the track table is the following:
+ * @code
+ *  CREATE TABLE `my_views_track` (
+ *      `object_id` int(11) NOT NULL default '0', -- this field type must be exact as your object id type
+ *      `viewer_id` int(11) NOT NULL default '0', -- viewer profile id
+ *      `viewer_nip` int(11) unsigned NOT NULL default '0', -- viewer ip address to track guest views
+ *      `date` int(11) NOT NULL default '0', -- timestamp of last recorded view
+ *      KEY `id` (`object_id`,`viewer_id`,`viewer_nip`)
+ *  ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+ * @endcode
+ *
+ */
+class BxDolView extends BxDolObject
+{
+    protected function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
+    {
+        parent::__construct($sSystem, $iId, $iInit, $oTemplate);
+        if(empty($this->_sSystem))
+            return;
+
+        $this->_oQuery = new BxDolViewQuery($this);
+    }
+
+   /**
+     * get votes object instanse
+     * @param $sSys view object name
+     * @param $iId associated content id, where vote is available
+     * @param $iInit perform initialization
+     * @return null on error, or ready to use class instance
+     */
+    public static function getObjectInstance($sSys, $iId, $iInit = true)
+    {
+        if(isset($GLOBALS['bxDolClasses']['BxDolView!' . $sSys . $iId]))
+            return $GLOBALS['bxDolClasses']['BxDolView!' . $sSys . $iId];
+
+        $aSystems = self::getSystems();
+        if(!isset($aSystems[$sSys]))
+            return null;
+
+        $sClassName = 'BxTemplView';
+        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);
+        return ($GLOBALS['bxDolClasses']['BxDolView!' . $sSys . $iId] = $o);
+    }
+
+    public static function &getSystems()
+    {
+        if(!isset($GLOBALS['bx_dol_view_systems']))
+            $GLOBALS['bx_dol_view_systems'] = BxDolDb::getInstance()->fromCache('sys_objects_view', 'getAllWithKey', '
+                SELECT
+                    `id` as `id`,
+                    `name` AS `name`,
+                    `table_track` AS `table_track`,
+                    `period` AS `period`,
+                    `is_on` AS `is_on`,
+                    `trigger_table` AS `trigger_table`,
+                    `trigger_field_id` AS `trigger_field_id`,
+                    `trigger_field_author` AS `trigger_field_author`,
+                    `trigger_field_count` AS `trigger_field_count`,
+                    `class_name` AS `class_name`,
+                    `class_file` AS `class_file`
+                FROM `sys_objects_view`', 'name');
+
+        return $GLOBALS['bx_dol_view_systems'];
+    }
+
+    public function actionGetViewedBy()
+    {
+        if (!$this->isEnabled())
+           return '';
+
+        return $this->_getViewedBy();
+    }
+
+    public function doView()
+    {
+        if(!$this->isEnabled())
+            return false;
+
+        $iObjectId = $this->getId();
+        $iAuthorId = $this->_getAuthorId();
+        $sAuthorIp = $this->_getAuthorIp();
+
+        if($this->_oQuery->doView($iObjectId, $iAuthorId, $sAuthorIp)) {
+            $this->_trigger();
+
+            $oZ = new BxDolAlerts($this->_sSystem, 'view', $iObjectId, $iAuthorId);
+            $oZ->alert();
+
+            return true;
+        }
+
+        return false;
+    }
+
+	/**
+     * Permissions functions
+     * 
+     * Note. The 'view' action is performed automatically and therefore the manual action is not allowed.
+     */
+    public function isAllowedView($isPerformAction = false)
+    {
+        return false;
+    }
+
+    public function isAllowedViewView($isPerformAction = false)
+    {
+        if(isAdmin())
+            return true;
+
+        return $this->checkAction('view_view', $isPerformAction);
+    }
+
+    public function isAllowedViewViewViewers($isPerformAction = false)
+    {
+        $oAcl = BxDolAcl::getInstance();
+        if(isAdmin() || $oAcl->isMemberLevelInSet(array(MEMBERSHIP_ID_MODERATOR, MEMBERSHIP_ID_ADMINISTRATOR)))
+            return true;
+
+        $iObjectAuthorId = $this->_oQuery->getObjectAuthorId($this->_iId);
+        return $iObjectAuthorId != 0 && $iObjectAuthorId == $this->_getAuthorId() && $this->checkAction('view_view_viewers', $isPerformAction);
+    }
+
+	/**
+     * Internal functions
+     */
+    protected function _getIconDo()
+    {
+    	return 'eye';
+    }
+
+    protected function _getTitleDo()
+    {
+    	return '_view_do_view';
+    }
+}
+
+/** @} */

+ 81 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolViewQuery.php

@@ -0,0 +1,81 @@
+<?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 BxDolView
+ */
+class BxDolViewQuery extends BxDolObjectQuery
+{
+    protected $_iPeriod;
+
+    public function __construct(&$oModule)
+    {
+        parent::__construct($oModule);
+
+        $aSystem = $this->_oModule->getSystemInfo();
+        $this->_iPeriod = (int)$aSystem['period'];
+    }
+
+    public function isPerformed($iObjectId, $iAuthorId)
+    {
+        $sQuery = $this->prepare("SELECT `date` FROM `{$this->_sTableTrack}` WHERE `object_id` = ? AND `viewer_id` = ? LIMIT 1", $iObjectId, $iAuthorId);
+        return (int)$this->getOne($sQuery) > 0;
+    }
+
+	public function getPerformedBy($iObjectId)
+    {
+        $sQuery = $this->prepare("SELECT `viewer_id` AS `id`, COUNT(`viewer_id`) AS `count` FROM `{$this->_sTableTrack}` WHERE `object_id`=? GROUP BY `viewer_id` ORDER BY `date` DESC", $iObjectId);
+        return $this->getAll($sQuery);
+    }
+
+	public function getView($iObjectId)
+    {
+        return array('count' => $this->getObjectCount($iObjectId));
+    }
+
+    public function doView($iObjectId, $iAuthorId, $sAuthorIp)
+    {
+        $iAuthorNip = ip2long($sAuthorIp);
+
+        $aBindings = array(
+        	'object_id' => $iObjectId
+        );
+        if($iAuthorId) {
+        	$aBindings['viewer_id'] = $iAuthorId;
+
+            $sWhere = " AND `viewer_id` = :viewer_id ";
+        }
+        else {
+        	$aBindings['viewer_nip'] = $iAuthorNip;
+
+            $sWhere = " AND `viewer_id` = '0' AND `viewer_nip` = :viewer_nip ";
+        }
+
+        $iDate = (int)$this->getOne("SELECT `date` FROM `{$this->_sTableTrack}` WHERE `object_id` = :object_id " . $sWhere, $aBindings);
+        $iDateNow = time();
+
+        if(!$iDate) {
+            $sQuery = $this->prepare("INSERT IGNORE INTO `{$this->_sTableTrack}` SET `object_id` = ?, `viewer_id` = ?, `viewer_nip` = ?, `date` = ?", $iObjectId, $iAuthorId, $iAuthorNip, $iDateNow);
+            return (int)$this->query($sQuery) > 0;
+        }
+
+        if(($iDateNow - $iDate) > $this->_iPeriod) {
+            $sQuery = $this->prepare("UPDATE `{$this->_sTableTrack}` SET `date` = ? WHERE `object_id` = ? AND `viewer_id` = ? AND `viewer_nip` = ?", $iDateNow, $iObjectId, $iAuthorId, $iAuthorNip);
+            return (int)$this->query($sQuery) > 0;
+        }
+    }
+
+    public function updateTriggerTable($iObjectId)
+    {
+        $sQuery = $this->prepare("UPDATE `{$this->_sTriggerTable}` SET `{$this->_sTriggerFieldCount}` = `{$this->_sTriggerFieldCount}` + 1 WHERE `{$this->_sTriggerFieldId}` = ?", $iObjectId);
+        return (int)$this->query($sQuery) > 0;
+    }
+}
+
+/** @} */

+ 311 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/classes/BxDolVote.php

@@ -0,0 +1,311 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+bx_import('BxDolAcl');
+
+define('BX_DOL_VOTE_TYPE_STARS', 'stars');
+define('BX_DOL_VOTE_TYPE_LIKES', 'likes');
+
+define('BX_DOL_VOTE_USAGE_BLOCK', 'block');
+define('BX_DOL_VOTE_USAGE_INLINE', 'inline');
+define('BX_DOL_VOTE_USAGE_DEFAULT', BX_DOL_VOTE_USAGE_BLOCK);
+
+/**
+ * Vote for any content
+ *
+ * Related classes:
+ * - BxDolVoteQuery - vote database queries
+ * - BxBaseVote - vote base representation
+ * - BxTemplVote - custom template representation
+ *
+ * AJAX vote for any content. Stars and Plus based representations are supported.
+ *
+ * To add vote section to your feature you need to add a record to 'sys_objects_vote' table:
+ *
+ * - ID - autoincremented id for internal usage
+ * - Name - your unique module name, with vendor prefix, lowercase and spaces are underscored
+ * - TableMain - table name where summary votigs are stored
+ * - TableTrack - table name where each vote is stored
+ * - PostTimeout - number of seconds to not allow duplicate vote
+ * - MinValue - min vote value, 1 by default
+ * - MaxValue - max vote value, 5 by default
+ * - IsUndo - is Undo enabled for Plus based votes
+ * - IsOn - is this vote object enabled
+ * - TriggerTable - table to be updated upon each vote
+ * - TriggerFieldId - TriggerTable table field with unique record id, primary key
+ * - TriggerFieldRate - TriggerTable table field with average rate
+ * - TriggerFieldRateCount - TriggerTable table field with votes count
+ * - ClassName - your custom class name, if you overrride default class
+ * - ClassFile - your custom class path
+ *
+ * You can refer to BoonEx modules for sample record in this table.
+ *
+ *
+ *
+ * @section example Example of usage:
+ * To get Star based vote you need to have different values for MinValue and MaxValue (for example 1 and 5)
+ * and IsUndo should be equal to 0. To get Plus(Like) based vote you need to have equal values
+ * for MinValue and MaxValue (for example 1) and IsUndo should be equal to 1. After filling the other
+ * paramenters in the table you can show vote in any place, using the following code:
+ * @code
+ * $o = BxDolVote::getObjectInstance('system object name', $iYourEntryId);
+ * if (!$o->isEnabled()) return '';
+ *     echo $o->getElementBlock();
+ * @endcode
+ *
+ *
+ * @section acl Memberships/ACL:
+ * - vote
+ *
+ *
+ *
+ * @section alerts Alerts:
+ * Alerts type/unit - every module has own type/unit, it equals to ObjectName.
+ * The following alerts are rised:
+ *
+ * - rate - comment was posted
+ *      - $iObjectId - entry id
+ *      - $iSenderId - rater user id
+ *      - $aExtra['rate'] - rate
+ *
+ */
+
+class BxDolVote extends BxDolObject
+{
+	protected $_bLike = true;
+	protected $_sType = BX_DOL_VOTE_TYPE_LIKES;
+
+    protected function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
+    {
+        parent::__construct($sSystem, $iId, $iInit, $oTemplate);
+        if(empty($this->_sSystem))
+            return;
+
+        $this->_oQuery = new BxDolVoteQuery($this);
+
+		$this->_bLike = $this->isLikeMode();
+        $this->_sType = $this->_bLike ? BX_DOL_VOTE_TYPE_LIKES : BX_DOL_VOTE_TYPE_STARS;
+    }
+
+    /**
+     * get votes object instanse
+     * @param $sSys vote object name
+     * @param $iId associated content id, where vote is available
+     * @param $iInit perform initialization
+     * @return null on error, or ready to use class instance
+     */
+    public static function getObjectInstance($sSys, $iId, $iInit = true, $oTemplate = false)
+    {
+        if(isset($GLOBALS['bxDolClasses']['BxDolVote!' . $sSys . $iId]))
+            return $GLOBALS['bxDolClasses']['BxDolVote!' . $sSys . $iId];
+
+        $aSystems = self::getSystems();
+        if(!isset($aSystems[$sSys]))
+            return null;
+
+        $sClassName = 'BxTemplVote';
+        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']['BxDolVote!' . $sSys . $iId] = $o);
+    }
+
+    public static function &getSystems()
+    {
+        if(!isset($GLOBALS['bx_dol_vote_systems']))
+            $GLOBALS['bx_dol_vote_systems'] = BxDolDb::getInstance()->fromCache('sys_objects_vote', 'getAllWithKey', '
+                SELECT
+                    `ID` as `id`,
+                    `Name` AS `name`,
+                    `TableMain` AS `table_main`,
+                    `TableTrack` AS `table_track`,
+                    `PostTimeout` AS `post_timeout`,
+                    `MinValue` AS `min_value`,
+                    `MaxValue` AS `max_value`,
+                    `IsUndo` AS `is_undo`,
+                    `IsOn` AS `is_on`,
+                    `TriggerTable` AS `trigger_table`,
+                    `TriggerFieldId` AS `trigger_field_id`,
+                    `TriggerFieldAuthor` AS `trigger_field_author`,
+                    `TriggerFieldRate` AS `trigger_field_rate`,
+                    `TriggerFieldRateCount` AS `trigger_field_count`,
+                    `ClassName` AS `class_name`,
+                    `ClassFile` AS `class_file`
+                FROM `sys_objects_vote`', 'name');
+
+        return $GLOBALS['bx_dol_vote_systems'];
+    }
+
+    public function isUndo()
+    {
+        return (int)$this->_aSystem['is_undo'] == 1;
+    }
+
+    public function isLikeMode()
+    {
+        $iMinValue = $this->getMinValue();
+        $iMaxValue = $this->getMaxValue();
+
+        return $iMinValue == $iMaxValue;
+    }
+
+    public function getMinValue()
+    {
+        return (int)$this->_aSystem['min_value'];
+    }
+
+    public function getMaxValue()
+    {
+        return (int)$this->_aSystem['max_value'];
+    }
+
+	public function getObjectAuthorId($iObjectId = 0)
+    {
+    	if(empty($this->_aSystem['trigger_field_author']))
+    		return 0;
+
+        return $this->_oQuery->getObjectAuthorId($iObjectId ? $iObjectId : $this->getId());
+    }
+
+    /**
+     * Interface functions for outer usage
+     */
+    public function getStatCounter()
+    {
+        $aVote = $this->_oQuery->getVote($this->getId());
+        return $aVote['count'];
+    }
+
+    public function getStatRate()
+    {
+        $aVote = $this->_oQuery->getVote($this->getId());
+        return $aVote['rate'];
+    }
+
+    /**
+     * Actions functions
+     */
+    public function actionVote()
+    {
+        if(!$this->isEnabled()) {
+            echoJson(array('code' => 1));
+            return;
+        }
+
+        $iObjectId = $this->getId();
+        $iObjectAuthorId = $this->getObjectAuthorId($iObjectId);
+        $iAuthorId = $this->_getAuthorId();
+        $iAuthorIp = $this->_getAuthorIp();
+
+        $bUndo = $this->isUndo();
+        $bLikeMode = $this->isLikeMode();
+
+        $bVoted = $this->isPerformed($iObjectId, $iAuthorId);
+        $bPerformUndo = $bVoted && $bUndo ? true : false;
+
+        if(!$bPerformUndo && !$this->isAllowedVote(true)) {
+            echoJson(array('code' => 2, 'msg' => $this->msgErrAllowedVote()));
+            return;
+        }
+
+        if((!$bLikeMode && !$this->_oQuery->isPostTimeoutEnded($iObjectId, $iAuthorIp)) || ($bLikeMode && $bVoted && !$bUndo)) {
+            echoJson(array('code' => 3, 'msg' => _t('_vote_err_duplicate_vote')));
+            return;
+        }
+
+        $iValue = bx_get('value');
+        if($iValue === false) {
+            echoJson(array('code' => 4));
+            return;
+        }
+
+        $iValue = bx_process_input($iValue, BX_DATA_INT);
+
+        $iMinValue = $this->getMinValue();
+        if($iValue < $iMinValue)
+            $iValue = $iMinValue;
+
+        $iMaxValue = $this->getMaxValue();
+        if($iValue > $iMaxValue)
+            $iValue = $iMaxValue;
+
+		$iId = $this->_oQuery->putVote($iObjectId, $iAuthorId, $iAuthorIp, $iValue, $bPerformUndo);
+        if($iId === false) {
+            echoJson(array('code' => 5));
+            return;
+        }
+
+        $this->_trigger();
+
+        $oZ = new BxDolAlerts($this->_sSystem, ($bPerformUndo ? 'un' : '') . 'doVote', $iObjectId, $iAuthorId, array('vote_id' => $iId, 'vote_author_id' => $iAuthorId, 'object_author_id' => $iObjectAuthorId, 'value' => $iValue));
+        $oZ->alert();
+
+        $oZ = new BxDolAlerts('vote', ($bPerformUndo ? 'un' : '') . 'do', $iId, $iAuthorId, array('object_system' => $this->_sSystem, 'object_id' => $iObjectId, 'object_author_id' => $iObjectAuthorId, 'value' => $iValue));
+        $oZ->alert();
+
+        $aVote = $this->_oQuery->getVote($iObjectId);
+        echoJson(array(
+            'code' => 0,
+            'rate' => $aVote['rate'],
+            'count' => $aVote['count'],
+            'countf' => (int)$aVote['count'] > 0 ? $this->_getLabelCounter($aVote['count']) : '',
+        	'label_icon' => $bLikeMode ? $this->_getIconDoLike(!$bVoted) : '',
+        	'label_title' => $bLikeMode ? _t($this->_getTitleDoLike(!$bVoted)) : '',
+        	'disabled' => !$bVoted && !$bUndo,
+        ));
+    }
+
+    public function actionGetVotedBy()
+    {
+        if (!$this->isEnabled())
+           return '';
+
+        return $this->_getVotedBy();
+    }
+
+    /**
+     * Permissions functions
+     */
+    public function isAllowedVote($isPerformAction = false)
+    {
+        if(isAdmin())
+            return true;
+
+        return $this->checkAction('vote', $isPerformAction);
+    }
+
+    public function msgErrAllowedVote()
+    {
+        return $this->checkActionErrorMsg('vote');
+    }
+
+    /**
+     * Internal functions
+     */
+    protected function _getIconDoLike($bVoted)
+    {
+    	return $bVoted && $this->isUndo() ?  'thumbs-down' : 'thumbs-up';
+    }
+
+    protected function _getTitleDoLike($bVoted)
+    {
+    	return $bVoted && $this->isUndo() ? '_vote_do_unlike' : '_vote_do_like';
+    }
+
+    protected function _getTitleDoBy()
+    {
+    	return '_vote_do_by';
+    }
+}
+
+/** @} */

+ 159 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/design.inc.php

@@ -0,0 +1,159 @@
+<?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
+ * @{
+ */
+
+/**
+ * design box with content only - no borders, no background, no caption
+ * @see DesignBoxContent
+ */
+define('BX_DB_CONTENT_ONLY', 0);
+
+/**
+ * default design box with content, borders and caption
+ * @see DesignBoxContent
+ */
+define('BX_DB_DEF', 1);
+
+/**
+ * just empty design box, without anything
+ * @see DesignBoxContent
+ */
+define('BX_DB_EMPTY', 2);
+
+/**
+ * design box with content, like BX_DB_DEF but without caption
+ * @see DesignBoxContent
+ */
+define('BX_DB_NO_CAPTION', 3);
+
+/**
+ * design box with content only wrapped with default padding - no borders, no background, no caption
+ * it can be used to just wrap content with default padding
+ * @see DesignBoxContent
+ */
+define('BX_DB_PADDING_CONTENT_ONLY', 10);
+
+/**
+ * default design box with content wrapped with default padding, borders and caption
+ * @see DesignBoxContent
+ */
+define('BX_DB_PADDING_DEF', 11);
+
+/**
+ * design box with content wrapped with default padding, like BX_DB_DEF but without caption
+ * @see DesignBoxContent
+ */
+define('BX_DB_PADDING_NO_CAPTION', 13);
+
+define('BX_DB_HIDDEN_PHONE', 1);
+define('BX_DB_HIDDEN_TABLET', 2);
+define('BX_DB_HIDDEN_DESKTOP', 3);
+
+define('BX_FORMAT_DATE', 'sys_format_date'); ///< date format identifier for use in @see bx_time_js function
+define('BX_FORMAT_TIME', 'sys_format_time'); ///< time format identifier for use in @see bx_time_js function
+define('BX_FORMAT_DATE_TIME', 'sys_format_datetime'); ///< datetime format identifier for use in @see bx_time_js function
+
+/**
+ * Display "design box" HTML code
+ *
+ * @see BxBaseFunctions::DesignBoxContent
+ *
+ * @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 BxTemplFunctions::getInstance()->designBoxContent ($sTitle, $sContent, $iTemplateNum, $mixedMenu);
+}
+
+/**
+ * Use this function in pages if you want to not cache it.
+ **/
+function send_headers_page_changed()
+{
+    $now = gmdate('D, d M Y H:i:s') . ' GMT';
+
+    header("Expires: $now");
+    header("Last-Modified: $now");
+    header("Cache-Control: no-cache, must-revalidate");
+    header("Pragma: no-cache");
+}
+
+function MsgBox($sText, $iTimer = 0)
+{
+    return BxTemplFunctions::getInstance()->msgBox($sText, $iTimer);
+}
+
+function PopupBox($sName, $sTitle, $sContent, $isHiddenByDefault = false)
+{
+    return BxTemplFunctions::getInstance()->popupBox($sName, $sTitle, $sContent, $isHiddenByDefault);
+}
+
+function getVersionComment()
+{
+    $aVer = explode('.', bx_get_ver());
+
+    // version output made for debug possibilities.
+    // randomizing made for security issues. do not change it...
+    $aVerR[0] = $aVer[0];
+    $aVerR[1] = rand( 0, 100 );
+    $aVerR[2] = $aVer[1];
+    $aVerR[3] = rand( 0, 100 );
+    $aVerR[4] = $aVer[2];
+
+    return '<!-- ' . implode(' ', $aVerR) . ' -->';
+}
+
+function getSiteStatUser()
+{
+    $sqlQuery = "SELECT * FROM `sys_statistics` ORDER BY `order`";
+
+    $aStat = BxDolDb::getInstance()->fromCache('sys_statistics', 'getAllWithKey', $sqlQuery, 'name');
+
+    return "<pre>TODO: nice output\n" . print_r($aStat, true) . '</pre>';
+}
+
+/**
+ * Output time wrapped in &lt;time&gt; 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.
+ *
+ * This is just short version for:
+ * @see BxTemplFunctions::timeForJs
+ *
+ * @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 bx_time_js ($iUnixTimestamp, $sFormatIdentifier = BX_FORMAT_DATE, $bForceFormat = false)
+{
+    return BxTemplFunctions::getInstance()->timeForJs ($iUnixTimestamp, $sFormatIdentifier, $bForceFormat);
+}
+
+/**
+ * Get UTC/GMT time string in ISO8601 date format from provided unix timestamp
+ * @param $iUnixTimestamp - unix timestamp
+ * @return ISO8601 formatted date/time string
+ */
+function bx_time_utc ($iUnixTimestamp)
+{
+    return gmdate(DATE_ISO8601, (int)$iUnixTimestamp);
+}
+
+$oZ = new BxDolAlerts('system', 'design_included', 0);
+$oZ->alert();
+
+/** @} */

+ 112 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/js/classes/BxDolChartGrowth.js

@@ -0,0 +1,112 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function BxDolChartGrowth (oOptions) {
+    this._sObjName = undefined == oOptions.sObjName ? 'oBxDolChartGrowth' : oOptions.sObjName;    // javascript object name, to run current object instance from onTimer
+    this._sActionsUrl = oOptions.sRootUrl + 'chart.php'; // actions url address
+
+    this._sAnimationEffect = 'fade';
+    this._iAnimationSpeed = 'slow';
+
+    this._sKeyObjects = 'bx_chart_growth_objects';
+    this._sKeyDateFrom = 'bx_chart_growth_date_from';
+    this._sKeyDateTo = 'bx_chart_growth_date_to';
+    this._sKeyGraph = 'bx_chart_growth_graph';
+
+    this._sDateFormat = 'yy-mm-dd';
+    this._oChart = null;
+    this._oChartOptionsDefault = {};
+    this._sChartColorBorder = undefined == oOptions.sChartColorBorder ? 'rgba(74, 144, 226, 1.0)' : oOptions.sChartColorBorder;
+    this._sChartColorBackground = undefined == oOptions.sChartColorBackground ? 'rgba(74, 144, 226, 0.4)' : oOptions.sChartColorBackground;
+
+    var $this = this;
+    $(document).ready(function() {
+    	$this.loadData();
+    });
+}
+
+BxDolChartGrowth.prototype.loadData = function()
+{
+	var $this = this;
+	var oGraph = $('#' + this._sKeyGraph);
+	var oGraphWrp = oGraph.parents('div:first');
+	var oGraphErr = oGraphWrp.find('.bx-chart-growth-graph-error').hide();
+
+    $('#' + this._sKeyObjects).attr('disabled', true);
+
+    bx_loading(oGraphWrp, true);
+
+    $.get(
+    	this._sActionsUrl,
+    	{
+    		object: $('#' + this._sKeyObjects).val(),
+    		action: 'load_data_by_interval',
+    		from: $.datepicker.formatDate($this._sDateFormat, $('#' + $this._sKeyDateFrom).datepicker('getDate')),
+            to: $.datepicker.formatDate($this._sDateFormat, $('#' + $this._sKeyDateTo).datepicker('getDate'))
+    	},
+    	function(oData) {
+    		$('#' + $this._sKeyObjects).attr('disabled', false);
+
+            bx_loading(oGraphWrp, false);
+
+            if(oData.error != undefined) {
+            	if($this._oChart)
+                	$this._oChart.destroy();
+
+            	oGraphErr.html(oData.error).show();
+                return;
+            } 
+
+            // hide date selector if chart doesn't support date range
+            if (oData.hide_date_range) {
+                $('#' + $this._sKeyDateFrom).parents('.bx-form-element-wrapper:first').fadeOut();
+                $('#' + $this._sKeyDateTo).parents('.bx-form-element-wrapper:first').fadeOut();
+            }
+            else {
+            	$('#' + $this._sKeyDateFrom).parents('.bx-form-element-wrapper:first').fadeIn();
+            	$('#' + $this._sKeyDateTo).parents('.bx-form-element-wrapper:first').fadeIn();
+            }
+
+            var oChartOptions = oData.options || {};
+            var oChartLabels = new Array();
+            var oChartData = new Array();
+
+            // create chart arrays
+            if(oData.column_date !== false && oData.column_count !== false)
+                for (var i in oData.data) {
+                    var sDate = oData.data[i][oData.column_date];
+                    var aDate = sDate.match(/(\d{4})-(\d{2})-(\d{2})/);
+                    if(!aDate || !aDate[1] || !aDate[2] || !aDate[3])
+                        continue;
+
+                    oChartLabels[i] = sDate;
+                    oChartData[i] = oData.data[i][oData.column_count];
+                }
+
+            if($this._oChart)
+            	$this._oChart.destroy();
+
+	    	$this._oChart = new Chart(oGraph, {
+	    		type: oData.type != undefined ? oData.type : 'line',
+	    		data: {
+	            	labels: oChartLabels,
+	            	datasets: [{
+	            		label: oData.title,
+	            		data: oChartData,
+	            		borderColor: $this._sChartColorBorder,
+	            		backgroundColor: $this._sChartColorBackground
+	            	}]
+	            },
+	    		options: $.extend({}, $this._oChartOptionsDefault, oChartOptions)
+	    	});
+    	},
+    	'json'
+    );
+};
+
+/** @} */

+ 661 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/js/classes/BxDolCmts.js

@@ -0,0 +1,661 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function BxDolCmts (options) {
+    this._sObjName = undefined == options.sObjName ? 'oCmts' : options.sObjName;    // javascript object name, to run current object instance from onTimer
+    this._sActionsUrl = options.sRootUrl + 'cmts.php'; // actions url address
+
+    this._sSystem = options.sSystem; // current comment system
+    this._iAuthorId = options.iAuthorId; // this comment's author ID.
+    this._iObjId = options.iObjId; // this object id comments
+    this._sBaseUrl = options.sBaseUrl; // base url to view comment's listing.
+
+    this._sPostFormPosition = undefined == options.sPostFormPosition ? 'top' : options.sPostFormPosition;
+    this._sDisplayType = undefined == options.sDisplayType ? 'threaded' : options.sDisplayType;
+    this._sBrowseType = undefined == options.sBrowseType ? 'tail' : options.sBrowseType;
+    this._sBrowseFilter = undefined == options.sBrowseFilter ? 'all' : options.sBrowseFilter;
+
+    this._sAnimationEffect = 'fade';
+    this._iAnimationSpeed = 'slow';
+
+    this._oSavedTexts = {};
+    this._sRootId = '#cmts-box-' + this._sSystem + '-' + this._iObjId;
+
+    // init post comment form
+    if ($(this._sRootId + ' .cmt-post-reply form').length) {
+    	var sFormId = $(this._sRootId + ' .cmt-post-reply form').attr('id');
+    	this.cmtInitFormPost(sFormId);
+    }
+
+    // blink (highlight) necessary comments
+    this._cmtsBlink($(this._sRootId));
+
+    this._bLiveUpdatePaused = false;
+}
+
+/*--- Main layout functionality ---*/
+BxDolCmts.prototype.cmtInitFormPost = function(sCmtFormId)
+{
+	var $this = this;
+	var oCmtForm = $('#' + sCmtFormId);
+
+	oCmtForm.ajaxForm({
+        dataType: "json",
+        beforeSubmit: function (formData, jqForm, options) {
+        	window[$this._sObjName].cmtBeforePostSubmit(oCmtForm);
+        },
+        success: function (oData) {
+        	window[$this._sObjName].cmtAfterPostSubmit(oCmtForm, oData);
+        }
+    });
+};
+
+BxDolCmts.prototype.cmtBeforePostSubmit = function(oCmtForm)
+{
+	this._loadingInButton($(oCmtForm).children().find(':submit'), true);
+};
+
+BxDolCmts.prototype.cmtAfterPostSubmit = function (oCmtForm, oData)
+{
+	this._loadingInButton($(oCmtForm).children().find(':submit'), false);
+
+	if(oData && oData.msg != undefined)
+        alert(oData.msg);
+
+	if(oData && oData.id != undefined) {
+		var iCmtId = parseInt(oData.id);
+        if(iCmtId > 0) {
+        	var $this = this;
+
+            this._getCmt(oCmtForm, iCmtId);
+            this._getForm(undefined, parseInt(oData.parent_id), function(sForm) {
+            	var oForm = $(sForm);
+            	oCmtForm.parents('.cmt-reply:first').hide().html(oForm.html()).show();
+
+            	var sFormId = oForm.find('form').attr('id');
+            	$this.cmtInitFormPost(sFormId);
+            });
+        }
+
+        return;
+	}
+
+	if(oData && oData.form != undefined && oData.form_id != undefined) {
+		$('#' + oData.form_id).replaceWith(oData.form);
+		this.cmtInitFormPost(oData.form_id);
+
+		return;
+	}
+};
+
+BxDolCmts.prototype.cmtInitFormEdit = function(sCmtFormId)
+{
+	var $this = this;
+	var oCmtForm = $('#' + sCmtFormId);
+
+	oCmtForm.ajaxForm({
+        dataType: "json",
+        beforeSubmit: function (formData, jqForm, options) {
+        	window[$this._sObjName].cmtBeforeEditSubmit(oCmtForm);
+        },
+        success: function (oData) {
+        	window[$this._sObjName].cmtAfterEditSubmit(oCmtForm, oData);
+        }
+    });
+};
+
+BxDolCmts.prototype.cmtBeforeEditSubmit = function(oCmtForm)
+{
+	this._loadingInButton($(oCmtForm).children().find(':submit'), true);
+};
+
+BxDolCmts.prototype.cmtAfterEditSubmit = function (oCmtForm, oData)
+{
+	var $this = this;
+	this._loadingInButton($(oCmtForm).children().find(':submit'), false);
+
+	if(oData && oData.msg != undefined)
+        alert(oData.msg);
+
+	if(oData && oData.id != undefined && oData.text != undefined) {
+		var iCmtId = parseInt(oData.id);
+		if(iCmtId > 0) {
+			$('#cmt' + iCmtId + ' .cmt-body').bx_anim('hide', this._sAnimationEffect, this._iAnimationSpeed, function() {
+				$(this).html(oData.text).bx_anim('show', $this._sAnimationEffect, $this._iAnimationSpeed);
+			});
+        }
+
+        return;
+	}
+
+	if(oData && oData.form != undefined && oData.form_id != undefined) {
+		$('#' + oData.form_id).replaceWith(oData.form);
+		this.cmtInitFormEdit(oData.form_id);
+
+		return;
+	}
+};
+
+BxDolCmts.prototype.cmtEdit = function(oLink, iCmtId) {
+    var $this = this;
+    var oParams = this._getDefaultActions();
+    oParams['action'] = 'GetFormEdit';
+    oParams['Cmt'] = iCmtId;
+
+    $(oLink).parents('.cmt-comment-manage:first').dolPopupHide();
+
+    var sBodyId = this._sRootId + ' #cmt' + iCmtId + ' .cmt-body:first';
+    if ($(sBodyId + ' > form').length) {
+        $(sBodyId).bx_anim('hide', $this._sAnimationEffect, $this._iAnimationSpeed, function() {
+            $(this).html($this._oSavedTexts[iCmtId]).bx_anim('show', $this._sAnimationEffect, $this._iAnimationSpeed);
+        });
+        return;
+    }
+    else
+        this._oSavedTexts[iCmtId] = $(sBodyId).html();
+
+    this._loadingInContent (oLink, true);
+
+    jQuery.post (
+        this._sActionsUrl,
+        oParams,
+        function (oData) {
+        	$this._loadingInContent (oLink, false);
+
+        	if(oData && oData.msg != undefined)
+                alert(oData.msg);
+
+        	if(oData && oData.form != undefined && oData.form_id != undefined) {
+        		$(oLink).parents('.bx-popup-applied:first:visible').dolPopupHide();
+
+        		$(sBodyId).bx_anim('hide', $this._sAnimationEffect, $this._iAnimationSpeed, function() {
+                    $(this).html(oData.form).bx_anim('show', $this._sAnimationEffect, $this._iAnimationSpeed, function() {
+                    	$this.cmtInitFormEdit(oData.form_id);
+                    });
+                });
+        	}
+        },
+        'json'
+    );
+};
+
+BxDolCmts.prototype.cmtRemove = function(e, iCmtId) {
+    if (!this._confirm()) 
+    	return;
+
+    var $this = this;
+    var oParams = this._getDefaultActions();
+    oParams['action'] = 'Remove';
+    oParams['Cmt'] = iCmtId;
+
+    this._loadingInContent(e, true);
+
+    jQuery.post (
+        this._sActionsUrl,
+        oParams,
+        function(oData) {
+            $this._loadingInContent(e, false);
+
+            if(oData && oData.msg != undefined)
+                alert(oData.msg);
+
+            if(oData && oData.id != undefined) {
+            	$(e).parents('.bx-popup-applied:first:visible').dolPopupHide();
+
+            	$('#cmt' + oData.id).bx_anim('hide', $this._sAnimationEffect, $this._iAnimationSpeed, function() {
+                	var oCounter = $(this).parent('ul.cmts').siblings('.cmt-cont').find('.cmt-actions a.cmt-comment-replies span');
+                	if(oCounter)
+                		oCounter.html(oCounter.html() - 1);
+
+                	$(this).remove();
+                });
+            }
+        },
+        'json'
+    );
+};
+
+BxDolCmts.prototype.cmtLoad = function(oLink, iCmtParentId, iStart, iPerView)
+{
+	var $this = this;
+	var bButton = $(oLink).hasClass('bx-btn');
+
+	if(bButton)
+		this._loadingInButton(oLink, true);
+	else 
+		this._loading($(oLink).parents('ul.cmts:first'), true);
+
+	this._getCmts(oLink, {
+		CmtParent: iCmtParentId,
+		CmtStart: iStart,
+		CmtPerView: iPerView,
+		CmtBrowse: this._sBrowseType,
+		CmtFilter: this._sBrowseFilter,
+		CmtDisplay: this._sDisplayType
+	}, function(sListId, sContent) {
+		if(bButton)
+			$this._loadingInButton(oLink, false);
+		else 
+			$this._loading($(oLink).parents('ul.cmts:first'), false);
+
+		$this._cmtsReplace($(oLink).parents('li:first'), sContent);
+	});
+};
+
+BxDolCmts.prototype.cmtChangeDisplay = function(oLink, sType)
+{
+	var $this = this;
+	this._getCmts(oLink, {
+		CmtParent: 0,
+		CmtBrowse: this._sBrowseType,
+		CmtFilter: this._sBrowseFilter,
+		CmtDisplay: sType
+	}, function(sListId, sContent) {
+		$this._sDisplayType = sType;
+		$this._cmtsReplaceContent($(sListId), sContent);
+	});
+};
+
+BxDolCmts.prototype.cmtChangeBrowse = function(oLink, sType)
+{
+	var $this = this;
+	this._getCmts(oLink, {
+		CmtParent: 0,
+		CmtBrowse: sType,
+		CmtFilter: this._sBrowseFilter,
+		CmtDisplay: this._sDisplayType
+	}, function(sListId, sContent) {
+		$this._sBrowseType = sType;
+		$this._cmtsReplaceContent($(sListId), sContent);
+	});
+};
+
+BxDolCmts.prototype.cmtChangeFilter = function(oLink, sType)
+{
+	var $this = this;
+	this._getCmts(oLink, {
+		CmtParent: 0,
+		CmtBrowse: this._sBrowseType,
+		CmtFilter: sType,
+		CmtDisplay: this._sDisplayType
+	}, function(sListId, sContent) {
+		$this._sBrowseFilter = sType;
+		$this._cmtsReplaceContent($(sListId), sContent);
+	});
+};
+
+BxDolCmts.prototype.showReplacement = function(iCmtId)
+{
+    $('#cmt' + iCmtId + '-hidden').bx_anim('hide', this._sAnimationEffect, this._iAnimationSpeed, function(){
+        $(this).next('#cmt' + iCmtId).bx_anim('show', this._sAnimationEffect, this._iAnimationSpeed);
+    });
+};
+
+BxDolCmts.prototype.showMore = function(oLink)
+{
+	$(oLink).parent('span').next('span').show().prev('span').remove();
+};
+
+BxDolCmts.prototype.showImage = function(oLink, sUrl) {
+	$(this._sRootId + '-view-image-popup').dolPopupImage(sUrl, $(oLink).parents('.cmt-attached:first'));
+};
+
+BxDolCmts.prototype.toggleReply = function(e, iCmtParentId)
+{
+	var $this = this;
+	var sParentId = this._sRootId + ' #cmt' + iCmtParentId;
+	var fOnShow = function() {
+		$(this).find('textarea:first').focus();
+	};
+
+	var sReplyId = sParentId + ' > .cmt-reply';
+    if ($(sReplyId).length) {
+		$(sReplyId).bx_anim('toggle', this._sAnimationEffect, this._iAnimationSpeed, fOnShow);
+		return;
+    }
+
+	this._getForm(e, iCmtParentId, function(sForm) {
+		var oForm = $(sForm).hide().addClass('cmt-reply-' + $this._sPostFormPosition).addClass('cmt-reply-margin');
+        var oFormSibling = $(sParentId + ' > ul.cmts:first');
+        switch($this._sPostFormPosition) {
+        	case 'top':
+        		oFormSibling.before(oForm);
+        		break;
+
+        	case 'bottom':
+        		oFormSibling.after(oForm);
+        		break;
+        }
+
+        var sFormId = oForm.find('form').attr('id');
+        $this.cmtInitFormPost(sFormId);
+
+    	$(sParentId).children('.cmt-reply').bx_anim('toggle', $this._sAnimationEffect, $this._iAnimationSpeed, fOnShow);    		
+	});
+};
+
+BxDolCmts.prototype.toggleManagePopup = function(oLink, iCmtId) {
+	var oPopup = $('#' + this._sSystem + '-manage-' + iCmtId);
+	if(oPopup.is(':visible')) {
+		oPopup.dolPopupHide();
+		return;
+	}
+
+	oPopup.dolPopup({
+		pointer:{
+			el:$(oLink)
+		}
+	});
+};
+
+BxDolCmts.prototype.goTo = function(oLink, sGoToId, sBlinkIds, onLoad)
+{
+	$(oLink).parents('.bx-popup-applied:first:visible').dolPopupHide();
+
+	var $this = this;
+	this._getCmts(null, {
+		CmtParent: 0,
+		CmtBrowse: this._sBrowseType,
+		CmtFilter: this._sBrowseFilter,
+		CmtDisplay: this._sDisplayType,
+		CmtBlink: sBlinkIds
+	}, function(sListId, sContent) {
+		$this._cmtsReplaceContent($(sListId), sContent, function() {
+			location.hash = sGoToId;
+
+			if(typeof onLoad == 'function')
+				onLoad();
+		});
+	});
+};
+
+BxDolCmts.prototype.goToAndReply = function(oLink, sGoToId, sBlinkIds)
+{
+	var $this = this;
+	this.goTo(oLink, sGoToId, sBlinkIds, function() {
+		var aBlinkIds = sBlinkIds.split(",");
+		$.each(aBlinkIds, function(iIndex, iValue) {
+			$this.toggleReply(null, iValue);
+		});
+	});
+};
+
+/*----------------------------*/
+/*--- Live Updates methods ---*/
+/*----------------------------*/
+BxDolCmts.prototype.showLiveUpdate = function(oData)
+{
+	/*
+	 * Note. oData.count_old and oData.count_new are also available and can be checked or used in notification popup.  
+	 */
+	if(!oData.code)
+		return;
+
+	var $this = this;
+
+	var oNotifs = $(oData.code);
+	var sId = oNotifs.attr('id');
+	$('#' + sId).remove();
+
+	oNotifs.prependTo('body').dolPopup({
+    	position: 'fixed',
+    	left: '1rem',
+    	top: 'auto',
+    	bottom: '1rem',
+    	fog: false,
+    	onBeforeShow: function() {
+    	},
+    	onBeforeHide: function() {
+    	},
+    	onHide: function() {
+    		$this.resumeLiveUpdates();
+    	}
+    });
+};
+
+BxDolCmts.prototype.previousLiveUpdate = function(oLink)
+{
+	var fPrevious = function() {
+		var sClass = 'bx-popup-chain-item';
+		$(oLink).parents('.' + sClass + ':first').hide().prev('.' + sClass).show();
+	};
+
+	if(!this.pauseLiveUpdates(fPrevious));
+		fPrevious();
+};
+
+BxDolCmts.prototype.hideLiveUpdate = function(oLink)
+{
+	$(oLink).parents('.bx-popup-applied:visible:first').dolPopupHide();
+};
+
+BxDolCmts.prototype.resumeLiveUpdates = function(onLoad)
+{
+	if(!this._bLiveUpdatePaused)
+		return false;
+
+	var $this = this;
+	this.changeLiveUpdates('ResumeLiveUpdate', function() {
+		$this._bLiveUpdatePaused = false;
+
+		if(typeof onLoad == 'function')
+			onLoad();
+	});
+
+	return true;
+};
+
+BxDolCmts.prototype.pauseLiveUpdates = function(onLoad)
+{
+	if(this._bLiveUpdatePaused)
+		return false;
+
+	var $this = this;
+	this.changeLiveUpdates('PauseLiveUpdate', function() {
+		$this._bLiveUpdatePaused = true;
+
+		if(typeof onLoad == 'function')
+			onLoad();
+	});
+
+	return true;
+};
+
+BxDolCmts.prototype.changeLiveUpdates = function(sAction, onLoad)
+{
+	var $this = this;
+    var oParams = this._getDefaultActions();
+    oParams['action'] = sAction;
+
+	jQuery.get(
+	    this._sActionsUrl,
+	    oParams,
+	    function() {
+	    	if(typeof onLoad == 'function')
+				onLoad();
+	    }
+	);
+};
+
+/*----------------------------------*/
+/*--- Methods for internal usage ---*/
+/*----------------------------------*/
+BxDolCmts.prototype._getCmt = function (e, iCmtId)
+{
+    var $this = this;
+    var oData = this._getDefaultActions();
+    oData['action'] = 'GetCmt';
+    oData['Cmt'] = iCmtId;
+    oData['CmtBrowse'] = this._sBrowseType;
+    oData['CmtDisplay'] = this._sDisplayType;
+
+    this._loadingInBlock (e);
+
+    jQuery.post (
+        this._sActionsUrl,
+        oData,
+        function (oData) {
+            $this._loadingInBlock (e, false);
+
+            var sListId = $this._sRootId + ' #cmt' + oData.vparent_id + ' > ul';
+            var sReplyFormId = $this._sRootId + ' #cmt' + oData.parent_id + ' > .cmt-reply';
+
+            //--- Hide reply form ---//
+            if($(sReplyFormId).length)
+            	$(sReplyFormId).bx_anim('hide', $this._sAnimationEffect, $this._iAnimationSpeed);
+
+            //--- Some number of comments already loaded ---//
+            if($(sListId + ' > li.cmt').length)
+                $(sListId + ' > li.cmt:last').after($(oData.content).hide()).next('li.cmt:hidden').bxTime().bx_anim('toggle', $this._sAnimationEffect, $this._iAnimationSpeed);
+            //-- There is no comments at all ---//
+            else
+            	$(sListId).hide().html(oData.content).bxTime().bx_anim('show', $this._sAnimationEffect, $this._iAnimationSpeed);
+        },
+        'json'
+    );
+};
+
+BxDolCmts.prototype._getCmts = function (oElement, oRequestParams, onLoad)
+{
+    var $this = this;
+    var oData = this._getDefaultActions();    
+    oData['action'] = 'GetCmts';
+    oData = $.extend({}, oData, oRequestParams);
+
+    var sListId =  this._sRootId + ' #cmt' + oData['CmtParent'] + ' > ul:first';
+
+    if(oElement)
+        this._loadingInBlock(oElement, true);
+
+    jQuery.post (
+        this._sActionsUrl,
+        oData,
+        function(s) {
+        	if(oElement)
+        		$this._loadingInBlock(oElement, false);
+
+        	if(typeof onLoad == 'function')
+    			onLoad(sListId, s);
+        	else
+        		$this._cmtsAppend(sListId, s);
+        }
+    );
+};
+
+BxDolCmts.prototype._getForm = function (e, iCmtParentId, onLoad)
+{
+	var $this = this;
+
+    var oData = this._getDefaultActions();
+    oData['action'] = 'GetFormPost';
+    oData['CmtType'] = 'reply';
+    oData['CmtParent'] = iCmtParentId;
+    oData['CmtBrowse'] = this._sBrowseType;
+    oData['CmtDisplay'] = this._sDisplayType;
+
+    if(e)
+    	this._loadingInContent(e, true);
+
+    jQuery.post (
+        this._sActionsUrl,
+        oData,
+        function (s) {
+        	if(e)
+        		$this._loadingInContent(e, false);
+
+            if(typeof onLoad == 'function')
+    			onLoad(s);
+        }
+    );
+};
+
+BxDolCmts.prototype._cmtsAppend = function(sIdTo, sContent)
+{
+	$(sIdTo).append($(sContent).hide()).children(':hidden').bxTime().bx_anim('show', this._sAnimationEffect, this._iAnimationSpeed);
+};
+
+BxDolCmts.prototype._cmtsPrepend = function(sIdTo, sContent)
+{
+	$(sIdTo).prepend($(sContent).hide()).children(':hidden').bxTime().bx_anim('show', this._sAnimationEffect, this._iAnimationSpeed);
+};
+
+BxDolCmts.prototype._cmtsReplace = function(oReplace, sContent, onLoad)
+{
+	var $this = this;
+	$(oReplace).bx_anim('hide', this._sAnimationEffect, this._iAnimationSpeed, function() {
+		$(this).after($(sContent).hide()).nextAll(':hidden').bxTime().bx_anim('show', $this._sAnimationEffect, $this._iAnimationSpeed, function() {
+			$this._cmtsBlink($(this));
+
+			if(typeof onLoad == 'function')
+    			onLoad();
+		});
+		$(this).remove();
+	});
+};
+BxDolCmts.prototype._cmtsReplaceContent = function(oParent, sContent, onLoad)
+{
+	var $this = this;
+	$(oParent).bx_anim('hide', this._sAnimationEffect, this._iAnimationSpeed, function() {
+		$(this).html(sContent).bxTime().bx_anim('show', $this._sAnimationEffect, $this._iAnimationSpeed, function() {
+			$this._cmtsBlink($(this));
+
+			if(typeof onLoad == 'function')
+    			onLoad();
+		});
+	});
+};
+
+BxDolCmts.prototype._cmtsBlink = function(oParent)
+{
+	var sBlinkClass = 'cmt-blink';
+
+	oParent.find('.' + sBlinkClass + '-plate:visible').animate({
+		opacity: 0
+	}, 
+	5000, 
+	function() {
+		oParent.find('.' + sBlinkClass).removeClass(sBlinkClass);
+	});
+};
+
+BxDolCmts.prototype._getDefaultActions = function() {
+	var oDate = new Date();
+    return {
+        sys: this._sSystem,
+        id: this._iObjId,
+        _t: oDate.getTime()
+    };
+};
+
+BxDolCmts.prototype._loading = function(e, bShow) {
+	var oParent = $(e).length ? $(e) : $('body'); 
+	bx_loading(oParent, bShow);
+};
+
+BxDolCmts.prototype._loadingInBlock = function(e, bShow) {
+	var oParent = $(e).length ? $(e).parents('.bx-db-content:first') : $('body'); 
+	bx_loading(oParent, bShow);
+};
+
+BxDolCmts.prototype._loadingInContent = function(e, bShow) {
+	var oParent = $(e).length ? $(e).parents('li.cmt:first,.cmt-reply:first') : $('body'); 
+	bx_loading(oParent, bShow);
+};
+
+BxDolCmts.prototype._loadingInButton = function(e, bShow) {
+	if($(e).length)
+		bx_loading_btn($(e), bShow);
+	else
+		bx_loading($('body'), bShow);	
+};
+
+BxDolCmts.prototype._confirm = function() {
+    return confirm(_t('_are you sure?'));
+};
+
+/** @} */

+ 437 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/js/classes/BxDolGrid.js

@@ -0,0 +1,437 @@
+/**
+ * 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 sUrl = sUrlRoot + "grid.php?o=" + this._sObject + "&a=" + sAction;
+    var i;
+    var $this = this;
+
+    if (typeof(isConfirm) != 'undefined' && parseInt(isConfirm) == 1 && !confirm('undefined' == typeof(this._oConfirmMessages[sAction]) ? _t('_sys_grid_confirmation') : this._oConfirmMessages[sAction]))
+        return;
+
+    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 += '&filter=' + encodeURIComponent (this._sFilter);
+    if (this._sOrderField.length)
+        sUrl += '&order_field=' + encodeURIComponent (this._sOrderField);
+    if (this._sOrderDir.length)
+        sUrl += '&order_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);
+    });
+
+}
+
+BxDolGrid.prototype.processJson = function (oData, sAction, isDisableLoading) {
+    if (typeof(isDisableLoading) == 'undefined' || !isDisableLoading)
+        this.loading(false);
+
+    if (oData && undefined != oData.grid) {        
+        $('#' + this._sIdContainer).html(oData.grid);
+        this._onDataReloaded(true);
+    }
+    if (oData && undefined != oData.msg) {
+        alert(oData.msg);
+    }
+    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);
+    }
+}
+
+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._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 () {
+                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;
+};
+
+/** @} */

+ 141 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/js/classes/BxDolReport.js

@@ -0,0 +1,141 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function BxDolReport(options)
+{
+	this._sObjName = undefined == options.sObjName ? 'oReport' : options.sObjName; // javascript object name, to run current object instance from onTimer
+	this._sSystem = options.sSystem; // current comment system
+	this._iAuthorId = options.iAuthorId; // this comment's author ID.
+    this._iObjId = options.iObjId; // this object id comments
+
+    this._sActionsUri = 'report.php';
+    this._sActionsUrl = options.sRootUrl + this._sActionsUri; // actions url address
+
+    this._sAnimationEffect = 'fade';
+    this._iAnimationSpeed = 'slow';
+    this._sSP = undefined == options.sStylePrefix ? 'bx-report' : options.sStylePrefix;
+    this._aHtmlIds = options.aHtmlIds;
+
+    this._oParent = null;
+}
+
+BxDolReport.prototype.toggleByPopup = function(oLink) {
+	var $this = this;
+    var oData = this._getDefaultParams();
+    oData['action'] = 'GetReportedBy';
+
+	$(oLink).dolPopupAjax({
+		id: this._aHtmlIds['by_popup'], 
+		url: bx_append_url_params(this._sActionsUri, oData)
+	});
+};
+
+BxDolReport.prototype.report = function(oLink) {
+	var $this = this;
+    var oData = this._getDefaultParams();
+    oData['action'] = 'Report';
+
+    this._oParent = $(oLink);
+
+    $.get(
+    	this._sActionsUrl,
+    	oData,
+    	function(oData) {
+    		$this.processJson(oData, this._oParent);
+    	},
+    	'json'
+    );
+};
+
+BxDolReport.prototype.onReport = function(oData, oElement)
+{
+	if(oData && oData.msg != undefined)
+        alert(oData.msg);
+
+	if(oData && oData.code != 0)
+        return;
+
+	if(oData && oData.label_icon)
+		$(oElement).find('.sys-icon').attr('class', 'sys-icon ' + oData.label_icon);
+
+	if(oData && oData.label_title) {
+		$(oElement).attr('title', oData.label_title);
+		$(oElement).find('span').html(oData.label_title);
+	}
+
+	if(oData && oData.disabled)
+		$(oElement).removeAttr('onclick').addClass($(oElement).hasClass('bx-btn') ? 'bx-btn-disabled' : 'bx-report-disabled');
+
+    var oCounter = this._getCounter(oElement);
+    if(oCounter && oCounter.length > 0) {
+    	oCounter.html(oData.countf);
+
+    	oCounter.parents('.' + this._sSP + '-counter-holder:first').bx_anim(oData.count > 0 ? 'show' : 'hide');
+    }
+};
+
+BxDolReport.prototype.processJson = function(oData, oElement) {
+	oElement = oElement != undefined ? oElement : this._oParent;
+
+    //--- Show Message
+    if(oData && oData.msg != undefined)
+        alert(oData.msg);
+    if(oData && oData.message != undefined)
+        alert(oData.message);
+
+    //--- Show Popup
+    if(oData && oData.popup != undefined) {
+    	$('#' + oData.popup_id).remove();
+
+    	$(oData.popup).hide().prependTo('body').dolPopup({
+    		pointer: {
+    			el: oElement
+    		},
+            fog: {
+				color: '#fff',
+				opacity: .7
+            }
+        });
+    }
+
+    //--- Evaluate JS code
+    if (oData && oData.eval != undefined)
+        eval(oData.eval);
+};
+
+BxDolReport.prototype._getButtons = function(oElement) {
+	if($(oElement).hasClass(this._sSP))
+		return $(oElement).find('.' + this._sSP + '-button');
+	else
+		return $(oElement).parents('.' + this._sSP + ':first').find('.' + this._sSP + '-button');
+};
+
+BxDolReport.prototype._getCounter = function(oElement) {
+	if($(oElement).hasClass(this._sSP))
+		return $(oElement).find('.' + this._sSP + '-counter');
+	else 
+		return $(oElement).parents('.' + this._sSP + ':first').find('.' + this._sSP + '-counter');
+};
+
+BxDolReport.prototype._loadingInButton = function(e, bShow) {
+	if($(e).length)
+		bx_loading_btn($(e), bShow);
+	else
+		bx_loading($('body'), bShow);	
+};
+
+BxDolReport.prototype._getDefaultParams = function() {
+	var oDate = new Date();
+    return {
+        sys: this._sSystem,
+        object_id: this._iObjId,
+        _t: oDate.getTime()
+    };
+};
+
+/** @} */

+ 760 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/js/functions.js

@@ -0,0 +1,760 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+function processJsonData(oData) {
+	var $this = this;
+
+	if(oData && oData.message != undefined && oData.message.length != 0)
+    	alert(oData.message);
+
+    if(oData && oData.reload != undefined && parseInt(oData.reload) == 1)
+    	document.location = document.location;
+
+    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').dolPopup(oOptions);
+    }
+
+    if (oData && oData.eval != undefined)
+        eval(oData.eval);
+};
+
+function getHtmlData( elem, url, callback, method , confirmation)
+{
+    if ('undefined' != typeof(confirmation) && confirmation && !confirm(_t('_are you sure?'))) 
+        return false;
+
+    // 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);
+        });
+
+    }
+}
+
+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'));
+    	if(!isNaN(iStart) && !isNaN(iPerPage) && sUrl.indexOf('start') == -1 && sUrl.indexOf('per_page') == -1)
+    		sUrl = bx_append_url_params(sUrl, 'start=' + iStart + '&per_page=' + iPerPage);
+    }
+
+    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 bUseSvg = true;
+
+    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">' + (bUseSvg ? bx_loading_svg('colored', 'bx-btn-height') : '') + '</b>');
+
+    if(!bUseSvg)
+    	bx_loading_animate(oButton.find('.bx-loading-ajax-btn'), aSpinnerSmallOpts);    
+}
+
+function bx_loading_content (oElement, bEnable, isReplace) {
+	var bUseSvg = true;
+
+    var oBlock = $(oElement);
+    var oLoading = $('<div class="bx-loading-ajax bx-def-z-index-front">' + (bUseSvg ? 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(!bUseSvg)
+        	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),
+            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) {
+	//--- 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('.bx-menu-item-addon');
+
+		if(oMenuItemAddon.length > 0)
+			oMenuItemAddon.html(oData.count_new);
+		else
+			oMenuItem.append(oData.code.replace('{count}', oData.count_new));
+	}
+
+	//--- Update Parent Menu Item
+	if(oData.mi_parent) {
+		var oMenuItem = $('.bx-menu-object-' + oData.mi_parent.menu_object + ' #bx-menu-toolbar-item-' + oData.mi_parent.menu_item);
+		var oMenuItemAddon = oMenuItem.find('.bx-menu-item-addon');
+
+		if(oMenuItemAddon.length > 0)
+			oMenuItemAddon.html(parseInt(oMenuItemAddon.html()) + (oData.count_new - oData.count_old));
+		else
+			oMenuItem.append(oData.code.replace('{count}', oData.count_new));
+	}
+}
+/**
+ * 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) {
+            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) {
+    if ('undefined' != typeof(bConfirm) && bConfirm && !confirm(_t('_are you sure?')))
+        return;
+                
+    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) {
+            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
+    });
+}
+
+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 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];
+}
+
+/** @} */

+ 39 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/js/jquery.dolConverLinks.js

@@ -0,0 +1,39 @@
+(function( $ ){
+	$.fn.dolConverLinks = function(options) {
+		if(bx_get_param('sys_embedly_api_key') != '' && bx_get_param('sys_embedly_api_pattern') != '')
+			return this.dolEmbedly(options);
+
+		if(bx_get_param('sys_iframely_api_key') != '')
+			return this.dolIframely(options);
+
+		return this;
+	};
+
+	$.fn.dolIframely = function(options) {        
+		return this.each(function() {
+			iframely.load($(this).get(0));
+		});
+	};
+
+	$.fn.dolEmbedly = function(options) {
+        var o = $.extend({}, {'max-width':900}, options);
+		var sEmbedlyKey = bx_get_param('sys_embedly_api_key');
+		var sEmbedlyPattern = bx_get_param('sys_embedly_api_pattern');
+		if(!sEmbedlyKey || !sEmbedlyPattern)
+			return this;
+
+        var eBox = $(this).parents('.bx-db-content, .bx-def-box');
+        var iMaxWidth = eBox.size() ? eBox.innerWidth() : $(window).width() - 70;
+
+        if (iMaxWidth > o['max-width']) iMaxWidth = o['max-width'];
+        
+		return this.each(function() {
+			$(this).embedly({
+                key: sEmbedlyKey,
+                query: {maxwidth: iMaxWidth},
+                // only videos/sound/images are supported, to generate own list goto http://embed.ly/tools/generator
+                urlRe: sEmbedlyPattern
+            });
+		});
+	};
+})( jQuery );

+ 596 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/js/jquery.dolPopup.js

@@ -0,0 +1,596 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+(function($) {
+
+    $.fn.dolPopupDefaultOptions = {
+        closeOnOuterClick: true,
+        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'
+    }; 
+
+    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 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()
+            });
+        });
+
+        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) {
+                    if (o.speed > 0)
+                        $('#bx-popup-fog').fadeIn(o.speed);
+                    else
+                        $('#bx-popup-fog').show();
+                }
+
+                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.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');
+
+            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);
+
+                }, o.speed);
+
+            } else {
+
+                if (o.fog)
+                    $('#bx-popup-fog').hide();
+
+                $el.hide(function() {
+                	if(typeof(onHide) == 'function')
+                    	onHide($el);
+                });
+            }
+            
+            _dolPopupLockScreen(false);
+        });
+    };
+
+
+    $.fn.dolPopupAjax = function(options) { 
+        
+        if ('undefined' == typeof(options) || 'object' != typeof(options) || 'undefined' == typeof(options.url))
+            return;
+
+        if ('undefined' == typeof(options.container))
+            options.container = '.bx-popup-content-wrapped';
+
+        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);
+
+                $('#' + sPopupId).dolPopup($.extend({}, options, {
+                    pointer: oPointerOptions,
+                    onHide: function () {
+                        if (!$.isWindow(e[0]))
+                            bx_menu_on(e, false);
+                    }
+                }));
+
+            } 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('body').find(options.container).hide();
+
+                $('#' + sPopupId).addClass($('#bx-popup-loading').attr('class'));
+
+                var oLoading = $('#' + sPopupId + ' .bx-popup-loading-wrapped');
+                bx_loading_content(oLoading, true, true);
+
+                $('#' + sPopupId).dolPopup($.extend({}, options, {
+                    pointer: oPointerOptions,
+                    onHide: function () {
+                        if (!$.isWindow(e[0]))
+                            bx_menu_on(e, false);
+                    }
+                }));
+
+                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 () {
+                	if($('#' + sPopupId).find('img').length > 0)
+                		$('#' + sPopupId).find('img').load(fOnLoad);
+                	else
+                		fOnLoad();
+                });
+            }
+        });
+    };
+
+
+    $.fn.dolPopupImage = function(sUrl, sLoadingElement) {
+
+        return this.each(function() {
+
+            var ePopup = $(this);
+            var k = .95,
+                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 () {
+
+                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 if ((w * k - eImg.width) < (h * k - eImg.height)) {
+                    ePopup.find('img').css({
+                        width: '' + (eImg.width > w * k ? parseInt(w * k) : eImg.width) + 'px',
+                        height: 'auto'
+                    }).addClass('bx-constrain-width');
+                } else {
+                    ePopup.find('img').css({
+                        width: 'auto',
+                        height: '' + (eImg.height > h * k ? parseInt(h * k) : eImg.height) + '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._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.position({
+                        of: window,                    
+                        my: 'center center+' + (oElement.outerHeight() > $(window).height() ? parseInt((oElement.outerHeight() - $(window).height()) / 2) : '0'),
+                        at: 'center center',
+                        collision: 'none none'
+                    });
+            	};
+
+            	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);
+
+/** @} */

+ 264 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/js/jquery.webForms.js

@@ -0,0 +1,264 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+(function($) {
+
+    $.fn.addWebForms = function() {
+
+        // switchers
+        $('.bx-switcher-cont', this).each(function() {
+            var eSwitcher = $(this);
+            var eInput = $(this).find('input');
+
+            if (eSwitcher.hasClass('bx-form-switcher-processed'))
+                return;
+            eSwitcher.addClass('bx-form-switcher-processed');
+
+            eSwitcher.on('click', function() {
+                var $this = $(this);
+                if ($this.hasClass('on')) {
+                    $this.find('input').prop('checked', false).trigger('change');
+                    $this.removeClass('on').addClass('off');
+                } else {
+                    $this.removeClass('off').addClass('on');
+                    $this.find('input').prop('checked', true).trigger('change');
+                }
+                return false;
+            });
+
+            eInput.on('enable', function () {
+                if (!$(this).prop('checked'))
+                    $(this).prop('checked', true).trigger('change');
+                if (!eSwitcher.hasClass('on'))
+                    eSwitcher.removeClass('off').addClass('on');
+            });
+
+            eInput.on('disable', function () {
+                if ($(this).prop('checked'))
+                    $(this).prop('checked', false).trigger('change');
+                if (!eSwitcher.hasClass('off'))
+                    eSwitcher.removeClass('on').addClass('off');
+            });
+        });
+
+        // collapsable headers 
+        $('.bx-form-collapsable', this).each(function() {
+
+            var eFormSection = $(this);
+
+            if (eFormSection.hasClass('bx-form-js-processed'))
+                return;
+
+            eFormSection.addClass('bx-form-js-processed');
+
+            var fCallback = function() {
+
+                if (eFormSection.hasClass('bx-form-collapsed')) {
+                    // show
+                    eFormSection.removeClass('bx-form-collapsed');
+
+                    if (eFormSection.hasClass('bx-form-section-hidden')) {
+                        $('.bx-form-section-content:first', eFormSection).hide();
+                        eFormSection.removeClass('bx-form-section-hidden');
+                    }
+
+                    $('.bx-form-section-content:first', eFormSection).slideDown(function () {
+                        eFormSection.removeClass('bx-form-section-hidden');
+
+                        //--- fire custom event 'bx_show'
+                        eFormSection.trigger('bx_show');
+                    }); 
+
+                } else {
+                    // hide
+                    eFormSection.addClass('bx-form-collapsed');
+
+                    $('.bx-form-section-content:first', eFormSection).slideUp(function () {                    
+                        eFormSection.addClass('bx-form-collapsed bx-form-section-hidden');
+
+                        //--- fire custom event 'bx_hide'
+                        eFormSection.trigger('bx_hide');
+                    }); 
+                }
+
+            };
+
+            $('<u class="bx-form-section-toggler"><i class="sys-icon chevron-right"></i></u>').prependTo($('legend', eFormSection));
+            $('legend:first', eFormSection).click(fCallback);
+        });
+
+        
+        $("input", this).each(function() {
+        	var oInput = $(this);
+
+            var onCreateRange = function (event, ui) {
+                var eInput = $(this).parent().find('input');
+                var iSliderWidth = eInput.innerWidth() - 160;
+                $(this).css('width',  iSliderWidth + 'px').css('top', (eInput.innerHeight() - $(this).outerHeight())/2 + 1 + 'px');
+                eInput.css('padding-right', (iSliderWidth + parseFloat($(this).css('margin-right')) + 10) + 'px');                        
+            };
+
+            // DoubleRange
+            if (this.getAttribute("type") == 'doublerange') {
+
+                var cur = $(this);
+
+                if (cur.hasClass('bx-form-doublerange-processed'))
+                    return;
+                cur.addClass('bx-form-doublerange-processed');
+
+                var $slider = $('<div class="bx-def-margin-right"></div>').insertAfter(cur);
+
+                var iMin = cur.attr("min") ? parseFloat(cur.attr("min"), 10) : 0;
+                var iMax = cur.attr("max") ? parseFloat(cur.attr("max"), 10) : 100;
+                var sRangeDv = cur.attr("range-divider") ? cur.attr("range-divider") : '-';
+
+                var funcGetValues = function (e) {
+
+                    var values = e.val().split(sRangeDv, 2); // get values
+
+                    if (typeof(values[0]) != 'undefined' && values[0].length)
+                        values[0] = parseFloat(values[0]);
+                    else
+                        values[0] = iMin;
+
+                    if (typeof(values[1]) != 'undefined' && values[1].length)
+                        values[1] = parseFloat(values[1]);
+                    else
+                        values[1] = iMax;
+
+                    return values;
+                };
+
+                var onChange = function(e, ui) {
+                    values = ui.values;
+                    cur.val( values[0] + sRangeDv + values[1] );
+                };
+
+                $slider.slider({
+                    range: true,
+                    min: iMin,
+                    max: iMax,
+                    step: parseFloat(cur.attr("step")) ? parseFloat(cur.attr("step")) : 1,
+                    values: funcGetValues(cur),
+                    change: onChange,
+                    slide: onChange,
+                    create: onCreateRange
+                });
+
+                cur.bind('change', function () {
+                    var values = funcGetValues($(this));
+                    $slider.slider("option", "values", values);
+                });
+
+            }
+
+            // Single range or slider
+            else if (this.getAttribute("type") == 'slider') {
+
+                var cur = $(this);
+
+                if (cur.hasClass('bx-form-range-processed'))
+                    return;
+                cur.addClass('bx-form-range-processed');
+
+                var $slider = $('<div class="bx-def-margin-right"></div>').insertAfter(cur)
+
+                $slider.css('width', ($slider.parent().innerWidth() - cur.outerWidth() - 50) + 'px');
+
+                var iMin = cur.attr("min") ? parseFloat(cur.attr("min"), 10) : 0;
+                var iMax = cur.attr("max") ? parseFloat(cur.attr("max"), 10) : 100;
+
+                var onChange = function(e, ui) {
+                    cur.val( ui.value );
+                };
+
+                $slider.slider({
+                    min: iMin,
+                    max: iMax,
+                    step: parseFloat(cur.attr("step")) ? parseFloat(cur.attr("step")) : 1,
+                    value: cur.val(),
+                    change: onChange,
+                    slide: onChange,
+                    create: onCreateRange
+                });
+
+                cur.bind('change', function () {
+                    $slider.slider("option", "value", $(this).val());
+                });
+
+            }
+
+			// Date/Time pickers
+			if (this.getAttribute("type") == "date" || this.getAttribute("type") == "date_calendar" || this.getAttribute("type") == "datetime" || this.getAttribute("type") == "date_time" || this.getAttribute("type") == "datepicker") {
+                if ($(this).hasClass('bx-form-datepicker-processed'))
+                    return;
+                $(this).addClass('bx-form-datepicker-processed');
+
+                var iYearMin = '1900';
+                var iYearMax = '2100';
+                var m;
+
+                if ($(this).attr('min') && (m = $(this).attr('min').match(/^(\d{4})/)))
+                    sYearMin = m[1];
+                if ($(this).attr('max') && (m = $(this).attr('max').match(/^(\d{4})/)))
+                    sYearMax = m[1];
+
+                var onBeforeShow = function(oInput, oInstance) {
+                	$(oInstance.dpDiv).addClass('bx-form-datepicker-modal');
+                };
+
+                if (this.getAttribute("type") == "date" || this.getAttribute("type") == "date_calendar" || this.getAttribute("type") == "datepicker") { // Date picker
+
+                    $(this).datepicker({
+                        changeYear: true,
+                        changeMonth: true,
+                        dateFormat: 'yy-mm-dd',
+                        //defaultDate: '-22y',
+                        yearRange: iYearMin + ':' + iYearMax,
+                        beforeShow: onBeforeShow
+                    });
+
+                } else if(this.getAttribute("type") == "datetime" || this.getAttribute("type") == "date_time") { // DateTime picker
+
+                    $(this).datetimepicker({
+                        changeYear: true,
+                        changeMonth: true,
+                        dateFormat: 'yy-mm-dd',
+                        beforeShow: onBeforeShow
+                    });
+                }
+
+                if (window.navigator.appVersion.search(/Chrome\/(.*?) /) == -1 || parseInt(window.navigator.appVersion.match(/Chrome\/(\d+)\./)[1], 10) < 24)
+                    if( this.getAttribute("allow_input") == null)
+                        $(this).attr('readonly', 'readonly');
+			}
+
+			// RGB/RGBA pickers
+			if(this.getAttribute('type') == 'text' && (oInput.hasClass('bx-form-input-rgb') || oInput.hasClass('bx-form-input-rgba')) && !oInput.hasClass('minicolors-input')) {
+				oInput.minicolors({
+                    control: oInput.attr('data-control') || 'hue',
+                    format: oInput.attr('data-format') || 'rgb',
+                    letterCase: oInput.attr('data-letterCase') || 'lowercase',
+                    opacity: oInput.hasClass('bx-form-input-rgba'),
+                    position: oInput.attr('data-position') || 'bottom left',
+                    theme: 'bootstrap'
+                });
+				
+			}
+        });
+        return this;
+    };
+
+})(jQuery);
+
+$(document).ready(function() {
+    $(this).addWebForms();
+});
+
+/** @} */

+ 92 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/params.inc.php

@@ -0,0 +1,92 @@
+<?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_START', microtime ());
+
+//--- Main URLs ---//
+define('BX_DOL_URL_PLUGINS', BX_DOL_URL_ROOT . 'plugins/');
+define('BX_DOL_URL_PLUGINS_PUBLIC', BX_DOL_URL_ROOT . 'plugins_public/');
+define('BX_DOL_URL_MODULES', BX_DOL_URL_ROOT . 'modules/');
+define('BX_DOL_URL_CACHE_PUBLIC', BX_DOL_URL_ROOT . 'cache_public/');
+define('BX_DOL_URL_BASE', BX_DOL_URL_ROOT . 'template/');
+
+//--- Main Pathes ---//
+define('BX_DIRECTORY_PATH_INC', BX_DIRECTORY_PATH_ROOT . 'inc/');
+define('BX_DIRECTORY_PATH_BASE', BX_DIRECTORY_PATH_ROOT . 'template/');
+define('BX_DIRECTORY_PATH_CACHE', BX_DIRECTORY_PATH_ROOT . 'cache/');
+define('BX_DIRECTORY_PATH_CACHE_PUBLIC', BX_DIRECTORY_PATH_ROOT . 'cache_public/');
+define('BX_DIRECTORY_PATH_CLASSES', BX_DIRECTORY_PATH_ROOT . 'inc/classes/');
+define('BX_DIRECTORY_PATH_PLUGINS', BX_DIRECTORY_PATH_ROOT . 'plugins/');
+define('BX_DIRECTORY_PATH_PLUGINS_PUBLIC', BX_DIRECTORY_PATH_ROOT . 'plugins_public/');
+define('BX_DIRECTORY_PATH_MODULES', BX_DIRECTORY_PATH_ROOT . 'modules/');
+define('BX_DIRECTORY_PATH_TMP', BX_DIRECTORY_PATH_ROOT . 'tmp/');
+define('BX_DIRECTORY_PATH_LOGS', BX_DIRECTORY_PATH_ROOT . 'logs/');
+define('BX_DIRECTORY_STORAGE', BX_DIRECTORY_PATH_ROOT . 'storage/');
+
+//--- System settings ---//
+define('BX_DOL_DIR_RIGHTS', 0777);
+define('BX_DOL_FILE_RIGHTS', 0666);
+
+define('BX_DOL_STORAGE_OBJ_IMAGES', 'sys_images');
+
+if (!defined('BX_DOL_INT_MAX'))
+    define('BX_DOL_INT_MAX', 2147483647);
+
+define('BX_DOL_TRANSCODER_OBJ_ICON_APPLE', 'sys_icon_apple');
+define('BX_DOL_TRANSCODER_OBJ_ICON_FACEBOOK', 'sys_icon_facebook');
+define('BX_DOL_TRANSCODER_OBJ_ICON_FAVICON', 'sys_icon_favicon');
+define('BX_DOL_TRANSCODER_OBJ_COVER', 'sys_cover');
+
+//--- Module types ---//
+if (!defined('BX_DOL_MODULE_TYPE_MODULE')) {
+    define('BX_DOL_MODULE_TYPE_MODULE', 'module');
+    define('BX_DOL_MODULE_TYPE_LANGUAGE', 'language');
+    define('BX_DOL_MODULE_TYPE_TEMPLATE', 'template');
+}
+
+//--- Payment types ---//
+define('BX_PAYMENT_TYPE_SINGLE', 'single');
+define('BX_PAYMENT_TYPE_RECURRING', 'recurring');
+
+//--- Studio settings ---//
+define('BX_DOL_STUDIO_FOLDER', 'studio');
+
+define('BX_DOL_URL_STUDIO', BX_DOL_URL_ROOT . BX_DOL_STUDIO_FOLDER . '/');
+define('BX_DOL_URL_STUDIO_BASE', BX_DOL_URL_STUDIO . 'template/');
+
+define('BX_DOL_DIR_STUDIO', BX_DIRECTORY_PATH_ROOT . BX_DOL_STUDIO_FOLDER . '/');
+define('BX_DOL_DIR_STUDIO_INC', BX_DOL_DIR_STUDIO . 'inc/');
+define('BX_DOL_DIR_STUDIO_CLASSES', BX_DOL_DIR_STUDIO . 'classes/');
+define('BX_DOL_DIR_STUDIO_BASE', BX_DOL_DIR_STUDIO . 'template/');
+
+//--- User Roles ---//
+define('BX_DOL_ROLE_GUEST', 0);
+define('BX_DOL_ROLE_MEMBER', 1);
+define('BX_DOL_ROLE_ADMIN', 2);
+
+//--- Profile Statuses ---//
+define('BX_PROFILE_STATUS_SUSPENDED', 'suspended'); ///< profile status - suspended, profile is syspended by admin/moderator and usually can't access the site
+define('BX_PROFILE_STATUS_ACTIVE', 'active'); ///< profile status - active, profile is active on the site
+define('BX_PROFILE_STATUS_PENDING', 'pending'); ///< profile status - pending, default method of approving is manual approving
+
+//--- Include files needed for basic functionality ---//
+require_once(BX_DIRECTORY_PATH_CLASSES . 'BxDol.php');
+require_once(BX_DIRECTORY_PATH_INC . 'utils.inc.php');
+require_once(BX_DIRECTORY_PATH_INC . 'version.inc.php');
+
+spl_autoload_register('bx_autoload');
+
+BxDolDb::getInstance();
+
+require_once(BX_DIRECTORY_PATH_INC . 'profiles.inc.php');
+
+$o = new BxDolAlerts('system', 'begin', 0);
+$o->alert();
+
+/** @} */

+ 1559 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/inc/utils.inc.php

@@ -0,0 +1,1559 @@
+<?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 $mixedData;
+    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)
+{
+    $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);
+}
+
+/**
+ * Send system email 
+ */
+function sendMailTemplateSystem($sTemplateName, $aReplaceVars = array(), $iEmailType = BX_EMAIL_SYSTEM)
+{
+    $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);
+}
+/**
+ * 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
+ * @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())
+{
+    // 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) {
+        if(!is_array($aPlus))
+            $aPlus = array();
+        $oEmailTemplates = BxDolEmailTemplates::getInstance();
+        $sMailSubject = $oEmailTemplates->parseContent($sMailSubject, $aPlus, $iRecipientID);
+        $sMailBody = $oEmailTemplates->parseContent($sMailBody, $aPlus, $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']);
+    }
+
+    // send mail
+    if( 'html' == $sEmailFlag) {
+        $sMailHeader = "Content-type: text/html; charset=UTF-8\r\n" . $sMailHeader;
+        $iSendingResult = mail( $sRecipientEmail, $sMailSubject, $sMailBody, $sMailHeader, $sMailParameters );
+    } else {
+        $sMailHeader = "Content-type: text/plain; charset=UTF-8\r\n" . $sMailHeader;
+        $sMailBody = html2txt($sMailBody);
+        $iSendingResult = mail( $sRecipientEmail, $sMailSubject, html2txt($sMailBody), $sMailHeader, $sMailParameters );
+    }
+
+    // system alert
+    if (!$isDisableAlert)
+        bx_alert('system', 'send_mail', (isset($aRecipientInfo['ID']) ? $aRecipientInfo['ID'] : 0), '', $aAlert);
+
+    return $iSendingResult;
+}
+
+/*
+ * 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');
+
+        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',
+		    ));
+		}
+
+        $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); 
+            }
+
+        }
+    }
+
+    return $aResult;
+}
+
+function bx_parse_html_tag ($sContent, $sTag, $sAttrNameName, $sAttrNameValue, $sAttrContentName, $sCharset = false)
+{
+    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);
+
+    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();
+}
+
+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]));
+    }
+
+    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);    
+}
+
+/** @} */

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

+ 92 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/general/classes/BxBaseModGeneralCmtsSearchResult.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    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(),
+        	'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, 'perPage' => 3),
+            '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($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'
+        );
+    }
+}
+
+/** @} */

+ 92 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/general/classes/BxBaseModGeneralConfig.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    BaseGeneral Base classes for modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModGeneralConfig extends BxDolModuleConfig
+{
+    public $CNF;
+
+    protected $_aObjects;
+    protected $_aPrefixes;
+    protected $_aJsClasses;
+    protected $_aJsObjects;
+    protected $_aGridObjects;
+
+    function __construct($aModule)
+    {
+        parent::__construct($aModule);
+
+        $this->CNF = array();
+
+        $this->_aObjects = array();
+        $this->_aPrefixes = array();
+        $this->_aJsClasses = array();
+        $this->_aJsObjects = array();
+        $this->_aGridObjects = array();
+    }
+
+    public function getObject($sType = '')
+    {
+    	if(empty($sType))
+            return $this->_aObjects;
+
+        return isset($this->_aObjects[$sType]) ? $this->_aObjects[$sType] : '';
+    }
+
+	public function getPrefix($sType = '')
+    {
+    	if(empty($sType))
+            return $this->_aPrefixes;
+
+        return isset($this->_aPrefixes[$sType]) ? $this->_aPrefixes[$sType] : '';
+    }
+
+	public function getJsClass($sType)
+    {
+        return isset($this->_aJsClasses[$sType]) ? $this->_aJsClasses[$sType] : '';
+    }
+
+    public function getJsObject($sType)
+    {
+		return isset($this->_aJsObjects[$sType]) ? $this->_aJsObjects[$sType] : '';
+    }
+
+    public function getGridObject($sType)
+    {
+		return isset($this->_aGridObjects[$sType]) ? $this->_aGridObjects[$sType] : '';
+    }
+
+    /*
+     * Note. The first Transcoder in the array $aTranscoders has the highest priority. 
+     */
+    public function getImageUrl($iId, $aTranscoders)
+    {
+        $sResult = '';
+        if(empty($iId) || empty($aTranscoders) || !is_array($aTranscoders))
+            return $sResult;
+
+        foreach($aTranscoders as $sTranscoder) {
+            if(empty($this->CNF[$sTranscoder])) 
+                continue;
+
+            $oTranscoder = BxDolTranscoderImage::getObjectInstance($this->CNF[$sTranscoder]);
+        	if(!$oTranscoder)
+        	    continue;
+
+            $sResult = $oTranscoder->getFileUrl($iId);
+            if(!empty($sResult))
+                break;
+        }
+
+        return $sResult;
+    }
+}
+
+/** @} */

+ 153 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/general/classes/BxBaseModGeneralDb.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
+ *
+ * @{
+ */
+
+/*
+ * 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'] . "`";
+
+                $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;
+
+                        default:
+                             $sSearchValue = " " . $aSearchParam['operator'] . " :" . $sSearchParam;
+                             $aMethod['params'][1][$sSearchParam] = $aSearchParam['value'];                             
+                    }
+
+                    $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'] . "` = ?", $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'] . "`");
+
+        if (!($aFields = explode(',', getParam($CNF['PARAM_SEARCHABLE_FIELDS']))))
+            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, ', ') . ")");
+    }
+
+}
+
+/** @} */

+ 388 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/general/classes/BxBaseModGeneralFormsEntryHelper.php

@@ -0,0 +1,388 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    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 ()
+    {
+        return BxDolForm::getObjectInstance($this->_oModule->_oConfig->CNF['OBJECT_FORM_ENTRY'], $this->_oModule->_oConfig->CNF['OBJECT_FORM_ENTRY_DISPLAY_ADD'], $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 ()
+    {
+        return BxDolForm::getObjectInstance($this->_oModule->_oConfig->CNF['OBJECT_FORM_ENTRY'], $this->_oModule->_oConfig->CNF['OBJECT_FORM_ENTRY_DISPLAY_DELETE'], $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)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // check and display form
+        $oForm = $this->getObjectFormAdd();
+        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);
+
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        return array('code' => 0, 'message' => '', 'content' => $aContentInfo);
+    }
+
+    public function addDataForm ()
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        // check access
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->_oModule->checkAllowedAdd())) {
+            $oProfile = BxDolProfile::getInstance();
+            $aProfileInfo = $oProfile->getInfo();
+            if ($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();
+        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;
+
+        // perform action
+        $this->_oModule->checkAllowedAdd(true);
+
+        // redirect
+        list ($oProfile, $aContentInfo) = $this->_getProfileAndContentData($iContentId);
+        $this->redirectAfterAdd($aContentInfo);
+    }
+    
+    protected 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)
+    {
+        $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->checkAllowedEdit($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;
+
+        // 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)
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        if (false === $sDisplay)
+            $sDisplay = $CNF['OBJECT_FORM_ENTRY_DISPLAY_DELETE'];
+
+        // 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->checkAllowedDelete($aContentInfo)))
+            return MsgBox($sMsg);
+
+        // check and display form
+        $oForm = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $sDisplay, $this->_oModule->_oTemplate);
+        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 = BxDolForm::getObjectInstance($CNF['OBJECT_FORM_ENTRY'], $CNF['OBJECT_FORM_ENTRY_DISPLAY_DELETE'], $this->_oModule->_oTemplate);
+
+        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->keywordsFields($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']);
+            if ($oMetatags->keywordsIsEnabled())
+                $oMetatags->keywordsAddAuto($aContentInfo[$CNF['FIELD_ID']], $aContentInfo, $CNF, $CNF['OBJECT_FORM_ENTRY_DISPLAY_EDIT']);
+            if ($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 ($oMetatags->keywordsIsEnabled() && $aContentInfo)
+                $oMetatags->keywordsAddAuto($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']);
+        }
+    }
+}
+
+/** @} */

+ 177 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/general/classes/BxBaseModGeneralGridAdministration.php

@@ -0,0 +1,177 @@
+<?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_DOL_MANAGE_TOOLS_ADMINISTRATION', 'administration');
+define('BX_DOL_MANAGE_TOOLS_COMMON', 'common');
+
+class BxBaseModGeneralGridAdministration extends BxTemplGrid
+{
+	protected $MODULE;
+	protected $_oModule;
+
+	protected $_sManageType;
+	protected $_sParamsDivider;
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+    	$this->_oModule = BxDolModule::getInstance($this->MODULE);
+    	if(!$oTemplate)
+			$oTemplate = $this->_oModule->_oTemplate;
+
+        parent::__construct ($aOptions, $oTemplate);
+
+        $this->_sManageType = BX_DOL_MANAGE_TOOLS_ADMINISTRATION;
+        $this->_sParamsDivider = '#-#';
+
+        $this->_sDefaultSortingOrder = 'DESC';
+    }
+
+	public function performActionDelete($aParams = array())
+    {
+    	$CNF = &$this->_oModule->_oConfig->CNF;
+
+        $iAffected = 0;
+        $aIds = bx_get('ids');
+        if(!$aIds || !is_array($aIds)) {
+            echoJson(array());
+            exit;
+        }
+
+        $aIdsAffected = array ();
+        foreach($aIds as $iId) {
+			$aContentInfo = $this->_getContentInfo($iId);
+	    	if($this->_oModule->checkAllowedDelete($aContentInfo) !== CHECK_ACTION_RESULT_ALLOWED)
+	    		continue;
+
+        	if(!$this->_doDelete($iId, $aParams))
+                continue;
+
+			if(!$this->_onDelete($iId, $aParams))
+				continue;
+
+			$this->_oModule->checkAllowedDelete($aContentInfo, true);
+
+            $aIdsAffected[] = $iId;
+            $iAffected++;
+        }
+
+        echoJson($iAffected ? array('grid' => $this->getCode(false), 'blink' => $aIdsAffected) : array('msg' => _t($CNF['T']['grid_action_err_delete'])));
+    }
+
+    protected function _getActionDelete($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+		if($this->_sManageType == BX_DOL_MANAGE_TOOLS_ADMINISTRATION && $this->_oModule->checkAllowedEditAnyEntry() !== CHECK_ACTION_RESULT_ALLOWED)
+			return '';
+
+    	return $this->_getActionDefault($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getActionSettings($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+    	$sJsObject = $this->_oModule->_oConfig->getJsObject('manage_tools');
+    	$sMenuName = $this->_oModule->_oConfig->CNF['OBJECT_MENU_MANAGE_TOOLS'];
+
+    	$oMenu = BxDolMenu::getObjectInstance($sMenuName);
+    	$oMenu->setContentId($aRow['id']);
+
+    	$sMenu = $oMenu->getCode();
+    	if(empty($sMenu))
+    		return '';
+
+    	$a['attr'] = array_merge($a['attr'], array(
+    		"bx-popup-id" => $sMenuName . "-" . $aRow['id'],
+    		"onclick" => "$(this).off('click'); " . $sJsObject . ".onClickSettings('" . $sMenuName . "', this);"
+    	));
+
+    	return $this->_getActionDefault($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getFilterSelectOne($sFilterName, $sFilterValue, $aFilterValues)
+    {
+        if(empty($sFilterName) || empty($aFilterValues))
+            return '';
+
+		$CNF = &$this->_oModule->_oConfig->CNF;
+		$sJsObject = $this->_oModule->_oConfig->getJsObject('manage_tools');
+
+		foreach($aFilterValues as $sKey => $sValue)
+			$aFilterValues[$sKey] = _t($sValue);
+
+        $aInputModules = array(
+            'type' => 'select',
+            'name' => $sFilterName,
+            'attrs' => array(
+                'id' => 'bx-grid-' . $sFilterName . '-' . $this->_sObject,
+                'onChange' => 'javascript:' . $sJsObject . '.onChangeFilter(this)'
+            ),
+            'value' => $sFilterValue,
+            'values' => array_merge(array('' => _t($CNF['T']['filter_item_select_one_' . $sFilterName])), $aFilterValues)
+        );
+
+        $oForm = new BxTemplFormView(array());
+        return $oForm->genRow($aInputModules);
+    }
+
+	protected function _getSearchInput()
+    {
+        $sJsObject = $this->_oModule->_oConfig->getJsObject('manage_tools');
+
+        $aInputSearch = array(
+            'type' => 'text',
+            'name' => 'search',
+            'attrs' => array(
+                'id' => 'bx-grid-search-' . $this->_sObject,
+                'onKeyup' => 'javascript:$(this).off(\'keyup focusout\'); ' . $sJsObject . '.onChangeFilter(this)',
+                'onBlur' => 'javascript:' . $sJsObject . '.onChangeFilter(this)',
+            )
+        );
+
+        $oForm = new BxTemplFormView(array());
+        return $oForm->genRow($aInputSearch);
+    }
+
+	protected function _getContentInfo($iId)
+    {
+    	return $this->_oModule->_oDb->getContentInfoById($iId);
+    }
+
+	protected function _getManageAccountUrl($sFilter = '')
+    {
+    	$sModuleAccounts = 'bx_accounts';
+    	if(!BxDolModuleQuery::getInstance()->isEnabledByName($sModuleAccounts))
+    		return '';
+
+		$sTypeUpc = strtoupper($this->_sManageType);
+		$oModuleAccounts = BxDolModule::getInstance($sModuleAccounts);
+		if(!$oModuleAccounts || empty($oModuleAccounts->_oConfig->CNF['URL_MANAGE_' . $sTypeUpc]))
+			return '';
+
+		$sLink = $oModuleAccounts->_oConfig->CNF['URL_MANAGE_' . $sTypeUpc];
+
+		$sLink = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink($sLink);
+		
+		if(!empty($sFilter))
+			$sLink = bx_append_url_params($sLink, array('filter' => $sFilter));
+
+		return $sLink;
+    }
+
+	protected function _doDelete($iId, $aParams = array())
+    {
+    	return $this->_oModule->serviceDeleteEntity($iId) == '';
+    }
+
+    protected function _onDelete($iId, $aParams = array())
+    {
+    	return true;
+    }
+}
+
+/** @} */

+ 1190 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/general/classes/BxBaseModGeneralModule.php

@@ -0,0 +1,1190 @@
+<?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 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' => '' 
+            );
+
+        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'] : '',
+                );
+
+        return $aResult;
+    }
+
+    public function serviceGetSearchResultExtended($aParams)
+    {
+        if(empty($aParams) || !is_array($aParams))
+            return array();
+
+        return $this->_oDb->getEntriesBy(array('type' => 'search_ids', 'search_params' => $aParams));
+    }
+
+    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;
+    }
+
+	/**
+     * Display featured entries
+     * @return HTML string
+     */
+    public function serviceBrowseFeatured ($sUnitView = false, $bEmptyMessage = true, $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 '';
+
+        return $this->_serviceBrowse ('favorite', array_merge(array('user' => $oProfile->id()), $aParams), BX_DB_PADDING_DEF, true);
+    }
+
+    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 ($iAccount, $aValues)
+    {
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'FormsEntryHelper';
+        $oFormsHelper = new $sClass($this);
+        return $oFormsHelper->addData($iAccount, $aValues);
+    }
+
+    /**
+     * Create entry form
+     * @return HTML string
+     */
+    public function serviceEntityCreate ()
+    {
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'FormsEntryHelper';
+        $oFormsHelper = new $sClass($this);
+        return $oFormsHelper->addDataForm();
+    }
+
+    public function serviceEntityEdit ($iContentId = 0)
+    {
+        return $this->_serviceEntityForm ('editDataForm', $iContentId);
+    }
+
+    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)
+    {
+        return $this->_serviceEntityForm ('viewDataForm', $iContentId);
+    }
+
+	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)
+    {
+        if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iContentId)
+            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);
+    }
+
+    /**
+     * 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)
+    {
+        if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iContentId)
+            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)
+    {
+        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;
+
+        $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 . '_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' => '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 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.
+		);
+    }
+
+    public function _serviceGetNotification($aEvent, $sLangKey)
+    {
+    	$CNF = &$this->_oConfig->CNF;
+
+        $iContentId = (int)$aEvent['object_id'];
+        $oGroupProfile = BxDolProfile::getInstanceByContentAndType((int)$iContentId, $this->getName());
+        if (!$oGroupProfile)
+            return false;
+
+        $aContentInfo = $this->_oDb->getContentInfoById($iContentId);
+        if(empty($aContentInfo) || !is_array($aContentInfo))
+            return array();
+        
+        $oProfile = BxDolProfile::getInstance((int)$aEvent['subobject_id']);
+        if (!$oProfile)
+            return false;
+
+		return array(
+			'entry_sample' => $CNF['T']['txt_sample_single'],
+			'entry_url' => $oGroupProfile->getUrl(),
+			'entry_caption' => $oGroupProfile->getDisplayName(),
+			'entry_author' => $oGroupProfile->id(),
+			'subentry_sample' => $oProfile->getDisplayName(),
+			'subentry_url' => $oProfile->getUrl(),
+			'lang_key' => $sLangKey,
+		);
+    }
+    
+    /**
+     * 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)
+    {
+        $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), //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);
+    }
+    // ====== PERMISSION METHODS
+
+    /**
+     * @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 ($aDataEntry[$CNF['FIELD_AUTHOR']] == $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()
+        );
+    }
+
+    // ====== PROTECTED METHODS
+    protected function _isModerator ($isPerformAction = false)
+    {
+        return CHECK_ACTION_RESULT_ALLOWED === $this->checkAllowedEditAnyEntry ($isPerformAction);
+    }
+
+    protected function _serviceEntityForm ($sFormMethod, $iContentId = 0, $sDisplay = false)
+    {
+        if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iContentId)
+            return false;
+
+        bx_import('FormsEntryHelper', $this->_aModule);
+        $sClass = $this->_aModule['class_prefix'] . 'FormsEntryHelper';
+        $oFormsHelper = new $sClass($this);
+        return $oFormsHelper->$sFormMethod((int)$iContentId, $sDisplay);
+    }
+
+    protected function _serviceTemplateFunc ($sFunc, $iContentId, $sFuncGetContent = 'getContentInfoById')
+    {
+        if (!$iContentId)
+            $iContentId = bx_process_input(bx_get('id'), BX_DATA_INT);
+        if (!$iContentId)
+            return false;
+
+        $aContentInfo = $this->_oDb->$sFuncGetContent($iContentId);
+        if (!$aContentInfo)
+            return false;
+
+        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 _getContentForTimelinePost($aEvent, $aContentInfo)
+    {
+    	$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);
+
+    	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,
+		);
+    }
+
+    protected function _getImagesForTimelinePost($aEvent, $aContentInfo, $sUrl)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $sImage = '';
+        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'));
+
+        if($sImage == '' && isset($CNF['FIELD_THUMB']) && isset($aContentInfo[$CNF['FIELD_THUMB']]) && $aContentInfo[$CNF['FIELD_THUMB']])
+            $sImage = $this->_oConfig->getImageUrl($aContentInfo[$CNF['FIELD_THUMB']], array('OBJECT_IMAGES_TRANSCODER_GALLERY', 'OBJECT_IMAGES_TRANSCODER_THUMB'));
+
+        if(empty($sImage))
+            return array();
+
+        return array(
+		    array('url' => $sUrl, 'src' => $sImage),
+		);
+    }
+
+    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));
+    }
+
+    protected function _entitySocialSharing ($iId, $aParams = array())
+    {
+        $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 (
+                'url' => $sUrl . '#' . $oComments->getListAnchor(),
+                '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' => true));
+
+        //--- 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' => true));
+
+        //--- 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' => true));
+
+        //--- 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' => true));
+
+        //--- 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' => true)));
+
+        //--- 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' => true));
+
+        $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]]);
+    }
+}
+
+/** @} */

+ 115 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/general/classes/BxBaseModGeneralPageEntry.php

@@ -0,0 +1,115 @@
+<?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;
+
+    public function __construct($aObject, $oTemplate = false)
+    {
+        $this->_oModule = BxDolModule::getInstance($this->MODULE);
+        parent::__construct($aObject, $oTemplate ? $oTemplate : $this->_oModule->_oTemplate);
+    }
+
+    public function getCode ()
+    {
+        // check if content exists
+        if (!$this->_aContentInfo) { // 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->_oModule->getName() . '_cover');
+
+        // 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->metaAdd($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
+    }
+}
+
+/** @} */

+ 137 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/general/classes/BxBaseModGeneralTemplate.php

@@ -0,0 +1,137 @@
+<?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);
+    }
+
+	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->keywordsFields($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]);
+            }
+
+            // 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,
+        ));
+    }
+}
+
+/** @} */

+ 536 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/groups/classes/BxBaseModGroupsModule.php

@@ -0,0 +1,536 @@
+<?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'))
+            $oConn->removeConnection($iProfileId, $iGroupProfileId);
+    }
+
+    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' => $aContentInfo[$CNF['FIELD_AUTHOR']]));
+
+        }
+        // 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('content' => $aContentInfo, 'entry_title' => $sEntryTitle, 'entry_url' => $sEntryUrl, 'group_profile' => $iGroupProfileId, 'profile' => $iProfileId, 'notification_subobject_id' => $iProfileId, 'object_author_id' => $aContentInfo[$CNF['FIELD_AUTHOR']]));
+
+        }
+        // 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('content' => $aContentInfo, 'entry_title' => $sEntryTitle, 'entry_url' => $sEntryUrl, 'group_profile' => $iGroupProfileId, 'profile' => $iProfileId, 'notification_subobject_id' => $iProfileId, 'object_author_id' => $aContentInfo[$CNF['FIELD_AUTHOR']]));
+
+        }
+
+        // new fan was added
+        if ($oConnection->isConnected($oGroupProfile->id(), (int)$iInitiatorId, true)) {
+            bx_alert($this->getName(), 'fan_added', $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' => $aContentInfo[$CNF['FIELD_AUTHOR']]));
+            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)
+    {
+        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;
+
+        bx_import('BxDolConnection');
+        $s = $this->serviceBrowseConnectionsQuick ($oGroupProfile->id(), $this->_oConfig->CNF['OBJECT_CONNECTIONS'], BX_CONNECTIONS_CONTENT_TYPE_CONTENT, true);
+        if (!$s)
+            return MsgBox(_t('_sys_txt_empty'));
+        return $s;
+    }
+
+    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 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']);
+    }
+
+    /**
+     * Entry post for Timeline module
+     */
+    public function serviceGetTimelinePost($aEvent)
+    {
+        $a = parent::serviceGetTimelinePost($aEvent);
+
+        $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 checkAllowedViewCoverImage ($aDataEntry, $isPerformAction = false)
+    {
+        $oPrivacy = BxDolPrivacy::getObjectInstance($this->_oConfig->CNF['OBJECT_PRIVACY_VIEW']);
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = parent::checkAllowedViewCoverImage($aDataEntry)) && $oPrivacy->isPartiallyVisible($aDataEntry[$this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']]))
+            return CHECK_ACTION_RESULT_ALLOWED;
+        
+        return $sMsg;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedViewProfileImage ($aDataEntry, $isPerformAction = false)
+    {
+        return $this->checkAllowedViewCoverImage($aDataEntry, $isPerformAction);
+    }
+
+    /**
+     * @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 CHECK_ACTION_RESULT_ALLOWED;
+
+        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)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(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);
+    }
+}
+
+/** @} */

+ 240 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/notifications/classes/BxBaseModNotificationsDb.php

@@ -0,0 +1,240 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseNotifications Base classes for Notifications like modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/*
+ * Module database queries
+ */
+class BxBaseModNotificationsDb extends BxBaseModGeneralDb
+{
+    protected $_oConfig;
+
+    protected $_sTable;
+    protected $_sTableHandlers;
+
+    public function __construct(&$oConfig)
+    {
+        parent::__construct($oConfig);
+
+        $this->_oConfig = $oConfig;
+
+		$this->_sTable = $this->_sPrefix . 'events';
+		$this->_sTableHandlers = $this->_sPrefix . 'handlers';
+    }
+
+    public function getAlertHandlerId()
+    {
+        $sQuery = $this->prepare("SELECT `id` FROM `sys_alerts_handlers` WHERE `name`=? LIMIT 1", $this->_oConfig->getObject('alert'));
+        return (int)$this->getOne($sQuery);
+    }
+
+    public function insertData($aData)
+    {
+    	$aHandlerDescriptor = $this->_oConfig->getHandlerDescriptor();
+
+    	//--- Update Timeline Handlers ---//
+        foreach($aData['handlers'] as $aHandler) {
+            $sContent = $sPrivacy = '';
+            if($aHandler['type'] == BX_BASE_MOD_NTFS_HANDLER_TYPE_INSERT) {
+            	if(empty($aHandler['module_class']))
+            		$aHandler['module_class'] = 'Module';
+
+            	$sContent = serialize(array_intersect_key($aHandler, $aHandlerDescriptor));
+            	$sPrivacy = !empty($aHandler['module_event_privacy']) ? $aHandler['module_event_privacy'] : '';
+            }
+
+            $sQuery = $this->prepare("INSERT INTO
+                    `{$this->_sTableHandlers}`
+                SET
+                	`group`=?,
+                    `type`=?,
+                    `alert_unit`=?,
+                    `alert_action`=?,
+                    `content`=?,
+                    `privacy`=?", $aHandler['group'], $aHandler['type'], $aHandler['alert_unit'], $aHandler['alert_action'], $sContent, $sPrivacy);
+
+            $this->query($sQuery);
+        }
+
+        //--- Update System Alerts ---//
+        $iHandlerId = $this->getAlertHandlerId();
+        foreach($aData['alerts'] as $aAlert) {
+            $sQuery = $this->prepare("INSERT INTO
+                    `sys_alerts`
+                SET
+                    `unit`=?,
+                    `action`=?,
+                    `handler_id`=?", $aAlert['unit'], $aAlert['action'], $iHandlerId);
+
+            $this->query($sQuery);
+        }
+    }
+
+    public function deleteData($aData)
+    {
+    	//--- Update Timeline Handlers ---//
+        foreach($aData['handlers'] as $aHandler) {
+            $sQuery = $this->prepare("DELETE FROM
+                    `{$this->_sTableHandlers}`
+                WHERE
+                    `alert_unit`=? AND
+                    `alert_action`=?
+                LIMIT 1", $aHandler['alert_unit'], $aHandler['alert_action']);
+
+            $this->query($sQuery);
+        }
+
+        //--- Update System Alerts ---//
+        $iHandlerId = $this->getAlertHandlerId();
+        foreach($aData['alerts'] as $aAlert) {
+            $sQuery = $this->prepare("DELETE FROM
+                    `sys_alerts`
+                WHERE
+                    `unit`=? AND
+                    `action`=? AND
+                    `handler_id`=?
+                LIMIT 1", $aAlert['unit'], $aAlert['action'], $iHandlerId);
+
+            $this->query($sQuery);
+        }
+    }
+
+    public function deleteModuleEvents($aData)
+    {
+		//Delete system events.
+    	foreach($aData['handlers'] as $aHandler)
+            $this->deleteEvent(array('type' => $aHandler['alert_unit'], 'action' => $aHandler['alert_action']));
+    }
+
+	public function activateModuleEvents($aData, $bActivate = true)
+    {
+    	$iActivate = $bActivate ? 1 : 0;
+
+    	//Activate (deactivate) system events.
+    	foreach($aData['handlers'] as $aHandler)    		
+            $this->updateEvent(array('active' => $iActivate), array('type' => $aHandler['alert_unit'], 'action' => $aHandler['alert_action']));
+    }
+
+    public function getHandlers($aParams = array())
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sWhereClause = '';
+
+        if(!empty($aParams))
+            switch($aParams['type']) {
+ 				case 'by_group_key_type':
+ 					$aMethod['name'] = 'getAllWithKey';
+ 					$aMethod['params'][1] = 'type';
+ 					$aMethod['params'][2] = array(
+	                	'group' => $aParams['group']
+	                );
+
+ 					$sWhereClause = "AND `group`=:group";
+ 					break;
+            }
+
+        $aMethod['params'][0] = "SELECT * FROM `{$this->_sTableHandlers}` WHERE 1 " . $sWhereClause;
+        return call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+    }
+
+    public function insertEvent($aParamsSet)
+    {
+        if(empty($aParamsSet))
+            return 0;
+
+        $aSet = array();
+        foreach($aParamsSet as $sKey => $sValue)
+           $aSet[] = "`" . $sKey . "`=:" . $sKey;
+
+		if(!isset($aParamsSet['date']))
+			$aSet[] = "`date`=UNIX_TIMESTAMP()";
+
+        if((int)$this->query("INSERT INTO `{$this->_sTable}` SET " . implode(", ", $aSet), $aParamsSet) <= 0)
+            return 0;
+
+        return (int)$this->lastId();
+    }
+
+    public function updateEvent($aParamsSet, $aParamsWhere)
+    {
+        if(empty($aParamsSet) || empty($aParamsWhere))
+            return false;
+
+        $sSql = "UPDATE `{$this->_sTable}` SET " . $this->arrayToSQL($aParamsSet) . " WHERE " . $this->arrayToSQL($aParamsWhere, " AND ");
+        return $this->query($sSql);
+    }
+
+    public function deleteEvent($aParams, $sWhereAddon = "")
+    {
+        $sSql = "DELETE FROM `{$this->_sTable}` WHERE " . $this->arrayToSQL($aParams, " AND ") . $sWhereAddon;
+        return $this->query($sSql);
+    }
+
+	public function getEvents($aParams, $bReturnCount = false)
+    {
+        list($sMethod, $sSelectClause, $sJoinClause, $sWhereClause, $sOrderClause, $sLimitClause) = $this->_getSqlPartsEvents($aParams);
+
+        $sSql = "SELECT {select}
+            FROM `{$this->_sTable}`
+            LEFT JOIN `{$this->_sTableHandlers}` ON `{$this->_sTable}`.`type`=`{$this->_sTableHandlers}`.`alert_unit` AND `{$this->_sTable}`.`action`=`{$this->_sTableHandlers}`.`alert_action` " . $sJoinClause . "
+            WHERE 1 " . $sWhereClause . " {order} {limit}";
+
+        $aEntries = $this->$sMethod(str_replace(array('{select}', '{order}', '{limit}'), array($sSelectClause . "`{$this->_sTable}`.*", $sOrderClause, $sLimitClause), $sSql));
+        if(!$bReturnCount)
+        	return $aEntries;
+
+		return array($aEntries, (int)$this->getOne(str_replace(array('{select}', '{order}', '{limit}'), array("COUNT(*)", "", ""), $sSql)));
+    }
+
+	protected function _getSqlPartsEvents($aParams)
+    {
+    	$sMethod = 'getAll';
+        $sSelectClause = $sJoinClause = $sWhereClause = $sOrderClause = $sLimitClause = "";
+
+        switch($aParams['browse']) {
+            case 'id':
+                $sMethod = 'getRow';
+                $sWhereClause = $this->prepareAsString("AND `{$this->_sTable}`.`id`=? ", $aParams['value']);
+                $sLimitClause = "LIMIT 1";
+                break;
+
+			case 'first':
+				$sMethod = 'getRow';
+				list($sJoinClause, $sWhereClause) = $this->_getSqlPartsEventsList($aParams);
+				$sOrderClause = "ORDER BY `{$this->_sTable}`.`date` DESC";
+				$sLimitClause = "LIMIT 1";
+				break;
+
+			case 'last':
+				$sMethod = 'getRow';
+				list($sJoinClause, $sWhereClause) = $this->_getSqlPartsEventsList($aParams);
+				$sOrderClause = "ORDER BY `{$this->_sTable}`.`date` ASC";
+				$sLimitClause = "LIMIT 1";
+				break;
+
+			case 'list':
+				list($sJoinClause, $sWhereClause) = $this->_getSqlPartsEventsList($aParams);
+				$sOrderClause = "ORDER BY `{$this->_sTable}`.`date` DESC";
+				$sLimitClause = isset($aParams['per_page']) ? "LIMIT " . $aParams['start'] . ", " . $aParams['per_page'] : "";
+				break;
+        }
+
+        return array($sMethod, $sSelectClause, $sJoinClause, $sWhereClause, $sOrderClause, $sLimitClause);
+    }
+
+    protected function _getSqlPartsEventsList($aParams)
+    {
+    	$sJoinClause = $sWhereClause = "";
+
+    	return array($sJoinClause, $sWhereClause);
+    }
+}
+
+/** @} */

+ 100 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/notifications/classes/BxBaseModNotificationsTemplate.php

@@ -0,0 +1,100 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseNotifications Base classes for Notifications like modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModNotificationsTemplate extends BxBaseModGeneralTemplate
+{
+	function __construct(&$oConfig, &$oDb)
+    {
+        parent::__construct($oConfig, $oDb);
+    }
+
+	public function getCssJs()
+    {
+        $this->addCss(array(
+            'view.css',
+            'view-media-tablet.css',
+            'view-media-desktop.css',
+        ));
+        $this->addJs(array(
+        	'jquery.anim.js',
+            'main.js',
+            'view.js',
+        ));
+    }
+
+	public function getJsCode($sType, $aParams = array(), $bWrap = true)
+    {
+        $oModule = $this->getModule();
+
+        $aParams = array_merge(array(
+        	'iOwnerId' => $oModule->_iOwnerId,
+            'sAnimationEffect' => $this->_oConfig->getAnimationEffect(),
+            'iAnimationSpeed' => $this->_oConfig->getAnimationSpeed(),
+            'aHtmlIds' => $this->_oConfig->getHtmlIds($sType)
+        ), $aParams);
+
+        $this->getCssJs();
+        return parent::getJsCode($sType, $aParams, $bWrap);
+    }
+    
+    public function getUnit(&$aEvent, $aBrowseParams = array())
+    {
+    	return '';
+    }
+
+	function unit($aData, $isCheckPrivateContent = true, $sTemplateName = 'unit.html')
+    {
+    	$this->getCssJs();
+
+    	if($sTemplateName == 'unit.html')
+    		return $this->getUnit($aData);
+
+        $oModule = $this->getModule();
+        $CNF = &$this->_oConfig->CNF;
+
+        if ($isCheckPrivateContent && CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $oModule->isAllowedView($aData))) {
+            $aVars = array (
+                'summary' => $sMsg,
+            );
+            return $this->parseHtmlByName('unit_private.html', $aVars);
+        }
+
+        list($sAuthorName, $sAuthorUrl, $sAuthorIcon) = $oModule->getUserInfo($aData['object_id']);
+        $bAuthorIcon = !empty($sAuthorIcon);
+
+        // generate html
+        $aVars = array (
+            'id' => $aData['id'],
+            'author' => $sAuthorName,
+            'author_url' => $sAuthorUrl,
+            'title' => bx_process_output($aData['title']),
+        	'item_url' => $this->_oConfig->getItemViewUrl($aData),
+            'item_date' => bx_time_js($aData['date'], BX_FORMAT_DATE),
+            'module_name' => _t($CNF['T']['txt_sample_single_ext']),
+            'ts' => $aData['date'],
+            'bx_if:show_icon' => array(
+                'condition' => $bAuthorIcon,
+                'content' => array(
+                    'author_icon' => $sAuthorIcon
+                )
+            ),
+            'bx_if:show_icon_empty' => array(
+                'condition' => !$bAuthorIcon,
+                'content' => array()
+            ),
+        );
+
+        return $this->parseHtmlByName($sTemplateName, $aVars);
+    }
+
+}
+
+/** @} */

+ 144 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/payment/classes/BxBaseModPaymentCart.php

@@ -0,0 +1,144 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BasePayment Base classes for Payment like modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModPaymentCart extends BxDol
+{
+	protected $MODULE;
+	protected $_oModule;
+
+	function __construct()
+    {
+        parent::__construct();
+
+        $this->_oModule = BxDolModule::getInstance($this->MODULE);
+    }
+
+	public function serviceGetCartUrl($iVendor = 0)
+    {
+    	if(!$this->_oModule->isLogged())
+            return '';
+
+		if($iVendor == 0)
+    		return $this->_oModule->_oConfig->getUrl('URL_CARTS');
+
+    	return  bx_append_url_params($this->_oModule->_oConfig->getUrl('URL_CART'), array('seller_id' => $iVendor));
+    }
+
+    public function serviceGetCartJs($sType = '')
+    {
+        return $this->_oModule->_oTemplate->displayCartJs($sType);
+    }
+
+    public function serviceGetAddToCartJs($iVendorId, $mixedModuleId, $iItemId, $iItemCount, $bNeedRedirect = false)
+    {
+		$iModuleId = $this->_oModule->_oConfig->getModuleId($mixedModuleId);
+        if(empty($iModuleId))
+            return '';
+
+        return $this->_oModule->_oTemplate->displayAddToCartJs($iVendorId, $iModuleId, $iItemId, $iItemCount, $bNeedRedirect);
+    }
+
+	public function serviceGetAddToCartLink($iVendorId, $mixedModuleId, $iItemId, $iItemCount, $bNeedRedirect = false)
+    {
+        $iModuleId = $this->_oModule->_oConfig->getModuleId($mixedModuleId);
+        if(empty($iModuleId))
+            return '';
+
+		return $this->_oModule->_oTemplate->displayAddToCartLink($iVendorId, $iModuleId, $iItemId, $iItemCount, $bNeedRedirect);
+    }
+
+	public function serviceGetSubscriptionsUrl($iVendor = 0)
+    {
+    	if(!$this->_oModule->isLogged())
+            return '';
+
+		if($iVendor == 0)
+    		return $this->_oModule->_oConfig->getUrl('URL_SUBSCRIPTIONS');
+
+    	return  bx_append_url_params($this->_oModule->_oConfig->getUrl('URL_SUBSCRIPTIONS'), array('seller_id' => $iVendor));
+    }
+
+	public function serviceGetSubscribeUrl($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount = 1)
+    {
+    	if(!$this->_oModule->isLogged())
+            return '';
+
+    	return  bx_append_url_params($this->_oModule->_oConfig->getUrl('URL_SUBSCRIBE'), array(
+    		'seller_id' => $iVendorId,
+    		'seller_provider' => $sVendorProvider,
+    		'module_id' => $mixedModuleId, 
+    		'item_id' => $iItemId,
+    		'item_count' => $iItemCount
+    	));
+    }
+
+	public function serviceGetSubscribeJs($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount = 1, $sRedirect = '')
+    {
+		$iModuleId = $this->_oModule->_oConfig->getModuleId($mixedModuleId);
+        if(empty($iModuleId))
+            return '';
+
+        return $this->_oModule->_oTemplate->displaySubscribeJs($iVendorId, $sVendorProvider, $iModuleId, $iItemId, $iItemCount, $sRedirect);
+    }
+
+	public function serviceGetSubscribeLink($iVendorId, $sVendorProvider, $mixedModuleId, $iItemId, $iItemCount = 1, $sRedirect = '')
+    {
+        $iModuleId = $this->_oModule->_oConfig->getModuleId($mixedModuleId);
+        if(empty($iModuleId))
+            return '';
+
+		return $this->_oModule->_oTemplate->displaySubscribeLink($iVendorId, $sVendorProvider, $iModuleId, $iItemId, $iItemCount, $sRedirect);
+    }
+
+	public function serviceGetCartItemDescriptor($iVendorId, $iModuleId, $iItemId, $iItemCount)
+	{
+		return $this->_oModule->_oConfig->descriptorA2S(array($iVendorId, $iModuleId, $iItemId, $iItemCount));
+	}
+
+    public function serviceGetCartItemsCount($iUserId = 0)
+    {
+    	$iUserId = !empty($iUserId) ? $iUserId : $this->_oModule->getProfileId();
+        if(empty($iUserId))
+            return 0;
+
+        $aInfo = $this->getInfo(BX_PAYMENT_TYPE_SINGLE, $iUserId);
+
+        $iCount = 0;
+        foreach($aInfo as $iVendorId => $aVendorCart)
+            $iCount += $aVendorCart['items_count'];
+
+        return $iCount;
+    }
+
+	protected function _parseByVendor($iUserId)
+    {
+        $sItems = $this->_oModule->_oDb->getCartItems($iUserId);
+        return $this->_reparseBy($this->_oModule->_oConfig->descriptorsM2A($sItems), 'vendor_id');
+    }
+
+    protected function _parseByModule($iUserId)
+    {
+        $sItems = $this->_oModule->_oDb->getCartItems($iUserId);
+        return $this->_reparseBy($this->_oModule->_oConfig->descriptorsM2A($sItems), 'module_id');
+    }
+
+	protected function _reparseBy($aItems, $sKey)
+    {
+        $aResult = array();
+        foreach($aItems as $aItem)
+            if(isset($aItem[$sKey]))
+                $aResult[$aItem[$sKey]][] = $aItem;
+
+        return $aResult;
+    }
+}
+
+/** @} */

+ 148 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/payment/classes/BxBaseModPaymentConfig.php

@@ -0,0 +1,148 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BasePayment Base classes for Payment like modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModPaymentConfig extends BxBaseModGeneralConfig
+{
+	protected $_oDb;
+
+	protected $_sCurrencySign;
+    protected $_sCurrencyCode;
+
+    protected $_aPerPage;
+    protected $_aHtmlIds;
+
+    protected $_sAnimationEffect;
+    protected $_iAnimationSpeed;
+
+    function __construct($aModule)
+    {
+        parent::__construct($aModule);
+
+        $this->CNF = array_merge($this->CNF, array(
+        	'OBJECT_FORM_PRELISTS_CURRENCIES' => '',
+
+        	'KEY_SESSION_PENDING' => $this->_sName . '_pending_id',
+        	'KEY_REQUEST_PENDING' => $this->_sName . '_pending_id',
+        	
+        	'DIVIDER_DESCRIPTOR' => '_',
+        	'DIVIDER_DESCRIPTORS' => ':',
+        ));
+
+		$this->_aPrefixes = array(
+			'general' => $this->_sName . '_',
+			'langs' => '_' . $this->_sName . '_',
+        	'options' => $this->_sName . '_'
+		);
+
+		$this->_aObjects = array_merge($this->_aObjects, array(
+			'form_pendings' => $this->_sName . '_form_pendings',
+			'form_processed' => $this->_sName . '_form_processed',
+			'display_pendings_process' => $this->_sName . '_form_pendings_process',
+			'display_processed_add' => $this->_sName . '_form_processed_add',
+
+			'menu_dashboard' => 'sys_account_dashboard',
+			'menu_cart_submenu' => $this->_sName . '_menu_cart_submenu',
+        	'menu_orders_submenu' => $this->_sName . '_menu_orders_submenu',
+    		'menu_sbs_submenu' => $this->_sName . '_menu_sbs_submenu',
+		    'menu_sbs_actions' => $this->_sName . '_menu_sbs_actions',
+
+			'grid_history' => $this->_sName . '_grid_orders_history',
+			'grid_processed' => $this->_sName . '_grid_orders_processed',
+			'grid_pending' => $this->_sName . '_grid_orders_pending',
+			'grid_carts' => $this->_sName . '_grid_carts',
+			'grid_cart' => $this->_sName . '_grid_cart',
+			'grid_sbs_list_my' => $this->_sName . '_grid_sbs_list_my',
+			'grid_sbs_list_all' => $this->_sName . '_grid_sbs_list_all',
+			'grid_sbs_history' => $this->_sName . '_grid_sbs_history',
+		));
+
+		$this->_aPerPage = array();
+		$this->_aHtmlIds = array();
+
+		$this->_sAnimationEffect = 'fade';
+        $this->_iAnimationSpeed = 'slow';
+    }
+
+    public function init(&$oDb)
+    {
+    	$this->_oDb = $oDb;
+    }
+
+    public function getDefaultCurrencySign()
+    {
+        return $this->_sCurrencySign;
+    }
+
+    public function getDefaultCurrencyCode()
+    {
+        return $this->_sCurrencyCode;
+    }
+
+	public function getKey($sType)
+    {
+    	$sResult = '';
+    	if(empty($sType) || !isset($this->CNF[$sType]))
+            return $sResult;
+
+        return $this->CNF[$sType];
+    }
+
+    public function getUrl($sType, $aParams = array(), $bSsl = false)
+    {
+		$sResult = '';
+		if(empty($sType) || !isset($this->CNF[$sType]))
+			return $sResult;
+
+		if(strncmp($this->CNF[$sType], BX_DOL_URL_ROOT, strlen(BX_DOL_URL_ROOT)) === 0)
+			$sResult = bx_append_url_params($this->CNF[$sType], $aParams);
+		else
+			$sResult = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink($this->CNF[$sType], $aParams);
+
+    	return $bSsl ? $this->http2https($sResult) : $sResult;
+    }
+
+	public function getDivider($sType)
+    {
+    	$sResult = '';
+    	if(empty($sType) || !isset($this->CNF[$sType]))
+            return $sResult;
+
+        return $this->CNF[$sType];
+    }
+
+	public function getPerPage($sType = 'default')
+    {
+    	if(empty($sType))
+            return $this->_aPerPage;
+
+        return isset($this->_aPerPage[$sType]) ? $this->_aPerPage[$sType] : '';
+    }
+
+	public function getHtmlIds($sType, $sKey = '')
+    {
+        if(empty($sKey))
+            return isset($this->_aHtmlIds[$sType]) ? $this->_aHtmlIds[$sType] : array();
+
+        return isset($this->_aHtmlIds[$sType][$sKey]) ? $this->_aHtmlIds[$sType][$sKey] : '';
+    }
+
+    public function getAnimationEffect()
+    {
+        return $this->_sAnimationEffect;
+    }
+
+	public function getAnimationSpeed()
+    {
+        return $this->_iAnimationSpeed;
+    }
+}
+
+/** @} */

+ 198 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/payment/classes/BxBaseModPaymentModule.php

@@ -0,0 +1,198 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BasePayment Base classes for Payment like modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModPaymentModule extends BxBaseModGeneralModule
+{
+	protected $_sLangsPrefix;
+
+	function __construct($aModule)
+    {
+        parent::__construct($aModule);
+
+        $this->_oConfig->init($this->_oDb);
+
+        $this->_sLangsPrefix = $this->_oConfig->getPrefix('langs');
+    }
+
+	public function serviceUpdateDependentModules($sModule = 'all', $bInstall = true)
+    {
+    	$aModules = $sModule == 'all' ? $this->_oDb->getModulesBy(array('type' => 'modules'), false) : array($this->_oDb->getModuleByName($sModule, false));
+
+        foreach($aModules as $aModule) {
+        	if(empty($aModule) || empty($aModule['name']))
+        		continue;
+
+        	$mixedData = $this->callGetPaymentData($aModule['name']);
+        	if($mixedData === false)
+        		continue;
+
+			$sMethodName = $bInstall ? 'insertModule' : 'deleteModule';
+			$this->_oDb->$sMethodName($mixedData);
+        }
+    }
+
+    public function serviceGetCurrencyInfo()
+    {
+        return array(
+            'sign' => $this->_oConfig->getDefaultCurrencySign(),
+            'code' => $this->_oConfig->getDefaultCurrencyCode()
+        );
+    }
+
+	public function serviceGetOption($sOption)
+    {
+    	$sMethod = 'get' . bx_gen_method_name($sOption);
+    	if(method_exists($this->_oConfig, $sMethod))
+    		return $this->_oConfig->$sMethod();
+
+    	return $this->_oDb->getParam($this->_oConfig->getPrefix('options') . $sOption);
+    }
+
+	public function serviceGetProvidersCart($iVendorId)
+	{
+		$aVendorProviders = $this->_oDb->getVendorInfoProvidersSingle($iVendorId);
+
+		$aResult = array();
+		foreach($aVendorProviders as $aProvider) {
+			$aProvider['caption_cart'] = _t($this->_sLangsPrefix . 'txt_cart_' . $aProvider['name']);
+
+			$aResult[] = $aProvider;
+		}
+
+		return $aResult;
+	}
+
+	public function getVendorInfo($iUserId)
+    {
+        return array_merge($this->getProfileInfo($iUserId), array(
+        	'currency_code' => $this->_oConfig->getDefaultCurrencyCode(),
+			'currency_sign' => $this->_oConfig->getDefaultCurrencySign()
+        ));
+    }
+
+	public function getObjectJoin()
+    {
+        $sClassName = $this->_oConfig->getClassPrefix() . 'Join';
+        if(!isset($GLOBALS['bxDolClasses'][$sClassName])) {
+        	bx_import('Join', $this->_aModule);
+            $GLOBALS['bxDolClasses'][$sClassName] = new $sClassName();
+        }
+
+        return $GLOBALS['bxDolClasses'][$sClassName];
+    }
+
+	public function getObjectCart()
+    {
+        $sClassName = $this->_oConfig->getClassPrefix() . 'Cart';
+        if(!isset($GLOBALS['bxDolClasses'][$sClassName])) {
+        	bx_import('Cart', $this->_aModule);
+            $GLOBALS['bxDolClasses'][$sClassName] = new $sClassName();
+        }
+
+        return $GLOBALS['bxDolClasses'][$sClassName];
+    }
+
+    public function getObjectOrders()
+    {
+    	$sClassName = $this->_oConfig->getClassPrefix() . 'Orders';
+        if(!isset($GLOBALS['bxDolClasses'][$sClassName])) {
+        	bx_import('Orders', $this->_aModule);
+            $GLOBALS['bxDolClasses'][$sClassName] = new $sClassName();
+        }
+
+        return $GLOBALS['bxDolClasses'][$sClassName];
+    }
+
+	public function getObjectDetails()
+    {
+    	$sClassName = $this->_oConfig->getClassPrefix() . 'Details';
+        if(!isset($GLOBALS['bxDolClasses'][$sClassName])) {
+        	bx_import('Details', $this->_aModule);
+            $GLOBALS['bxDolClasses'][$sClassName] = new $sClassName();
+        }
+
+        return $GLOBALS['bxDolClasses'][$sClassName];
+    }
+
+    public function getObjectSubscriptions()
+    {
+    	$sClassName = $this->_oConfig->getClassPrefix() . 'Subscriptions';
+        if(!isset($GLOBALS['bxDolClasses'][$sClassName])) {
+        	bx_import('Subscriptions', $this->_aModule);
+            $GLOBALS['bxDolClasses'][$sClassName] = new $sClassName();
+        }
+
+        return $GLOBALS['bxDolClasses'][$sClassName];
+    }
+
+	public function getObjectProvider($sProvider, $mixedVendorId = BX_PAYMENT_EMPTY_ID)
+	{
+		$aProvider = $this->_oDb->getProviders(array('type' => 'by_name', 'name' => $sProvider));
+		if(empty($aProvider) || !is_array($aProvider) || empty($aProvider['class_name']))
+			return false;
+
+		if(is_numeric($mixedVendorId) && (int)$mixedVendorId != BX_PAYMENT_EMPTY_ID)
+			$aProvider['options'] = $this->_oDb->getOptions((int)$mixedVendorId, $aProvider['id']);
+
+		$sClassPath = !empty($aProvider['class_file']) ? BX_DIRECTORY_PATH_ROOT . $aProvider['class_file'] : $this->_oConfig->getClassPath() . $aProvider['class_name'] . '.php';
+        if(!file_exists($sClassPath))
+        	return false;
+
+        require_once($sClassPath);
+        return new $aProvider['class_name']($aProvider);
+	}
+
+    public function callGetPaymentData($mixedModule)
+    {
+    	$sMethod = 'get_payment_data';
+		if(!BxDolRequest::serviceExists($mixedModule, $sMethod)) 
+			return false;
+
+		return BxDolService::call($mixedModule, $sMethod);
+    }
+
+    public function callGetCartItem($mixedModule, $aParams)
+    {
+    	return BxDolService::call($mixedModule, 'get_cart_item', $aParams);
+    }
+
+    public function callGetCartItems($mixedModule, $aParams)
+    {
+    	return BxDolService::call($mixedModule, 'get_cart_items', $aParams);
+    }
+
+    public function callRegisterCartItem($mixedModule, $aParams)
+    {
+    	return BxDolService::call($mixedModule, 'register_cart_item', $aParams);
+    }
+
+	public function callRegisterSubscriptionItem($mixedModule, $aParams)
+    {
+    	return BxDolService::call($mixedModule, 'register_subscription_item', $aParams);
+    }
+
+    public function callUnregisterCartItem($mixedModule, $aParams)
+    {
+		return BxDolService::call($mixedModule, 'unregister_cart_item', $aParams);
+    }
+
+	public function callUnregisterSubscriptionItem($mixedModule, $aParams)
+    {
+		return BxDolService::call($mixedModule, 'unregister_subscription_item', $aParams);
+    }
+
+    public function callCancelSubscriptionItem($mixedModule, $aParams)
+    {
+    	return BxDolService::call($mixedModule, 'cancel_subscription_item', $aParams);
+    }
+}
+
+/** @} */

+ 49 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/payment/classes/BxBaseModPaymentOrders.php

@@ -0,0 +1,49 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BasePayment Base classes for Payment like modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModPaymentOrders extends BxDol
+{
+	protected $MODULE;
+	protected $_oModule;
+
+	function __construct()
+    {
+        parent::__construct();
+
+        $this->_oModule = BxDolModule::getInstance($this->MODULE);
+    }
+
+	public function serviceGetOrdersUrl()
+    {
+    	if(!$this->_oModule->isLogged())
+            return '';
+
+    	return $this->_oModule->_oConfig->getUrl('URL_ORDERS');
+    }
+
+    public function serviceGetOrdersCount($sType, $iProfileId = 0)
+    {
+        if(!in_array($sType, array('new')))
+            return 0;
+
+    	$iProfileId = !empty($iProfileId) ? $iProfileId : $this->_oModule->getProfileId();
+        if(empty($iProfileId))
+            return 0;
+
+        $aOrders = $this->_oModule->_oDb->getOrderProcessed(array('type' => $sType, 'seller_id' => $iProfileId));
+        if(empty($aOrders) || !is_array($aOrders))
+            return 0;
+
+        return count($aOrders);
+    }
+}
+
+/** @} */

+ 138 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/payment/classes/BxBaseModPaymentTemplate.php

@@ -0,0 +1,138 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BasePayment Base classes for Payment like modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModPaymentTemplate extends BxBaseModGeneralTemplate
+{
+    function __construct($oConfig, $oDb)
+    {
+        parent::__construct($oConfig, $oDb);
+    }
+
+	public function displayJsCode($sType, $aParams = array(), $aRequestParams = array())
+    {
+    	$sJsClass = $this->_oConfig->getJsClass($sType);
+    	$sJsObject = $this->_oConfig->getJsObject($sType);
+
+    	$aParams = array_merge(array(
+    		'sActionUrl' => BX_DOL_URL_ROOT . $this->_oConfig->getBaseUri(), 
+    		'sObjName' => $sJsObject,
+    		'sAnimationEffect' => $this->_oConfig->getAnimationEffect(),
+    		'iAnimationSpeed' => $this->_oConfig->getAnimationSpeed(),
+    		'aHtmlIds' => $this->_oConfig->getHtmlIds($sType),
+    		'oRequestParams' => $aRequestParams
+    	), $aParams);
+
+        return $this->_wrapInTagJsCode("var " . $sJsObject . " = new " . $sJsClass . "(" . json_encode($aParams) . ");");
+    }
+
+	public function displayPageCode(&$aParams)
+    {
+		check_logged();
+		$oTemplate = BxDolTemplate::getInstance();
+
+        $iIndex = isset($aParams['index']) ? (int)$aParams['index'] : BX_PAGE_DEFAULT;
+        $oTemplate->setPageNameIndex($iIndex);
+
+        if(isset($aParams['js']))
+			$oTemplate->addJs($aParams['js']);
+		if(isset($aParams['css']))
+			$oTemplate->addCss($aParams['css']);
+
+		if(isset($aParams['title']['page']))
+            $oTemplate->setPageHeader($aParams['title']['page']);
+        if(isset($aParams['title']['block']))
+         	$oTemplate->setPageParams(array('header_text' => $aParams['title']['block']));
+
+        if(isset($aParams['content']))
+            foreach($aParams['content'] as $sKey => $sValue)
+            	$oTemplate->setPageContent($sKey, $sValue);
+
+		$oTemplate->getPageCode();
+    }
+
+	public function displayPageCodeResponse($sMessage, $bWrap = true, $bCenter = false)
+    {
+        $this->displayPageCodeText($this->_sLangsPrefix . 'page_title_response', $sMessage, $bWrap, $bCenter);
+    }
+
+    public function displayPageCodeError($sMessage, $bWrap = true, $bCenter = false)
+    {
+        $this->displayPageCodeText($this->_sLangsPrefix . 'page_title_error', $sMessage, $bWrap, $bCenter);
+    }
+
+    public function displayPageCodeRedirect($sUrl, $aData = array(), $sMethod = 'post', $sMessage = '', $bWrap = true)
+    {
+    	if(empty($sMessage))
+    		$sMessage = $this->_sLangsPrefix . 'msg_redirecting';
+
+    	$aTmplVarsData = array();
+    	if(!empty($aData) && is_array($aData))
+    		foreach($aData as $sKey => $mixedValue)
+    			$aTmplVarsData[] = array('name' => bx_html_attribute($sKey), 'value' => bx_html_attribute($mixedValue));
+
+    	$aParams = array(
+    		'index' => BX_PAGE_CLEAR,
+    		'content' => array(
+                'page_main_code' => $this->parseHtmlByName('redirect.html', array(
+    				'content' => $bWrap ? MsgBox(_t($sMessage)) : $sMessage,
+    				'action' => $sUrl,
+    				'method' => $sMethod,
+    				'bx_repeat:items' => $aTmplVarsData
+    			))
+    		)
+    	);
+    	$this->displayPageCode($aParams);
+    }
+
+	public function displayProfileLink($mixedProfile)
+    {
+    	if(!is_array($mixedProfile))
+    		$mixedProfile = BxDolModule::getInstance($this->MODULE)->getProfileInfo((int)$mixedProfile);
+
+    	return $this->displayLink('link', array(
+    		'href' => $mixedProfile['link'],
+            'title' => bx_html_attribute(!empty($mixedProfile['title']) ? $mixedProfile['title'] : $mixedProfile['name']),
+            'content' => $mixedProfile['name']
+    	));
+    }
+
+    public function displayLink($sTemplate, $aParams)
+    {
+    	return $this->parseHtmlByName($sTemplate . '.html', array(
+            'href' => $aParams['href'],
+            'title' => $aParams['title'],
+            'content' => $aParams['content']
+        ));
+    }
+
+    protected function displayPageCodeText($sTitle, $sText, $bWrap = true, $bCenter = false)
+    {
+        if($bWrap)
+            $sText = MsgBox(_t($sText));
+
+        if($bCenter)
+            $sText = $this->parsePageByName('center.html', array(
+                'content' => $sText
+            ));
+
+		$aParams = array(
+            'title' => array(
+                'page' => _t($sTitle)
+            ),
+            'content' => array(
+                'page_main_code' => $sText
+            )
+        );
+        $this->displayPageCode($aParams);
+    }
+}
+
+/** @} */

+ 3 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/payment/template/center.html

@@ -0,0 +1,3 @@
+<div class="bx-def-font-align-center">
+    <div style="display:inline-block;">__content__</div>
+</div>

+ 193 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/profile/classes/BxBaseModProfileFormsEntryHelper.php

@@ -0,0 +1,193 @@
+<?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);
+        $this->setAutoApproval(isset($oModule->_oConfig->CNF['PARAM_AUTOAPPROVAL']) ? (bool)getParam($oModule->_oConfig->CNF['PARAM_AUTOAPPROVAL']) : true);
+    }
+
+    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
+        $oForm->processFiles($CNF['FIELD_PICTURE'], $iContentId, false);
+        $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
+        $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();
+        $oForm->processFiles($CNF['FIELD_PICTURE'], $iContentId, true);
+        $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 '';
+    }
+
+    protected 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 = $sRedirectCustom;
+        }
+
+        $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);
+    }
+}
+
+/** @} */

+ 52 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/profile/classes/BxBaseModProfileInstaller.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    BaseProfile Base classes for profile modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModProfileInstaller extends BxBaseModGeneralInstaller
+{
+    protected $_sParamDefaultProfileType;
+
+    function __construct($aConfig)
+    {
+        parent::__construct($aConfig);
+
+        $this->_sParamDefaultProfileType = 'sys_account_default_profile_type';
+    }
+
+    function enable($aParams)
+    {
+        $aResult = parent::enable($aParams);
+        if(BxDolService::call($this->_aConfig['name'], 'act_as_profile') !== true)
+            return $aResult;
+
+        if($aResult['result'] && getParam($this->_sParamDefaultProfileType) == '')
+        	setParam($this->_sParamDefaultProfileType, $this->_aConfig['name']);
+
+        return $aResult;
+    }
+
+    function disable($aParams)
+    {
+        $aResult = parent::disable($aParams);
+        if(BxDolService::call($this->_aConfig['name'], 'act_as_profile') !== true)
+            return $aResult;
+
+        if($aResult['result'] && getParam($this->_sParamDefaultProfileType) == $this->_aConfig['name'])
+        	setParam($this->_sParamDefaultProfileType, '');
+
+        if ($aResult['result']) { // disabling was successful
+            // TODO: switch accounts context which active profiles belong to this module
+        }
+
+        return $aResult;
+    }
+}
+
+/** @} */

+ 771 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/profile/classes/BxBaseModProfileModule.php

@@ -0,0 +1,771 @@
+<?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)
+            $aRet[] = array ('label' => $this->serviceProfileName($r['content_id']), 'value' => $r['profile_id']);
+        return $aRet;
+    }
+
+    public function serviceProfileUnit ($iContentId)
+    {
+        return $this->_serviceTemplateFunc('unit', $iContentId);
+    }
+
+    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);
+    }
+    
+    public function servicePrepareFields ($aFieldsProfile)
+    {
+        return $aFieldsProfile;
+    }
+
+    public function serviceFormsHelper ()
+    {
+        return parent::serviceFormsHelper ();
+    }
+
+    public function serviceEntityCreate ()
+    {
+	    BxDolInformer::getInstance($this->_oTemplate)->setEnabled(false);
+
+	    return parent::serviceEntityCreate ();
+    }
+
+    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)
+    {
+        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;
+
+        $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)
+    {
+    	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;
+
+		return BxDolAcl::getInstance()->getProfileMembership($aContentInfo['profile_id']);
+    }
+    public function serviceProfileFriends ($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;
+
+        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;
+    }
+
+    /**
+     * 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)
+    {
+        return $this->_serviceGetNotification($aEvent, '_bx_' . $this->_oConfig->getUri() . '_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)
+    {
+        if (CHECK_ACTION_RESULT_ALLOWED !== ($sMsg = $this->checkAllowedView($aDataEntry))) {
+            bx_import('BxDolPrivacy');
+            if (BX_DOL_PG_FRIENDS == $aDataEntry[$this->_oConfig->CNF['FIELD_ALLOW_VIEW_TO']])
+                return CHECK_ACTION_RESULT_ALLOWED;
+        }
+        
+        return $sMsg;
+    }
+
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedViewCoverImage ($aDataEntry, $isPerformAction = false)
+    {
+        return $this->checkAllowedView($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']);
+        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');
+
+        $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));
+    }
+}
+
+/** @} */

+ 229 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/profile/classes/BxBaseModProfileTemplate.php

@@ -0,0 +1,229 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseProfile Base classes for profile modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+/*
+ * Profile based modules representation.
+ */
+class BxBaseModProfileTemplate extends BxBaseModGeneralTemplate
+{
+    function __construct(&$oConfig, &$oDb)
+    {
+        parent::__construct($oConfig, $oDb);
+    }
+
+    /**
+     * 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;
+
+        $oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_VIEW']);
+
+        $isPublic = CHECK_ACTION_RESULT_ALLOWED === $this->getModule()->checkAllowedView($aData) || $oPrivacy->isPartiallyVisible($aData[$CNF['FIELD_ALLOW_VIEW_TO']]);
+
+        //$aVars = parent::unitVars ($aData, $isCheckPrivateContent, $sTemplateName);
+
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aData[$CNF['FIELD_ID']], $this->MODULE);
+        
+        // get profile's url
+        $sUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aData[$CNF['FIELD_ID']]);
+
+        // connections object 
+        $sConnectionsObject = isset($CNF['OBJECT_CONNECTIONS']) ? $CNF['OBJECT_CONNECTIONS'] : 'sys_profiles_friends';
+        $oConn = BxDolConnection::getObjectInstance($sConnectionsObject);
+        $aConnectionTitles = $this->getModule()->serviceGetConnectionButtonsTitles($oProfile->id(), $sConnectionsObject);
+
+        // generate html
+        $aVars = array (
+            'id' => $aData[$CNF['FIELD_ID']],
+            'thumb_url' => $isPublic ? $this->thumb ($aData) : $this->getImageUrl('no-picture-thumb.png'),
+            'cover_url' => CHECK_ACTION_RESULT_ALLOWED === $this->getModule()->checkAllowedViewCoverImage($aData) ? $this->urlCoverUnit ($aData, true) : $this->getImageUrl('cover.jpg'),
+            'content_url' => $isPublic ? $sUrl : 'javascript:void(0);',
+            'title' => bx_process_output($aData[$CNF['FIELD_NAME']]),
+            'module_name' => _t($CNF['T']['txt_sample_single']),
+            'ts' => $aData[$CNF['FIELD_ADDED']],
+            'bx_if:info' => array(
+                'condition' => true,
+                'content' => array (
+                    'members' => $isPublic ? _t($CNF['T']['txt_N_fans'], $oConn ? $oConn->getConnectedInitiatorsCount($oProfile->id(), true) : 0) : '&nbsp;',
+                    'bx_if:btn' => array (
+                        'condition' => isLogged() && !empty($aConnectionTitles['add']) && CHECK_ACTION_RESULT_ALLOWED === $this->getModule()->checkAllowedFriendAdd($aData),
+                        'content' => array (
+                            'id' => $oProfile->id(),
+                            'title' => isset($aConnectionTitles['add']) ? $aConnectionTitles['add'] : '',
+                            'object' => $sConnectionsObject,
+                        ),
+                    ),
+                ),
+            ),
+        );
+
+        return $aVars;
+    }
+
+    /**
+     * Get profile cover
+     */
+    function setCover ($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;
+        }
+        
+        $sUrl = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&id=' . $aData[$CNF['FIELD_ID']]);
+
+        $sUrlPicture = $this->urlPicture ($aData);
+        $sUrlAvatar = $this->urlAvatar ($aData);
+        $sUrlPictureChange = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_EDIT_ENTRY'] . '&id=' . $aData[$CNF['FIELD_ID']]);
+
+        $sUrlCover = $this->urlCover ($aData);
+        $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' => bx_process_output($aData[$CNF['FIELD_NAME']]),
+            //'menu' => BxDolMenu::getObjectInstance($CNF['OBJECT_MENU_SUBMENU_VIEW_ENTRY_COVER'])->getCode(), // TODO: check if menu is used somewhere
+
+            'action_menu' => $oMenu ? $oMenu->getCode() : '',
+
+            'picture_avatar_url' => $sUrlAvatar,
+            '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;
+    }
+}
+
+/** @} */

+ 156 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/profile/template/css/main.css

@@ -0,0 +1,156 @@
+
+@import url(../../../general/template/css/main.css);
+
+/* browse unit */
+
+.bx-base-pofile-unit {
+    width:184px;
+    width:11.5rem;
+    float:left;
+}
+
+.bx-base-pofile-unit div {
+    width:136px;
+    width:8.5rem;
+}
+
+/* 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;
+}
+
+.bx-base-pofile-unit-cover img {
+    background-color:#fff;
+    vertical-align:middle;
+}
+
+.bx-base-pofile-unit-info,
+.bx-base-pofile-unit-title {
+    width:17.5rem;
+}
+
+.bx-base-pofile-unit-with-cover {
+    display:inline-block;
+    overflow:hidden;
+}
+
+.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 {
+    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;
+}
+
+/* view page */
+.bx-base-profile-entity-content,
+.bx-base-profile-entity-content p {
+    overflow: hidden;
+    text-overflow: ellipsis;
+}

+ 47 - 0
upgrade/files/9.0.0.B5-9.0.0.RC1/files/modules/base/text/classes/BxBaseModTextDb.php

@@ -0,0 +1,47 @@
+<?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 database queries
+ */
+class BxBaseModTextDb extends BxBaseModGeneralDb
+{
+    public function __construct(&$oConfig)
+    {
+        parent::__construct($oConfig);
+    }
+
+    public function searchByAuthorTerm($iAuthor, $sTerm, $iLimit)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if (empty($CNF['FIELDS_QUICK_SEARCH']))
+            return array();
+
+		$aBindings = array(
+		    'author' => $iAuthor
+		);
+
+        $sWhere = '';
+        foreach ($CNF['FIELDS_QUICK_SEARCH'] as $sField) {
+        	$aBindings[$sField] = '%' . $sTerm . '%';
+
+            $sWhere .= " OR `c`.`$sField` LIKE :" . $sField;
+        }
+
+        $sOrderBy = $this->prepareAsString(" ORDER BY `c`.`added` DESC LIMIT ?", (int)$iLimit);
+
+        $sQuery = "SELECT `c`.* FROM `" . $this->_oConfig->CNF['TABLE_ENTRIES'] . "` AS `c` WHERE `c`.`" . $CNF['FIELD_AUTHOR'] . "`=:author AND (0 $sWhere)" . $sOrderBy;
+        return $this->getAll($sQuery, $aBindings);
+    }
+}
+
+/** @} */

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio