Преглед изворни кода

Upgrade script from 9.0.0.B2 to 9.0.0.B3

Alex Trofimov пре 10 година
родитељ
комит
c00a9f69b0
100 измењених фајлова са 19188 додато и 0 уклоњено
  1. 16 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/.htaccess
  2. 18 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/README.md
  3. 24 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/cart.php
  4. 56 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/cmts.php
  5. 34 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/conn.php
  6. 30 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/favorite.php
  7. 30 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/get_rss_feed.php
  8. 31 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/grid.php
  9. 15 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/gzip_loader.php
  10. 52 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/image_transcoder.php
  11. 89 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDol.php
  12. 427 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolAccount.php
  13. 282 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolAccountQuery.php
  14. 704 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolAcl.php
  15. 388 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolAclQuery.php
  16. 166 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolAlerts.php
  17. 37 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCache.php
  18. 96 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCacheAPC.php
  19. 135 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCacheFile.php
  20. 70 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCacheFileHtml.php
  21. 96 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCacheMemcache.php
  22. 127 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCacheUtilities.php
  23. 85 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCacheXCache.php
  24. 17 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCalendar.php
  25. 134 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCaptcha.php
  26. 37 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCaptchaQuery.php
  27. 125 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCategory.php
  28. 54 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCategoryQuery.php
  29. 1108 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCmts.php
  30. 440 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCmtsQuery.php
  31. 708 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolConnection.php
  32. 235 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolConnectionQuery.php
  33. 160 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCover.php
  34. 92 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCron.php
  35. 80 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCronAccount.php
  36. 55 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCronAcl.php
  37. 121 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCronPruning.php
  38. 78 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCronQuery.php
  39. 22 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCronStorage.php
  40. 25 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCronTranscoder.php
  41. 27 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCronUpgradeCheck.php
  42. 21 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCronUpgradeModulesCheck.php
  43. 1115 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolDb.php
  44. 93 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolDesigns.php
  45. 162 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolEditor.php
  46. 37 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolEditorQuery.php
  47. 194 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolEmailTemplates.php
  48. 68 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolEmailTemplatesQuery.php
  49. 18 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolFactory.php
  50. 242 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolFavorite.php
  51. 60 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolFavoriteQuery.php
  52. 205 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolFile.php
  53. 1629 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolForm.php
  54. 56 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolFormNested.php
  55. 198 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolFormQuery.php
  56. 145 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolFtp.php
  57. 550 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolGrid.php
  58. 172 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolGridConnections.php
  59. 88 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolGridQuery.php
  60. 141 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolGzip.php
  61. 70 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolIO.php
  62. 301 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolImageResize.php
  63. 126 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolInformer.php
  64. 85 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolInstallerHasher.php
  65. 170 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolInstallerUtils.php
  66. 132 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolKey.php
  67. 52 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolKeyQuery.php
  68. 355 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolLanguages.php
  69. 386 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolLanguagesQuery.php
  70. 236 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolLiveUpdates.php
  71. 36 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolLiveUpdatesQuery.php
  72. 278 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolMenu.php
  73. 76 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolMenuQuery.php
  74. 598 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolMetatags.php
  75. 98 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolMetatagsQuery.php
  76. 61 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolMinify.php
  77. 169 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolModule.php
  78. 129 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolModuleConfig.php
  79. 37 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolModuleDb.php
  80. 204 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolModuleQuery.php
  81. 102 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolModuleTemplate.php
  82. 288 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolObject.php
  83. 137 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolObjectQuery.php
  84. 265 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolPage.php
  85. 93 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolPageQuery.php
  86. 358 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolPaginate.php
  87. 284 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolPayments.php
  88. 32 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolPaymentsQuery.php
  89. 259 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolPermalinks.php
  90. 339 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolPrivacy.php
  91. 186 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolPrivacyQuery.php
  92. 484 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolProfile.php
  93. 45 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolProfileForms.php
  94. 261 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolProfileQuery.php
  95. 145 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolProfileUndefined.php
  96. 239 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolReport.php
  97. 71 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolReportQuery.php
  98. 154 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolRequest.php
  99. 96 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolRss.php
  100. 31 0
      upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolRssBoonEx.php

+ 16 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/.htaccess

@@ -0,0 +1,16 @@
+
+Options -MultiViews -Indexes
+
+<IfModule mod_php5.c>
+    php_flag allow_url_include Off
+    php_flag register_globals Off
+</IfModule>
+
+<IfModule mod_rewrite.c>
+    RewriteEngine on
+    RewriteRule ^m/(.*)$  modules/index.php?r=$1 [QSA,L]
+    RewriteRule ^page/(.*)$  page.php?i=$1 [QSA,L]
+    RewriteRule ^s/([a-zA-Z0-9_]+)/([a-zA-Z0-9\.]+)  storage.php?o=$1&f=$2   [QSA,L]
+</IfModule>
+
+AddType image/svg+xml svg

+ 18 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/README.md

@@ -0,0 +1,18 @@
+#UNA (United Networking Applications) - [https://una.io](https://una.io)
+
+UNA is a **C**MS - **Community** Management System. It is not just one thing or one application. UNA is a combination of various web applications, databases, server apps and supporting ecosystem, which together enable you to create a community website. 
+
+An online community powered by UNA can work as a 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.  
+
+While inherently designed as a multi-user community platform, UNA can be used as a conventional CMS or blogging platform with a focus on content publishing. The full potential, however, lies in networking. Social networking, business networking or even networking with Internet-connected devices. 
+
+##License
+
+UNA is licensed under the MIT License, see license.txt for full details.
+
+##Help
+- [UNA,Inc. Community 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)
+

+ 24 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/cart.php

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

+ 56 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/cmts.php

@@ -0,0 +1,56 @@
+<?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();
+
+$sSys = isset($_REQUEST['sys']) ? bx_process_input($_REQUEST['sys']) : '';
+$iObjectId = isset($_REQUEST['id']) ? bx_process_input($_REQUEST['id'], BX_DATA_INT) : 0;
+$sAction = isset($_REQUEST['action']) && preg_match ('/^[A-Za-z_-]+$/', $_REQUEST['action']) ? bx_process_input($_REQUEST['action']) : '';
+
+$oCmts = BxDolCmts::getObjectInstance($sSys, $iObjectId, true);
+
+if ($oCmts && $sSys && $iObjectId) {
+    if($sAction) {
+        header('Content-Type: text/html; charset=utf-8');
+        $sMethod = 'action' . $sAction;
+        echo $oCmts->$sMethod();
+        exit;
+    }
+
+    $iCmtId = bx_get('cmt_id');
+    if($iCmtId !== false) {
+        $oTemplate = BxDolTemplate::getInstance();
+        $sComment = $oCmts->getCommentBlock($iCmtId);
+        if ($sComment) {
+
+            $sObjectTitle = $oCmts->getObjectTitle($iObjectId);
+
+            $sHeader = _t('_cmt_page_view_header', $sObjectTitle);
+            $sTitle = _t('_cmt_page_view_title', $oCmts->getBaseUrl(), $sObjectTitle);
+            $sContent = DesignBoxContent($sTitle, $sComment, BX_DB_PADDING_DEF);
+            
+            $oTemplate->setPageNameIndex(BX_PAGE_DEFAULT);
+            $oTemplate->setPageHeader($sHeader);
+            $oTemplate->setPageContent('page_main_code', $sContent);
+            $oTemplate->getPageCode();
+
+        } else {
+
+            $oTemplate->displayPageNotFound();
+
+        }
+    }
+}
+
+/** @} */

+ 34 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/conn.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");
+
+check_logged();
+
+$sObj = bx_get('obj') ? bx_get('obj') : '';
+$sAct = bx_get('act') && preg_match ('/^[A-Za-z_]+$/', bx_get('act')) ? bx_get('act') : '';
+$sFmt = bx_get('fmt') ? bx_get('fmt') : 'json';
+
+$oConn = BxDolConnection::getObjectInstance($sObj);
+
+$sMethod = 'action' . $sAct;
+if ($oConn && $sAct && method_exists($oConn, $sMethod)) {
+
+    echo $oConn->outputActionResult($oConn->$sMethod(), $sFmt);
+    exit;
+
+} else {
+
+    $oTemplate = BxDolTemplate::getInstance();
+    $oTemplate->displayPageNotFound();
+
+}
+
+/** @} */

+ 30 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/favorite.php

@@ -0,0 +1,30 @@
+<?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();
+
+$sSys = isset($_REQUEST['sys']) ? bx_process_input($_REQUEST['sys']) : false;
+$iObjectId = isset($_REQUEST['object_id']) ? bx_process_input($_REQUEST['object_id'], BX_DATA_INT) : 0;
+$sAction = isset($_REQUEST['action']) && preg_match ('/^[A-Za-z_-]+$/', $_REQUEST['action']) ? bx_process_input($_REQUEST['action']) : '';
+
+$oFavorite = BxDolFavorite::getObjectInstance($sSys, $iObjectId, true);
+
+if ($oFavorite && $sSys && $iObjectId && $sAction) {
+    header('Content-Type: text/html; charset=utf-8');
+    $sMethod = 'action' . ucfirst($sAction);
+    if(method_exists($oFavorite, $sMethod))
+        echo $oFavorite->$sMethod();
+}
+
+/** @} */

+ 30 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/get_rss_feed.php

@@ -0,0 +1,30 @@
+<?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");
+
+$sObject = bx_process_input(bx_get('object'));
+$mixedId = bx_process_input(bx_get('id'));
+$iUserId = bx_process_input(bx_get('member'), BX_DATA_INT);
+
+$oRss = BxDolRss::getObjectInstance($sObject);
+
+if ($oRss && ($s = $oRss->getFeed($mixedId, $iUserId))) {
+
+    header('Content-type: text/xml; charset=utf-8');
+	echo $s;
+
+} else {
+
+    BxDolTemplate::getInstance()->displayPageNotFound();
+    
+}
+
+/** @} */

+ 31 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/grid.php

@@ -0,0 +1,31 @@
+<?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");
+require_once(BX_DIRECTORY_PATH_INC . "utils.inc.php");
+
+bx_import('BxDolLanguages');
+
+$sObject = bx_process_input(bx_get('o'));
+if (!$sObject)
+    exit;
+
+$oGrid = BxDolGrid::getObjectInstance($sObject);
+if (!$oGrid) {
+    // no such grid object available
+    exit;
+}
+
+$sAction = 'performAction' . bx_gen_method_name(bx_process_input(bx_get('a')));
+if (method_exists($oGrid, $sAction)) {
+    $oGrid->$sAction();
+}
+
+/** @} */

+ 15 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/gzip_loader.php

@@ -0,0 +1,15 @@
+<?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');
+
+$sFile = bx_process_input($_GET['file']);
+BxDolGzip::load($sFile);
+
+/** @} */

+ 52 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/image_transcoder.php

@@ -0,0 +1,52 @@
+<?php
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+ob_start();
+
+require_once('./inc/header.inc.php');
+
+$sTranscoderObject = bx_process_input(bx_get('o'));
+$sHandler = bx_process_input(bx_get('h'));
+
+$oTranscoder = BxDolTranscoderImage::getObjectInstance($sTranscoderObject);
+
+if (!$oTranscoder) {
+    ob_end_clean();
+    bx_transcoder_error_occured();
+    exit;
+}
+
+ob_end_clean();
+
+if (isset($_GET['dpx']))
+    $oTranscoder->forceDevicePixelRatio((int)$_GET['dpx']);
+
+if (!$oTranscoder->isFileReady($sHandler) && !$oTranscoder->transcode ($sHandler)) {
+    bx_transcoder_error_occured();
+    exit;
+}
+
+$sImageUrl = $oTranscoder->getFileUrl($sHandler);
+if (!$sImageUrl) {
+    bx_transcoder_error_occured();
+    exit;
+}
+
+header('HTTP/1.1 301 Moved Permanently');
+header('Location: ' . $sImageUrl);
+exit;
+
+function bx_transcoder_error_occured($sMethod = 'displayPageNotFound')
+{
+    require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+    $oTemplate = BxDolTemplate::getInstance();
+    $oTemplate->$sMethod ();
+}
+
+/** @} */

+ 89 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDol.php

@@ -0,0 +1,89 @@
+<?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
+ * @{
+ */
+
+/**
+ * @page objects Objects
+ * Classes which represents high level programming constructions to generate ready functionality, like Comments, Votings, Forms.
+ */
+
+/**
+ * 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();
+}
+
+/**
+ * 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);
+    public function serviceProfilesSearch ($sTerm, $iLimit);
+    public function serviceFormsHelper ();
+    public function serviceActAsProfile ();
+    public function servicePrepareFields ($aFieldsProfile);
+}
+
+/** @} */

+ 427 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolAccount.php

@@ -0,0 +1,427 @@
+<?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();
+
+        if (preg_match("/^[_\.0-9a-z-]+@([0-9a-z][0-9a-z-]+\.)+[a-z]{2,4}$/i", $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)
+    {
+        $aCheck = checkActionModule($iProfileId, 'delete account', 'system', $isPerformAction);
+        if ($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return MsgBox($aCheck[CHECK_ACTION_MESSAGE]);
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
+}
+
+/** @} */

+ 282 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolAccountQuery.php

@@ -0,0 +1,282 @@
+<?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__];
+    }
+
+    /**
+     * 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);
+    }
+}
+
+/** @} */

+ 704 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolAcl.php

@@ -0,0 +1,704 @@
+<?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 $iPermissions - integer value to check permissions for, every bit is matched with some membership id
+     * @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($iPermissions, $iProfileId = false)
+    {
+        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;
+        }
+
+        // profile is not active, so return standard memberships according to profile status
+        if (BX_PROFILE_STATUS_ACTIVE != $sProfileStatus) {
+            $oAccount = $aProfileInfo ? BxDolAccount::getInstance($aProfileInfo['account_id']) : false;
+            if ($oAccount && !$oAccount->isConfirmed())
+                $iLevelId = MEMBERSHIP_ID_UNCONFIRMED; // every account's profile is unconfirmed if account is unconfirmed
+            elseif (!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);
+}
+
+/** @} */

+ 388 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolAclQuery.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    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Database queries for ACL
+ * @see BxDolAcl
+ */
+class BxDolAclQuery extends BxDolDb implements iBxDolSingleton
+{
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolAclQuery();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    function getLevels($aParams, &$aItems, $bReturnCount = true)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sSelectClause = $sJoinClause = $sWhereClause = $sOrderClause = $sLimitClause = "";
+
+        if(!isset($aParams['order']) || empty($aParams['order']))
+           $sOrderClause = "ORDER BY `tal`.`Order` ASC";
+
+        switch($aParams['type']) {
+            case 'by_id':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = array(
+                	'id' => $aParams['value']
+                );
+
+                $sWhereClause .= "AND `tal`.`ID`=:id";
+                $sLimitClause .= "LIMIT 1";
+                break;
+
+            case 'all_active':
+                $sWhereClause .= "AND `tal`.`Active`='yes'";
+                break;
+
+            case 'all_active_purchasble_pair':
+                $aMethod['name'] = "getPairs";
+                $aMethod['params'][1] = 'id';
+                $aMethod['params'][2] = 'name';
+                $sWhereClause .= "AND `tal`.`Active`='yes' AND `tal`.`Purchasable`='yes'";
+                break;
+
+            case 'all_active_pair':
+                $aMethod['name'] = "getPairs";
+                $aMethod['params'][1] = 'id';
+                $aMethod['params'][2] = 'name';
+                $sWhereClause .= "AND `tal`.`Active`='yes'";
+                break;
+
+            case 'all_pair':
+                $aMethod['name'] = "getPairs";
+                $aMethod['params'][1] = 'id';
+                $aMethod['params'][2] = 'name';
+                break;
+
+            case 'all_order_id':
+                $sOrderClause = "ORDER BY `tal`.`ID` ASC";
+                break;
+
+            case 'all':
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT " . ($bReturnCount ? "SQL_CALC_FOUND_ROWS" : "") . "
+                `tal`.`ID` AS `id`,
+                `tal`.`Name` AS `name`,
+                `tal`.`Icon` AS `icon`,
+                `tal`.`Description` AS `description`,
+                `tal`.`Active` AS `active`,
+                `tal`.`Purchasable` AS `purchasable`,
+                `tal`.`Removable` AS `removable`,
+                `tal`.`QuotaSize` AS `quota_size`,
+                `tal`.`QuotaNumber` AS `quota_number`,
+                `tal`.`QuotaMaxFileSize` AS `quota_max_file_size`,
+                `tal`.`Order` AS `order`" . $sSelectClause . "
+            FROM `sys_acl_levels` AS `tal` " . $sJoinClause . "
+            WHERE 1 " . $sWhereClause . " " . $sOrderClause . " " . $sLimitClause;
+        $aItems = call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+
+        if(!$bReturnCount)
+            return !empty($aItems);
+
+        return (int)$this->getOne("SELECT FOUND_ROWS()");
+    }
+
+    function getActions($aParams, &$aItems, $bReturnCount = true)
+    {
+        $aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sSelectClause = $sJoinClause = $sWhereClause = $sGroupClause = $sOrderClause = $sLimitClause = "";
+
+        if(!isset($aParams['order']) || empty($aParams['order']))
+           $sOrderClause = "ORDER BY `taa`.`Title` ASC";
+
+           
+        switch($aParams['type']) {
+            case 'by_names_and_module':
+            	$aMethod['params'][1] = array(
+                	'module' => $aParams['module']
+                );
+
+                $sWhereClause .= " AND `taa`.`Name` IN(" . $this->implode_escape($aParams['value']) . ") AND `taa`.`Module` = :module ";
+                break;
+
+            case 'by_names':
+                $sWhereClause .= " AND `taa`.`Name` IN(" . $this->implode_escape($aParams['value']) . ")";
+                break;
+
+            case 'by_level_id':
+            	$aMethod['params'][1] = array(
+                	'level_id' => $aParams['value'],
+                	'level_code' => pow(2, ($aParams['value'] - 1))
+                );
+
+                $sSelectClause .= ", `tam`.`AllowedCount` AS `allowed_count`, `tam`.`AllowedPeriodLen` AS `allowed_period_len`, `tam`.`AllowedPeriodStart` AS `allowed_period_start`, `tam`.`AllowedPeriodEnd` AS `allowed_period_end`, `tam`.`AdditionalParamValue` AS `additional_param_value` ";
+                $sJoinClause .= "LEFT JOIN `sys_acl_matrix` AS `tam` ON `taa`.`ID`=`tam`.`IDAction` ";
+                $sWhereClause .= "AND `tam`.`IDLevel`=:level_id AND (`taa`.`DisabledForLevels`='0' OR `taa`.`DisabledForLevels`&:level_code=0)";
+                break;
+
+            case 'by_level_id_key_id':
+                $aMethod['name'] = 'getAllWithKey';
+                $aMethod['params'][1] = 'id';
+                $aMethod['params'][2] = array(
+                	'level_id' => $aParams['value']
+                );
+
+                $sSelectClause .= ", `tam`.`AllowedCount` AS `allowed_count`, `tam`.`AllowedPeriodLen` AS `allowed_period_len` ";
+                $sJoinClause .= "LEFT JOIN `sys_acl_matrix` AS `tam` ON `taa`.`ID`=`tam`.`IDAction` ";
+                $sWhereClause .= "AND `tam`.`IDLevel`=:level_id";
+                break;
+
+            case 'counter_by_modules':
+                $aMethod['name'] = 'getPairs';
+                $aMethod['params'][1] = 'module';
+                $aMethod['params'][2] = 'counter';
+                $sSelectClause = ", COUNT(*) AS `counter`";
+                $sGroupClause = "GROUP BY `taa`.`Module`";
+                break;
+
+            case 'counter_by_levels':
+                $aMethod['name'] = 'getPairs';
+                $aMethod['params'][1] = 'level_id';
+                $aMethod['params'][2] = 'counter';
+                $sSelectClause = ", `tam`.`IDLevel` AS `level_id`, COUNT(`tam`.`IDAction`) AS `counter`";
+                $sJoinClause = "LEFT JOIN `sys_acl_matrix` AS `tam` ON `taa`.`ID`=`tam`.`IDAction` AND (`taa`.`DisabledForLevels`='0' OR `taa`.`DisabledForLevels`&POW(2, `tam`.`IDLevel`-1)=0) ";
+                $sGroupClause = "GROUP BY `tam`.`IDLevel`";
+                break;
+        }
+
+        $aMethod['params'][0] = "SELECT " . ($bReturnCount ? "SQL_CALC_FOUND_ROWS" : "") . "
+                `taa`.`ID` AS `id`,
+                `taa`.`Module` AS `module`,
+                `taa`.`Name` AS `name`,
+                `taa`.`Title` AS `title`,
+                `taa`.`Countable` AS `countable`,
+                `taa`.`DisabledForLevels` AS `disabled_for_levels`" . $sSelectClause . "
+            FROM `sys_acl_actions` AS `taa` " . $sJoinClause . "
+            WHERE 1 " . $sWhereClause . " " . $sGroupClause . " " . $sOrderClause . " " . $sLimitClause;
+        $aItems = call_user_func_array(array($this, $aMethod['name']), $aMethod['params']);
+
+        if(!$bReturnCount)
+            return !empty($aItems);
+
+        return (int)$this->getOne("SELECT FOUND_ROWS()");
+    }
+
+    /**
+     * Fetch the last purchased/assigned membership that is still active for the given profile.
+     *
+     * NOTE. Don't use cache here, because it's causing an error, if a number of memberrship levels are purchased at the same time.
+     * fromMemory returns the same DateExpires because setMembership (old buyMembership) function is called in cycle in the same session.
+     */
+    function getLevelCurrent($iProfileId, $iTime = 0)
+    {
+        $iTime = $iTime == 0 ? time() : (int)$iTime;
+
+        $sSql = $this->prepare("
+            SELECT  `sys_acl_levels_members`.`IDLevel` as `id`,
+                    `sys_acl_levels`.`Name` AS `name`,
+                    `sys_acl_levels`.`QuotaSize` AS `quota_size`,
+                    `sys_acl_levels`.`QuotaNumber` AS `quota_number`,
+                    `sys_acl_levels`.`QuotaMaxFileSize` AS `quota_max_file_size`,
+                    UNIX_TIMESTAMP(`sys_acl_levels_members`.`DateStarts`) as `date_starts`,
+                    UNIX_TIMESTAMP(`sys_acl_levels_members`.`DateExpires`) as `date_expires`,
+                    `sys_acl_levels_members`.`TransactionID` AS `transaction_id`,
+                    `sys_profiles`.`status`
+            FROM `sys_acl_levels_members`
+            RIGHT JOIN `sys_profiles` ON `sys_acl_levels_members`.IDMember = `sys_profiles`.`id`
+                AND (`sys_acl_levels_members`.DateStarts IS NULL OR `sys_acl_levels_members`.DateStarts <= FROM_UNIXTIME(?))
+                AND (`sys_acl_levels_members`.DateExpires IS NULL OR `sys_acl_levels_members`.DateExpires > FROM_UNIXTIME(?))
+            LEFT JOIN `sys_acl_levels` ON `sys_acl_levels_members`.IDLevel = `sys_acl_levels`.ID
+            WHERE `sys_profiles`.`id` = ?
+            ORDER BY `sys_acl_levels_members`.DateStarts DESC
+            LIMIT 1", $iTime, $iTime, $iProfileId);
+
+        return $this->getRow($sSql);
+    }
+
+    function getLevelByIdCached($iLevel)
+    {
+        $sQuery = $this->prepare("SELECT
+                `tal`.`ID` AS `id`,
+                `tal`.`Name` AS `name`,
+                `tal`.`QuotaSize` AS `quota_size`,
+                `tal`.`QuotaNumber` AS `quota_number`,
+                `tal`.`QuotaMaxFileSize` AS `quota_max_file_size`
+            FROM `sys_acl_levels` AS `tal`
+            WHERE `tal`.`ID`=?
+            LIMIT 1", $iLevel);
+        return $this->fromCache('sys_acl_levels' . $iLevel, 'getRow', $sQuery);
+    }
+
+    function getAction($iMembershipId, $iActionId)
+    {
+        $sQuery = $this->prepare("SELECT
+                `tam`.`IDAction` AS `id`,
+                `taa`.`Name` AS `name`,
+                `taa`.`Title` AS `title`,
+                `tam`.`AllowedCount` AS `allowed_count`,
+                `tam`.`AllowedPeriodLen` AS `allowed_period_len`,
+                UNIX_TIMESTAMP(`tam`.`AllowedPeriodStart`) as `allowed_period_start`,
+                UNIX_TIMESTAMP(`tam`.`AllowedPeriodEnd`) as `allowed_period_end`,
+                `tam`.`AdditionalParamValue` AS `additional_param_value`
+            FROM `sys_acl_actions` AS `taa`
+            LEFT JOIN `sys_acl_matrix` AS `tam` ON `tam`.`IDAction` = `taa`.`ID` AND `tam`.`IDLevel` = ?
+            WHERE `taa`.`ID` = ?", $iMembershipId, $iActionId);
+        return $this->getRow($sQuery);
+    }
+
+    function getActionTrack($iActionId, $iProfileId)
+    {
+        $sQuery = $this->prepare("SELECT
+                `taat`.`ActionsLeft` AS `actions_left`,
+                UNIX_TIMESTAMP(`taat`.`ValidSince`) as `valid_since`
+            FROM `sys_acl_actions_track` AS `taat`
+            WHERE `taat`.`IDAction`=? AND `taat`.`IDMember`=?", $iActionId, $iProfileId);
+        return $this->getRow($sQuery);
+    }
+
+    function insertActionTarck($iActionId, $iProfileId, $iActionsLeft, $iValidSince)
+    {
+        $sQuery = $this->prepare("INSERT INTO `sys_acl_actions_track`(`IDAction`, `IDMember`, `ActionsLeft`, `ValidSince`) VALUES (?, ?, ?, FROM_UNIXTIME(?))", $iActionId, $iProfileId, $iActionsLeft, $iValidSince);
+        return (int)$this->query($sQuery) > 0;
+    }
+
+    function updateActionTrack($iActionId, $iProfileId, $iActionsLeft, $iValidSince = 0)
+    {
+    	$aBindings = array(
+    		'actions_left' => $iActionsLeft,
+    		'action_id' => $iActionId,
+    		'member_id' => $iProfileId
+    	);
+
+        $sUpdateAddon = "";
+        if($iValidSince != 0) {
+        	$aBindings['valid_since'] = $iValidSince;
+
+            $sUpdateAddon = ", ValidSince=FROM_UNIXTIME(:valid_since)";
+        }
+
+        $sQuery = "UPDATE `sys_acl_actions_track` SET `ActionsLeft`=:actions_left" . $sUpdateAddon . " WHERE `IDAction`=:action_id AND `IDMember`=:member_id";
+        return (int)$this->query($sQuery, $aBindings) > 0;
+    }
+
+    function insertLevelByProfileId($iProfileId, $iMembershipId, $iDateStarts, $aPeriod, $sTransactionId)
+    {
+    	$aBindings = array(
+			'member_id' => $iProfileId,
+			'level_id' => $iMembershipId,
+			'transaction_id' => $sTransactionId,
+			'date_starts' => $iDateStarts
+		);
+
+    	$sSetClause = '';
+    	if((int)$aPeriod['period'] != 0) {
+    		$aBindings['period'] = (int)$aPeriod['period'];
+
+	    	switch($aPeriod['period_unit']) {
+	    		case MEMBERSHIP_PERIOD_UNIT_DAY:
+	    		case MEMBERSHIP_PERIOD_UNIT_WEEK:
+	    			if($aPeriod['period_unit'] == MEMBERSHIP_PERIOD_UNIT_WEEK)
+	    				$aBindings['period'] *= 7;
+
+	    			$sSetClause = ", `DateExpires`=DATE_ADD(FROM_UNIXTIME(:date_starts), INTERVAL :period DAY)";
+	    			break;
+
+	    		case MEMBERSHIP_PERIOD_UNIT_MONTH:
+	    			$sSetClause = ", `DateExpires`=DATE_ADD(FROM_UNIXTIME(:date_starts), INTERVAL :period MONTH)";
+	    			break;
+
+	    		case MEMBERSHIP_PERIOD_UNIT_YEAR:
+	    			$sSetClause = ", `DateExpires`=DATE_ADD(FROM_UNIXTIME(:date_starts), INTERVAL :period YEAR)";
+	    			break;
+	    	}
+    	}
+
+        $sQuery = $this->prepare("INSERT `sys_acl_levels_members` SET `IDMember`=:member_id, `IDLevel`=:level_id, `DateStarts`=FROM_UNIXTIME(:date_starts), `TransactionID`=:transaction_id" . $sSetClause);
+        return (int)$this->query($sQuery, $aBindings) > 0;
+    }
+
+    function deleteLevelByProfileId($iProfileId, $bAll = false)
+    {
+        $sQuery = $this->prepare("DELETE FROM `sys_acl_levels_members` WHERE `IDMember` = ? " . ($bAll ? "" : " AND (`DateExpires` IS NULL OR `DateExpires` > NOW())"), $iProfileId);
+        return (int)$this->query($sQuery) > 0;
+    }
+
+	function deleteLevelBy($aWhere)
+    {
+    	if(empty($aWhere))
+    		return false;
+
+        $sQuery = "DELETE FROM `sys_acl_levels_members` WHERE " . $this->arrayToSQL($aWhere, ' AND ');
+        return (int)$this->query($sQuery) > 0;
+    }
+
+    function maintenance($iDaysToCleanMemLevels)
+    {
+        $sQuery = $this->prepare("DELETE FROM `sys_acl_levels_members` WHERE `DateExpires` < NOW() - INTERVAL ? DAY", $iDaysToCleanMemLevels);
+        if ($iDeleteMemLevels = $this->query($sQuery))
+            $this->query("OPTIMIZE TABLE `sys_acl_levels_members`");
+        return $iDeleteMemLevels;
+    }
+
+    function clearActionsTracksForMember($iMemberId)
+    {
+        $sQuery = $this->prepare("DELETE FROM `sys_acl_actions_track` WHERE `IDMember` = ?", (int)$iMemberId);
+	    return $this->query($sQuery);
+    }
+
+	function getContentByLevelAsSQLPart($sContentTable, $sContentField, $mixedLevelId)
+    {
+    	$sJoin = $sWhere = ""; 
+
+        if(is_array($mixedLevelId))
+            $sWhere .= " AND `tlm`.`IDLevel` IN (" . $this->implode_escape($mixedLevelId) . ")";
+        else
+            $sWhere .= $this->prepareAsString(" AND `tlm`.`IDLevel` = ?", (int)$mixedLevelId);
+		$sWhere .= " AND (`tlm`.`DateStarts` IS NULL OR `tlm`.`DateStarts` <= NOW()) AND (`tlm`.`DateExpires` IS NULL OR `tlm`.`DateExpires` > NOW())";
+
+		$sJoin .= " INNER JOIN `sys_acl_levels_members` AS `tlm` ON `" . $sContentTable . "`.`" . $sContentField . "`=`tlm`.`IDMember`";
+		$sJoin .= " INNER JOIN `sys_acl_levels` AS `tl` ON `tlm`.`IDLevel`=`tl`.`ID`"; 
+
+        return array(
+            'where' => $sWhere,
+        	'join' => $sJoin
+        );
+    }
+
+    function getProfilesByMembership($mixedLevelId)
+    {
+    	$aSqlParts = $this->getContentByLevelAsSQLPart('sys_profiles', 'id', $mixedLevelId);
+
+    	$sSql = $this->prepare("SELECT `sys_profiles`.* FROM `sys_profiles`" . $aSqlParts['join'] . " WHERE 1" . $aSqlParts['where']);
+    	return $this->getAll($sSql); 
+    }
+}
+
+/** @} */

+ 166 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolAlerts.php

@@ -0,0 +1,166 @@
+<?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
+ * @{
+ */
+
+/**
+ * Alert/Handler engine.
+ *
+ * Is needed to fire some alert(event) in one place and caught it with a handler somewhere else.
+ *
+ * Related classes:
+ *  BxDolAlertsResponse - abstract class for all response classes.
+ *  BxDolAlertsResponseUser - response class to process standard profile related alerts.
+ *
+ * Example of usage:
+ * 1. Fire an alert
+ *
+ * @code
+ * $oZ = new BxDolAlerts('unit_name', 'action', 'object_id', 'sender_id', 'extra_params');
+ * $oZ->alert();
+ * @endcode
+ *
+ * 2. Add handler and caught alert(s) @see BxDolAlertsResponseUser
+ *  a. Create Response class extending BxDolAlertsResponse class. It should process all necessary alerts which are passed to it.
+ *  b. Register your handler in the database by adding it in `sys_alerts_handlers` table.
+ *  c. Associate necessary alerts with the handler by adding them in the `sys_alerts` table.
+ *
+ *
+ * Memberships/ACL:
+ * Doesn't depend on user's membership.
+ *
+ *
+ * Alerts:
+ * no alerts available
+ *
+ */
+
+bx_import('BxDolDb');
+
+class BxDolAlerts extends BxDol
+{
+    public $sUnit;
+    public $sAction;
+    public $iObject;
+    public $iSender;
+    public $aExtras;
+
+    protected $_aAlerts;
+    protected $_aHandlers;
+
+    /**
+     * Constructor
+     * @param string $sType     - system type
+     * @param string $sAction   - system action
+     * @param int    $iObjectId - object id
+     * @param int    $iSenderId - sender (action's author) profile id, if it is false - then currectly logged in profile id is used
+     */
+    public function __construct($sUnit, $sAction, $iObjectId, $iSender = false, $aExtras = array())
+    {
+        parent::__construct();
+
+        if (getParam('sys_db_cache_enable')) {
+
+            $oDb = BxDolDb::getInstance();
+            $oCache = $oDb->getDbCacheObject();
+            $sCacheKey = $oDb->genDbCacheKey('sys_alerts');
+            $aData = $oCache->getData($sCacheKey);
+            if (null === $aData) {
+                $aData = $this->getAlertsData();
+                $oCache->setData ($sCacheKey, $aData);
+            }
+
+        } else {
+
+            $aData = $this->getAlertsData();
+
+        }
+
+        $this->_aAlerts = $aData['alerts'];
+        $this->_aHandlers = $aData['handlers'];
+
+        $this->sUnit = $sUnit;
+        $this->sAction = $sAction;
+        $this->iObject = (int)$iObjectId;
+        $this->aExtras = $aExtras;
+        if (false === $iSender) {
+            $oProfile = BxDolProfile::getInstance();
+            $this->iSender = $oProfile ? $oProfile->id() : 0;
+        } else {
+            $this->iSender = (int)$iSender;
+        }
+    }
+
+    public static function cacheInvalidate()
+    {
+        return BxDolDb::getInstance()->cleanCache ('sys_alerts');
+    }
+
+    /**
+     * Notifies the necessary handlers about the alert.
+     */
+    public function alert()
+    {
+        if (isset($this->_aAlerts[$this->sUnit]) && isset($this->_aAlerts[$this->sUnit][$this->sAction]))
+            foreach($this->_aAlerts[$this->sUnit][$this->sAction] as $iHandlerId) {
+                $aHandler = $this->_aHandlers[$iHandlerId];
+
+                if (isset($GLOBALS['bx_profiler']) && 'bx_profiler' != $aHandler['name']) $GLOBALS['bx_profiler']->beginAlert($this->sUnit, $this->sAction, $aHandler['name']);
+
+                if (!empty($aHandler['file']) && !empty($aHandler['class']) && file_exists(BX_DIRECTORY_PATH_ROOT . $aHandler['file'])) {
+                    if(!class_exists($aHandler['class'], false))
+                        require_once(BX_DIRECTORY_PATH_ROOT . $aHandler['file']);
+
+                    $oHandler = new $aHandler['class']();
+                    $oHandler->response($this);
+                } else if(!empty($aHandler['service_call']) && BxDolService::isSerializedService($aHandler['service_call'])) {
+                    $aService = unserialize($aHandler['service_call']);
+
+                    $aParams = array($this);
+                    if(isset($aService['params']) && is_array($aService['params']))
+                        $aParams = array_merge($aParams, $aService['params']);
+
+                    BxDolService::call($aService['module'], $aService['method'], $aParams, isset($aService['class']) ? $aService['class'] : 'Module');
+                }
+
+                if (isset($GLOBALS['bx_profiler']) && 'bx_profiler' != $aHandler['name']) $GLOBALS['bx_profiler']->endAlert($this->sUnit, $this->sAction, $aHandler['name']);
+            }
+    }
+
+    /**
+     * Cache alerts and handlers.
+     *
+     * @return an array with all alerts and handlers.
+     */
+    public function getAlertsData()
+    {
+        $oDb = BxDolDb::getInstance();
+        $aResult = array('alerts' => array(), 'handlers' => array());
+
+        $aAlerts = $oDb->getAll("SELECT `unit`, `action`, `handler_id` FROM `sys_alerts` ORDER BY `id` ASC");
+        foreach ($aAlerts as $aAlert)
+            $aResult['alerts'][$aAlert['unit']][$aAlert['action']][] = $aAlert['handler_id'];
+
+        $aHandlers = $oDb->getAll("SELECT `id`, `class`, `file`, `service_call`, `name` FROM `sys_alerts_handlers` ORDER BY `id` ASC");
+        foreach ($aHandlers as $aHandler)
+            $aResult['handlers'][$aHandler['id']] = array('class' => $aHandler['class'], 'file' => $aHandler['file'], 'service_call' => $aHandler['service_call'], 'name' => $aHandler['name']);
+
+        return $aResult;
+    }
+}
+
+class BxDolAlertsResponse extends BxDol
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    public function response($oAlert) {}
+}
+
+/** @} */

+ 37 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCache.php

@@ -0,0 +1,37 @@
+<?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 BxDolCache extends BxDol
+{
+    /**
+     * Is cache engine available?
+     * @return boolean
+     */
+    function isAvailable()
+    {
+        return true;
+    }
+
+    /**
+     * Are required php modules are installed for this cache engine ?
+     * @return boolean
+     */
+    function isInstalled()
+    {
+        return true;
+    }
+
+    function getData($sKey, $iTTL = false) {}
+    function setData($sKey, $mixedData, $iTTL = false) {}
+    function delData($sKey) {}
+    function removeAllByPrefix ($s) {}
+    function getSizeByPrefix ($s) {}
+}
+
+/** @} */

+ 96 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCacheAPC.php

@@ -0,0 +1,96 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolCacheAPC extends BxDolCache
+{
+    protected $iTTL = 3600;
+
+    /**
+     * Get data from shared memory cache
+     *
+     * @param  string $sKey - file name
+     * @param  int    $iTTL - time to live
+     * @return the    data is got from cache.
+     */
+    function getData($sKey, $iTTL = false)
+    {
+        $isSucess = false;
+        $mixedData = apc_fetch ($sKey, $isSucess);
+        if (!$isSucess)
+            return null;
+
+        return $mixedData;
+    }
+    /**
+     * Save data in shared memory cache
+     *
+     * @param  string  $sKey      - file name
+     * @param  mixed   $mixedData - the data to be cached in the file
+     * @param  int     $iTTL      - time to live
+     * @return boolean result of operation.
+     */
+    function setData($sKey, $mixedData, $iTTL = false)
+    {
+        return apc_store ($sKey, $mixedData, false === $iTTL ? $this->iTTL : $iTTL);
+    }
+
+    /**
+     * Delete cache from shared memory
+     *
+     * @param  string $sKey - file name
+     * @return result of the operation
+     */
+    function delData($sKey)
+    {
+        $isSucess = false;
+        apc_fetch ($sKey, $isSucess);
+        if (!$isSucess)
+            return true;
+
+        return apc_delete($sKey);
+    }
+
+    /**
+     * Check if apc cache functions are available
+     * @return boolean
+     */
+    function isAvailable()
+    {
+        return function_exists('apc_store');
+    }
+
+    /**
+     * Check if apc extension is loaded
+     * @return boolean
+     */
+    function isInstalled()
+    {
+        return extension_loaded('apc');
+    }
+
+    /**
+     * remove all data from cache by key prefix
+     * @return true on success
+     */
+    function removeAllByPrefix ($s)
+    {
+        $l = strlen($s);
+        $aKeys = apc_cache_info('user');
+        if (isset($aKeys['cache_list']) && is_array($aKeys['cache_list'])) {
+            foreach ($aKeys['cache_list'] as $r) {
+                $sKey = $r['info'];
+                if (0 === strncmp($sKey, $s, $l))
+                    $this->delData($sKey);
+            }
+        }
+        return true;
+    }
+}
+
+/** @} */

+ 135 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCacheFile.php

@@ -0,0 +1,135 @@
+<?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 BxDolCacheFile extends BxDolCache
+{
+    protected $sPath;
+
+    /**
+     * constructor
+     */
+    function __construct()
+    {
+        parent::__construct();
+        $this->sPath = BX_DIRECTORY_PATH_CACHE;
+    }
+
+    /**
+     * Get all data from the cache file.
+     *
+     * @param  string $sKey - file name
+     * @param  int    $iTTL - time to live
+     * @return the    data is got from cache.
+     */
+    function getData($sKey, $iTTL = false)
+    {
+        if (!file_exists($this->sPath . $sKey))
+            return null;
+
+        if ($iTTL > 0 && $this->_removeFileIfTtlExpired ($this->sPath . $sKey, $iTTL))
+            return null;
+
+        include($this->sPath . $sKey);
+        return $mixedData;
+    }
+
+    /**
+     * Save all data in cache file.
+     *
+     * @param  string  $sKey      - file name
+     * @param  mixed   $mixedData - the data to be cached in the file
+     * @param  int     $iTTL      - time to live
+     * @return boolean result of operation.
+     */
+    function setData($sKey, $mixedData, $iTTL = false)
+    {
+        if(file_exists($this->sPath . $sKey) && !is_writable($this->sPath . $sKey))
+           return false;
+
+        if(!($rHandler = fopen($this->sPath . $sKey, 'w')))
+           return false;
+
+        fwrite($rHandler, '<?php $mixedData=' . var_export($mixedData, true) . '; ?>');
+        fclose($rHandler);
+        @chmod($this->sPath . $sKey, 0666);
+
+        if (function_exists('opcache_invalidate')) opcache_invalidate($this->sPath . $sKey);
+
+        return true;
+    }
+
+    /**
+     * Delete cache file.
+     *
+     * @param  string $sKey - file name
+     * @return result of the operation
+     */
+    function delData($sKey)
+    {
+        $sFile = $this->sPath . $sKey;
+        return !file_exists($sFile) || @unlink($sFile);
+    }
+
+    /**
+     * remove all data from cache by key prefix
+     * @return true on success
+     */
+    function removeAllByPrefix ($s)
+    {
+        if (!($rHandler = opendir($this->sPath)))
+            return false;
+
+        $l = strlen($s);
+        while (($sFile = readdir($rHandler)) !== false)
+            if (0 === strncmp($sFile, $s, $l))
+                @unlink ($this->sPath . $sFile);
+
+        closedir($rHandler);
+
+        return true;
+    }
+
+    /**
+     * get size of cached data by name prefix
+     */
+    function getSizeByPrefix ($s)
+    {
+        if (!($rHandler = opendir($this->sPath)))
+            return false;
+
+        $iSize = 0;
+        $l = strlen($s);
+        while (($sFile = readdir($rHandler)) !== false)
+            if (0 === strncmp($sFile, $s, $l))
+                $iSize += @filesize ($this->sPath . $sFile);
+
+        closedir($rHandler);
+
+        return $iSize;
+    }
+
+    /**
+     * remove file from dist if TTL expored
+     * @param  string $sFile - full path to filename
+     * @param  int    $iTTL  - time to live in seconds
+     * @return true   if TTL is expired and file is deleted or false otherwise
+     */
+    function _removeFileIfTtlExpired ($sFile, $iTTL)
+    {
+        $iTimeDiff = time() - filectime($sFile);
+        if ($iTimeDiff > $iTTL) {
+            @unlink ($sFile);
+            return true;
+        } else {
+            return false;
+        }
+    }
+}
+
+/** @} */

+ 70 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCacheFileHtml.php

@@ -0,0 +1,70 @@
+<?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 BxDolCacheFileHtml extends BxDolCacheFile
+{
+    /**
+     * Get all data from the cache file.
+     *
+     * @param  string $sKey - file name
+     * @param  int    $iTTL - time to live
+     * @return the    data is got from cache.
+     */
+    function getData($sKey, $iTTL = false)
+    {
+        if(!file_exists($this->sPath . $sKey))
+            return null;
+
+        if ($iTTL > 0 && $this->_removeFileIfTtlExpired ($this->sPath . $sKey, $iTTL))
+            return null;
+
+        return file_get_contents($this->sPath . $sKey);
+    }
+
+    /**
+     * Get full path to cache file
+     */
+    function getDataFilePath($sKey, $iTTL = false)
+    {
+        if (!file_exists($this->sPath . $sKey))
+            return null;
+
+        if ($iTTL > 0 && $this->_removeFileIfTtlExpired ($this->sPath . $sKey, $iTTL))
+            return null;
+
+        return $this->sPath . $sKey;
+    }
+
+    /**
+     * Save all data in cache file.
+     *
+     * @param  string  $sKey      - file name
+     * @param  mixed   $mixedData - the data to be cached in the file
+     * @param  int     $iTTL      - time to live
+     * @return boolean result of operation.
+     */
+    function setData($sKey, $mixedData, $iTTL = false)
+    {
+        if(file_exists($this->sPath . $sKey) && !is_writable($this->sPath . $sKey))
+           return false;
+
+        if(!($rHandler = fopen($this->sPath . $sKey, 'w')))
+           return false;
+
+        fwrite($rHandler, $mixedData);
+        fclose($rHandler);
+        @chmod($this->sPath . $sKey, 0666);
+
+        if (function_exists('opcache_invalidate')) opcache_invalidate($this->sPath . $sKey);
+        
+        return true;
+    }
+}
+
+/** @} */

+ 96 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCacheMemcache.php

@@ -0,0 +1,96 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolCacheMemcache extends BxDolCache
+{
+    protected $iTTL = 3600;
+    protected $iStoreFlag = 0;
+    protected $oMemcache = null;
+
+    /**
+     * constructor
+     */
+    function __construct()
+    {
+        parent::__construct();
+        if (class_exists('Memcache')) {
+            $this->oMemcache = new Memcache();
+            if (!$this->oMemcache->connect (getParam('sys_cache_memcache_host'), getParam('sys_cache_memcache_port')))
+                $this->oMemcache = null;
+        }
+    }
+
+    /**
+     * Get data from cache server
+     *
+     * @param  string $sKey - file name
+     * @param  int    $iTTL - time to live
+     * @return the    data is got from cache.
+     */
+    function getData($sKey, $iTTL = false)
+    {
+        $mixedData = $this->oMemcache->get($sKey);
+        return false === $mixedData ? null : $mixedData;
+    }
+
+    /**
+     * Save data in cache server
+     *
+     * @param  string  $sKey      - file name
+     * @param  mixed   $mixedData - the data to be cached in the file
+     * @param  int     $iTTL      - time to live
+     * @return boolean result of operation.
+     */
+    function setData($sKey, $mixedData, $iTTL = false)
+    {
+        return $this->oMemcache->set($sKey, $mixedData, $this->iStoreFlag, false === $iTTL ? $this->iTTL : $iTTL);
+    }
+
+    /**
+     * Delete cache from cache server
+     *
+     * @param  string $sKey - file name
+     * @return result of the operation
+     */
+    function delData($sKey)
+    {
+        $this->oMemcache->delete($sKey);
+        return true;
+    }
+
+    /**
+     * Check if memcache is available
+     * @return boolean
+     */
+    function isAvailable()
+    {
+        return $this->oMemcache == null ? false : true;
+    }
+
+    /**
+     * Check if memcache extension is loaded
+     * @return boolean
+     */
+    function isInstalled()
+    {
+        return extension_loaded('memcache');
+    }
+
+    /**
+     * remove all data from cache by key prefix
+     * @return true on success
+     */
+    function removeAllByPrefix ($s)
+    {
+        // not implemented for current cache
+        return false;
+    }
+}
+
+/** @} */

+ 127 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCacheUtilities.php

@@ -0,0 +1,127 @@
+<?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
+ * @{
+ */
+
+// TODO: check if it is used or maybe usefull for future use, also it needs some refactoring
+
+class BxDolCacheUtilities extends BxDol
+{
+    public function __construct ()
+    {
+        parent::__construct();
+    }
+
+	/**
+     * Get singleton instance of the class
+     */
+    public static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolCacheUtilities();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    function clear($sCache)
+    {
+        return $this->_action($sCache, 'clear');
+    }
+
+    function size($sCache, $isFormatted = false)
+    {
+        $iSize = $this->_action($sCache, 'size');
+        return $isFormatted ? sprintf("%.2f", $iSize / 1024 / 1024) : $iSize;
+    }
+
+    protected function _action($sCache, $sMode = 'clear')
+    {
+        $sFuncCacheObject = $sMode == 'clear' ? '_clearObject' : '_getSizeObject';
+        $sFuncCacheFile = $sMode == 'clear' ? '_clearFile' : '_getSizeFile';
+
+        $mixedResult = false;
+        switch ($sCache) {
+            case 'db':
+            	if(getParam('sys_db_cache_enable') != 'on')
+					break;
+
+				$oCacheDb = BxDolDb::getInstance()->getDbCacheObject();
+                $mixedResult = $this->$sFuncCacheObject($oCacheDb, 'db_');
+                break;
+
+            case 'template':
+				if(getParam('sys_template_cache_enable') != 'on') 
+					break;
+
+				$oCacheTemplates = BxDolTemplate::getInstance()->getTemplatesCacheObject();
+                $mixedResult = $this->$sFuncCacheObject($oCacheTemplates, BxDolStudioTemplate::getInstance()->getCacheFilePrefix($sCache));
+                break;
+
+            case 'css':
+            	if(getParam('sys_template_cache_css_enable') != 'on')
+            		break;
+
+                $mixedResult = $this->$sFuncCacheFile(BxDolStudioTemplate::getInstance()->getCacheFilePrefix($sCache), BX_DIRECTORY_PATH_CACHE_PUBLIC);
+                break;
+
+            case 'js':
+            	if(getParam('sys_template_cache_js_enable') != 'on')
+            		break;
+
+                $mixedResult = $this->$sFuncCacheFile(BxDolStudioTemplate::getInstance()->getCacheFilePrefix($sCache), BX_DIRECTORY_PATH_CACHE_PUBLIC);
+                break;
+        }
+
+        return $mixedResult;
+    }
+
+    protected function _clearObject($oCache, $sPrefix)
+    {
+        if (!$oCache->removeAllByPrefix ($sPrefix))
+            return array('code' => 1, 'message' => _t('_adm_dbd_err_c_clean_failed'));
+        else
+            return array('code' => 0, 'message' => _t('_adm_dbd_msg_c_clean_success'));
+    }
+
+    protected function _clearFile($sPrefix, $sPath)
+    {
+        if (!($rHandler = opendir($sPath)))
+            return array('code' => 1, 'message' => _t('_adm_dbd_err_c_clean_failed'));
+
+        $l = strlen($sPrefix);
+        while (($sFile = readdir($rHandler)) !== false)
+            if (0 === strncmp($sFile, $sPrefix, $l))
+                @unlink($sPath . $sFile);
+
+        closedir($rHandler);
+
+        return array('code' => 0, 'message' => _t('_adm_dbd_msg_c_clean_success'));
+    }
+
+    protected function _getSizeObject($oCache, $sPrefix)
+    {
+        return $oCache->getSizeByPrefix ($sPrefix);
+    }
+
+    protected function _getSizeFile($sPrefix, $sPath)
+    {
+        if (!($rHandler = opendir($sPath)))
+            return 0;
+
+        $iSize = 0;
+        $l = strlen($sPrefix);
+        while (($sFile = readdir($rHandler)) !== false)
+            if (0 === strncmp($sFile, $sPrefix, $l))
+                $iSize += filesize($sPath . $sFile);
+
+        closedir($rHandler);
+
+        return $iSize;
+    }
+}
+
+/** @} */

+ 85 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCacheXCache.php

@@ -0,0 +1,85 @@
+<?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 BxDolCacheXCache extends BxDolCache
+{
+    protected $iTTL = 3600;
+
+    /**
+     * Get data from shared memory cache
+     *
+     * @param  string $sKey - file name
+     * @param  int    $iTTL - time to live
+     * @return the    data is got from cache.
+     */
+    function getData($sKey, $iTTL = false)
+    {
+        if (!xcache_isset($sKey))
+            return null;
+
+        return xcache_get($sKey);
+    }
+
+    /**
+     * Save data in shared memory cache
+     *
+     * @param  string  $sKey      - file name
+     * @param  mixed   $mixedData - the data to be cached in the file
+     * @param  int     $iTTL      - time to live
+     * @return boolean result of operation.
+     */
+    function setData($sKey, $mixedData, $iTTL = false)
+    {
+        $bResult = xcache_set($sKey, $mixedData, false === $iTTL ? $this->iTTL : $iTTL);
+        return $bResult;
+    }
+
+    /**
+     * Delete cache from shared memory
+     *
+     * @param  string $sKey - file name
+     * @return result of the operation
+     */
+    function delData($sKey)
+    {
+        if (!xcache_isset($sKey))
+            return true;
+
+        return xcache_unset($sKey);
+    }
+
+    /**
+     * Check if xcache functions are available
+     * @return boolean
+     */
+    function isAvailable()
+    {
+        return extension_loaded('xcache');
+    }
+
+    /**
+     * Check if xcache extension is loaded
+     * @return boolean
+     */
+    function isInstalled()
+    {
+        return extension_loaded('xcache');
+    }
+
+    /**
+     * remove all data from cache by key prefix
+     * @return true on success
+     */
+    function removeAllByPrefix ($s)
+    {
+        return xcache_unset_by_prefix ($s);
+    }
+}
+
+/** @} */

+ 17 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCalendar.php

@@ -0,0 +1,17 @@
+<?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 BxDolCalendar extends BxDol
+{
+    public function __construct ()
+    {
+    }
+}
+
+/** @} */

+ 134 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCaptcha.php

@@ -0,0 +1,134 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * @page objects
+ * @section captcha CAPTCHA
+ * @ref BxDolCaptcha
+ */
+
+/**
+ * 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
+    }
+}
+
+/** @} */

+ 37 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCaptchaQuery.php

@@ -0,0 +1,37 @@
+<?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 captcha objects.
+ * @see BxDolCaptcha
+ */
+class BxDolCaptchaQuery extends BxDolDb
+{
+    protected $_aObject;
+
+    public function __construct($aObject)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+    }
+
+    static public function getCaptchaObject ($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_captcha` WHERE `object` = ?", $sObject);
+        $aObject = $oDb->getRow($sQuery);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        return $aObject;
+    }
+
+}
+
+/** @} */

+ 125 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCategory.php

@@ -0,0 +1,125 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * @page objects
+ * @section category Category
+ * @ref BxDolCategory
+ */
+
+/**
+ * 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);
+    }
+}
+
+/** @} */

+ 54 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCategoryQuery.php

@@ -0,0 +1,54 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Database queries for category objects.
+ * @see BxDolCategory
+ */
+class BxDolCategoryQuery extends BxDolDb
+{
+    protected $_aObject;
+
+    public function __construct($aObject)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+    }
+
+    static public function getCategoryObject ($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_category` WHERE `object` = ?", $sObject);
+        $aObject = $oDb->getRow($sQuery);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        return $aObject;
+    }
+
+    static public function getCategoryObjectByFormAndList ($sObjectForm, $sListName)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_category` WHERE `form_object` = ? AND `list_name` = ?", $sObjectForm, $sListName);
+        $aObject = $oDb->getRow($sQuery);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        return $aObject;
+    }
+
+    static public function getItemsNumInCategory ($aObject, $sCategoryValue)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT COUNT(*) FROM `" . $aObject['table'] . "` " . $aObject['join'] . " WHERE `" . $aObject['field'] . "` = ? " . $aObject['where'], $sCategoryValue);
+        return $oDb->getOne($sQuery);
+    }
+}
+
+/** @} */

+ 1108 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCmts.php

@@ -0,0 +1,1108 @@
+<?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);
+
+/**
+ * @page objects
+ * @section comments Comments
+ * @ref BxDolCmts
+ */
+
+/**
+ * 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
+{
+    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`,
+                    `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)
+    {
+        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($aCmt['cmt_author_id'] == $this->_getAuthorId() && $this->checkAction ('comments edit own', $isPerformAction))
+            return true;
+
+        return $this->checkAction('comments edit all', $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));
+    }
+
+    public function actionGetFormEdit ()
+    {
+        if (!$this->isEnabled()){
+            echoJson(array());
+            return;
+        }
+
+        $iCmtId = bx_process_input(bx_get('Cmt'), BX_DATA_INT);
+        echoJson($this->getFormEdit($iCmtId));
+    }
+
+    public function actionGetCmt ()
+    {
+        if (!$this->isEnabled())
+            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 '';
+
+        $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));
+    }
+
+    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));
+    }
+
+    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;
+    }
+
+    /**
+     * 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)
+    {
+    	$aBp['vparent_id'] = isset($_REQUEST['CmtParent'])? bx_process_input($_REQUEST['CmtParent'], BX_DATA_INT) : 0;
+    	$aBp['type'] = isset($_REQUEST['CmtBrowse']) ? bx_process_input($_REQUEST['CmtBrowse'], BX_DATA_TEXT) : '';
+    	$aBp['filter'] = isset($_REQUEST['CmtFilter']) ? bx_process_input($_REQUEST['CmtFilter'], BX_DATA_TEXT) : '';
+        $aBp['start'] = isset($_REQUEST['CmtStart']) ? bx_process_input($_REQUEST['CmtStart'], BX_DATA_INT) : -1;
+        $aBp['per_view'] = isset($_REQUEST['CmtPerView']) ? bx_process_input($_REQUEST['CmtPerView'], BX_DATA_INT) : -1;
+
+        $aDp['type'] = isset($_REQUEST['CmtDisplay']) ? bx_process_input($_REQUEST['CmtDisplay'], BX_DATA_TEXT) : '';
+        $aDp['blink'] = isset($_REQUEST['CmtBlink']) ? bx_process_input($_REQUEST['CmtBlink'], BX_DATA_TEXT) : '';
+    }
+
+    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']);
+    }
+}
+
+/** @} */

+ 440 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCmtsQuery.php

@@ -0,0 +1,440 @@
+<?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 '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 = "ORDER BY `{$this->_sTable}`.`cmt_time` DESC";
+                $sLimitClause = "";
+                if(isset($aParams['per_page']))
+                	$sLimitClause = $this->prepareAsString("LIMIT ?, ?", $aParams['start'], $aParams['per_page']);
+
+                break;
+        }
+
+        $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` 
+        	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;
+    }
+}
+
+/** @} */

+ 708 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolConnection.php

@@ -0,0 +1,708 @@
+<?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');
+
+/**
+ * @page objects
+ * @section connection Connection
+ * @ref BxDolConnection
+ */
+
+/**
+ * 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.
+     * @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.
+     * @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, getLoggedId(), 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, getLoggedId(), 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);
+    }
+
+    /**
+     * 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 $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 ($sContentTable, $sContentField, $iContent, $isMutual = false)
+    {
+        return $this->_oQuery->getConnectedInitiatorsSQLParts($sContentTable, $sContentField, $iContent, $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');
+    }
+}
+
+/** @} */

+ 235 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolConnectionQuery.php

@@ -0,0 +1,235 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * 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)
+    {
+        $sWhere = $this->prepareAsString(" AND `c`.`initiator` = ?", $iInitiator);
+        if (false !== $isMutual)
+            $sWhere .= $this->prepareAsString(" AND `c`.`mutual` = ?", $isMutual);
+        return array(
+            'join' => "INNER JOIN `{$this->_sTable}` AS `c` ON (`c`.`content` = `$sContentTable`.`$sContentField` $sWhere)",
+        );
+    }
+
+    public function getConnectedInitiatorsSQLParts ($sContentTable, $sContentField, $iInitiator, $isMutual = false)
+    {
+        $sWhere = $this->prepareAsString(" AND `c`.`content` = ?", $iInitiator);
+        if (false !== $isMutual)
+            $sWhere .= $this->prepareAsString(" AND `c`.`mutual` = ?", $isMutual);
+        return array(
+            'join' => "INNER JOIN `{$this->_sTable}` AS `c` ON (`c`.`initiator` = `$sContentTable`.`$sContentField` $sWhere)",
+        );
+    }
+
+    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);
+    }
+
+}
+
+/** @} */

+ 160 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCover.php

@@ -0,0 +1,160 @@
+<?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
+ * @{
+ */
+
+/**
+ * @page objects
+ * @section cover Cover
+ * @ref BxDolCover
+ */
+
+/**
+ * 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;
+    }
+}
+
+/** @} */

+ 92 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCron.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    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * BxDolCron is parent class for all cron jobs, except the cases when the code is evaluated directly.
+ *
+ * periodic/cron.php file is used for cron jobs. It is started every minute and run cron jobs.
+ * The file runs jobs at regular intervals, the jobs are listed in `sys_cron_jobs` table.
+ *
+ * Fields shark_cron_jobs table:
+ *  id - key for the table
+ *  name - job name to be executed
+ *  time - format of entries are five fields of numbers specifying the minute,
+ *              hour, day of the month, month and day of the week that a task must be executed.
+ * @code
+ *              * * * * *
+ *              | | | | |
+ *              | | | | +--- day of week(0-6 with 0=Sunday)
+ *              | | | +----- month(1-12)
+ *              | | +------- day of month(1-31)
+ *              | +--------- hour(0-23)
+ *              +----------- minute(0-59)
+ * @endcode
+ *
+ *  class - class name which will run
+ *  file - path to class file
+ *  service_call - serialized service call which will be run
+ *
+ * The time numbers can be given as a comma separated list of simple numbers,
+ * ranges("2-5" is the same as "2,3,4,5"). A single "*" can be used in a field to indicate all
+ * valid numbers in that field, so it translates to "always". If a given time is valid in all five
+ * fields then a module function is executed. Here are a few examples that illustrate the possibilities:
+ *
+ * @code
+ *  will run at 16:10:
+ *  10 16
+ *  will run at 2:00 on saturday:
+ *  0 2 * * 6
+ *  will run at midnight on new years:
+ *  0 0 1 1 0
+ *  will run every 15 minutes:
+ *  *\/15
+ *  will run at 22:00 on work weekdays:
+ *  0 22 * * 1-5
+ *  will run each 23 minutes, 2:00, 4:00 ..., everyday
+ *  23 0-23/2
+ * @endcode
+ *
+ * Example add new cron job:
+ *
+ * 1. Create new class inherited from "BxDolCron" and add method "processing"
+ *
+ * @code
+ *      class BxDolCronMy extends BxDolCron {
+ *
+ *          function processing()
+ *          {
+ *              // insert code
+ *          }
+ *      }
+ * @endcode
+ *
+ * 2. Add record in `sys_cron_jobs` table
+ *
+ * @see an example of BxDolCronPruning, BxDolCronStorage.
+ *
+ *
+ * Memberships/ACL:
+ * Doesn't depend on user's membership.
+ *
+ *
+ * Alerts:
+ * no alerts available
+ *
+ */
+abstract class BxDolCron extends BxDol
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    abstract public function processing();
+}
+
+/** @} */

+ 80 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCronAccount.php

@@ -0,0 +1,80 @@
+<?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 function start()
+    {
+        set_time_limit(0);
+        ignore_user_abort();
+        ob_start();
+    }
+
+    protected function finish()
+    {
+        if (!($sOutput = ob_get_clean()))
+            return;
+
+		if(getParam('enable_notification_account') != 'on')
+			return;
+
+        $aTemplate = BxDolEmailTemplates::getInstance()->parseTemplate('t_Account', array(
+        	'site_title' => getParam('site_title'),
+        	'account_output' => $sOutput
+        ));
+        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));
+
+        $sAccounts = "";
+        foreach($aAccounts as $aAccount) {
+        	$oProfile = BxDolProfile::getInstance($aAccount['profile_id']);
+        	$sAccounts .= _t('_sys_notification_account_link', $oProfile->getUrl(), $oProfile->getDisplayName(), $aAccount['email']);
+        }
+
+        echo _t('_sys_notification_account', $sAccounts);
+    }
+
+    public function processing()
+    {
+        $this->start();
+
+        $this->processNewlyJoined();
+
+        $this->finish();
+    }
+}
+
+/** @} */

+ 55 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCronAcl.php

@@ -0,0 +1,55 @@
+<?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 BxDolCronAcl extends BxDolCron
+{
+    public function processing()
+    {
+    	$iExpireLetters = 0;
+        $iExpireNotificationDays = (int)getParam("sys_acl_expire_notification_days");
+        $bExpireNotifyOnce = getParam("sys_acl_expire_notify_once") == 'on';
+
+        $oAcl = BxDolAcl::getInstance();
+
+        $aIds = BxDolDb::getInstance()->getColumn("SELECT `id` FROM `sys_profiles` WHERE `type`<>'system'");
+        foreach($aIds as $iId) {
+            $aMemCur = $oAcl->getMemberMembershipInfo($iId);
+
+            // If expire_notification_days is -1 then notify after expiration
+            if($aMemCur['id'] == MEMBERSHIP_ID_STANDARD && $iExpireNotificationDays == -1 ) {
+            	$iDatePrev = time() - 24 * 3600;
+                $aMemPrev = $oAcl->getMemberMembershipInfo($iId, $iDatePrev); // Get previous membership level
+                if($aMemCur['id'] != $aMemPrev['id']) {
+                    $bMailResult = $oAcl->getExpirationLetter($iId, $aMemPrev['name'], -1);
+                    if($bMailResult)
+                        $iExpireLetters++;
+                }
+
+                continue;
+            }
+
+            // If memberhip is not standard then check if it will change
+            if($aMemCur['id'] != MEMBERSHIP_ID_STANDARD) {
+                $iDateNext = time() + $iExpireNotificationDays * 24 * 3600;
+                $aMemNext = $oAcl->getMemberMembershipInfo($iId, $iDateNext);
+                if($aMemCur['id'] != $aMemNext['id'] && $aMemNext['id'] == MEMBERSHIP_ID_STANDARD) {
+                    if(!$bExpireNotifyOnce || abs($iDateNext - $aMemCur['date_expires']) < 24 * 3600) {
+                        $bMailResult = $oAcl->getExpirationLetter($iId, $aMemCur['name'], (int)(($aMemCur['date_expires'] - time())/(24 * 3600)));
+                        if($bMailResult)
+                            $iExpireLetters++;
+                    }
+                }
+            }
+
+            continue;
+        }
+    }
+}
+
+/** @} */

+ 121 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCronPruning.php

@@ -0,0 +1,121 @@
+<?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 BxDolCronPruning extends BxDolCron
+{
+    protected function start()
+    {
+        set_time_limit(0);
+        ignore_user_abort();
+        ob_start();
+    }
+
+    protected function finish()
+    {
+        bx_alert('system', 'pruning', 0);
+
+        if (!($sOutput = ob_get_clean()))
+            return;
+
+		if(getParam('enable_notification_pruning') != 'on')
+			return;
+
+        $aTemplate = BxDolEmailTemplates::getInstance()->parseTemplate('t_Pruning', array('pruning_output' => $sOutput, 'site_title' => getParam('site_title')), 0, 0);
+        if ($aTemplate)
+            sendMail(getParam('site_email'), $aTemplate['Subject'], $aTemplate['Body'], 0, array(), BX_EMAIL_NOTIFY);
+    }
+
+    /**
+     * Clean database by deleting some expired data
+     */
+    protected function cleanDatabase()
+    {
+        // clean expired membership levels
+        $oAcl = BxDolAcl::getInstance();
+        $iDeleteMemLevels = $oAcl ? $oAcl->maintenance() : 0;
+
+        //--- Clean sessions ---//
+        $oSession = BxDolSession::getInstance();
+        $iSessions = $oSession ? $oSession->maintenance() : 0;
+
+        // clean old views
+        $iDeletedViews = BxDolView::maintenance ();
+
+        // clean storage engine expired private file tokens
+        $iDeletedExpiredTokens = BxDolStorage::pruning();
+
+        // clean outdated transcoded images
+        $iDeletedTranscodedImages = BxDolTranscoderImage::pruning();
+
+        // clean expired keys
+        $oKey = BxDolKey::getInstance();
+        $iDeletedKeys = $oKey ? $oKey->prune() : 0;
+
+        echo _t('_sys_pruning_db', $iDeleteMemLevels, $iSessions, $iDeletedViews, $iDeletedKeys, $iDeletedExpiredTokens, $iDeletedTranscodedImages);
+    }
+
+    /**
+     * Clean tmp folders (tmp, cache) by deleting old files (by default older than 1 month)
+     */
+    protected function cleanTmpFolders()
+    {
+        $iTmpFileLife = 2592000;  // one month
+        $aDirsToClean = array(
+            BX_DIRECTORY_PATH_TMP,
+            BX_DIRECTORY_PATH_CACHE,
+            BX_DIRECTORY_PATH_CACHE_PUBLIC,
+        );
+
+        $iNumTmp = 0;
+        $iNumDel = 0;
+
+        foreach ($aDirsToClean as $sDir) {
+
+            if (!($h = opendir($sDir)))
+                continue;
+
+            while ($sFile = readdir($h)) {
+
+                if ('.' == $sFile || '..' == $sFile || '.' == $sFile[0])
+                    continue;
+
+                ++$iNumTmp;
+
+                $iDiff = time() - filemtime($sDir . $sFile);
+
+                if ($iDiff < $iTmpFileLife)
+                    continue;
+
+                if (is_file($sDir . $sFile))
+                    @unlink ($sDir . $sFile);
+                else
+                    @bx_rrmdir($sDir . $sFile);
+
+                ++$iNumDel;
+            }
+
+            closedir($h);
+        }
+
+        echo _t('_sys_pruning_files', $iNumTmp, $iNumDel);
+    }
+
+    public function processing()
+    {
+        $this->start();
+
+        $this->cleanTmpFolders();
+
+        $this->cleanDatabase();
+
+        $this->finish();
+    }
+}
+
+/** @} */

+ 78 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCronQuery.php

@@ -0,0 +1,78 @@
+<?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 Cron
+ * @see BxDolCron
+ */
+class BxDolCronQuery extends BxDolDb implements iBxDolSingleton
+{
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolCronQuery();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    public function getJobs()
+    {
+    	return $this->fromCache('sys_cron_jobs', 'getAll', "SELECT * FROM `sys_cron_jobs`");
+    }
+
+    public function addTransientJobClass($sName, $sClass, $sFile)
+    {
+    	$sQuery = $this->prepare("INSERT INTO `sys_cron_jobs` SET `name`=?, `time`='transient', `class`=?, `file`=?", $sName, $sClass, $sFile);
+        return (int)$this->query($sQuery) > 0;
+    }
+
+	public function addTransientJobService($sName, $mixedService)
+    {
+    	if(is_array($mixedService))
+    		$mixedService = call_user_func_array('BxDolService::getSerializedService', $mixedService);
+
+    	if(!BxDolService::isSerializedService($mixedService))
+    		return false;
+
+    	$sQuery = $this->prepare("INSERT INTO `sys_cron_jobs` SET `name`=?, `time`='transient', `service_call`=?", $sName, $mixedService);
+        return (int)$this->query($sQuery) > 0;
+    }
+
+    public function getTransientJobs()
+    {
+    	return $this->getAllWithKey("SELECT * FROM `sys_cron_jobs` WHERE `time`='transient'", 'name');
+    }
+
+    public function deleteTransientJobs()
+    {
+    	return $this->query("DELETE FROM `sys_cron_jobs` WHERE `time`='transient'");
+    }
+}
+
+/** @} */

+ 22 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCronStorage.php

@@ -0,0 +1,22 @@
+<?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 BxDolCronStorage extends BxDolCron
+{
+    public function processing()
+    {
+        set_time_limit(36000);
+        ignore_user_abort();
+
+        if (BxDolStorage::pruneDeletions()) // if any files were deleted
+            BxDolInstallerUtils::checkModulesPendingUninstall(); // try to uninstall modules pending for uninstall
+    }
+}
+
+/** @} */

+ 25 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCronTranscoder.php

@@ -0,0 +1,25 @@
+<?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 BxDolCronTranscoder extends BxDolCron
+{
+    public function processing()
+    {
+        set_time_limit(10800);
+        ignore_user_abort();
+
+        if (!defined('BX_TRANSCODER_NO_TRANSCODING'))
+            BxDolTranscoder::processQueue();
+
+        if (defined('BX_TRANSCODER_PROCESS_COMPLETED'))
+            BxDolTranscoder::processCompleted(); 
+    }
+}
+
+/** @} */

+ 27 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCronUpgradeCheck.php

@@ -0,0 +1,27 @@
+<?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 BxDolCronUpgradeCheck extends BxDolCron
+{
+    public function processing()
+    {
+        if ('on' != getParam('sys_autoupdate_system'))
+            return;
+
+        $o = bx_instance('BxDolUpgrader');
+        if (!$o->prepare()) {
+            sendMailTemplateSystem('t_UpgradeFailed', array (
+                'error_msg' => $o->getError(),
+            ));
+            setParam('sys_autoupdate_system', ''); // disable auto-update if it is failed
+        }
+    }
+}
+
+/** @} */

+ 21 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolCronUpgradeModulesCheck.php

@@ -0,0 +1,21 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolCronUpgradeModulesCheck extends BxDolCron
+{
+    public function processing()
+    {
+        if('on' != getParam('sys_autoupdate_modules'))
+            return;
+
+		BxDolStudioInstallerUtils::getInstance()->performModulesUpgrade();
+    }
+}
+
+/** @} */

+ 1115 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolDb.php

@@ -0,0 +1,1115 @@
+<?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 = ''");
+			$this->pdoExec("SET storage_engine=" . $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);
+    }
+
+	/**
+     * execute sql query and return table of records as result
+     */
+    public function getAllWithKey($oStatement, $sFieldKey, $aBindings = array())
+    {
+    	$aResult = array();
+        if(!$oStatement)
+            return $aResult;
+		else if(!($oStatement instanceof PDOStatement) && is_string($oStatement))
+			$oStatement = $this->prepare($oStatement);
+
+        $aRow = $this->getFirstRow($oStatement, $aBindings, PDO::FETCH_ASSOC);
+        while(!empty($aRow)) {
+        	$aResult[$aRow[$sFieldKey]] = $aRow;
+
+        	$aRow = $this->getNextRow(PDO::FETCH_ASSOC);
+        }
+
+        return $aResult;
+    }
+
+    /**
+     * execute sql query and return table of records as result
+     */
+    public function getPairs($oStatement, $sFieldKey, $sFieldValue, $aBindings = array())
+    {
+    	$aResult = array();
+        if(!$oStatement)
+            return $aResult;
+		else if(!($oStatement instanceof PDOStatement) && is_string($oStatement))
+			$oStatement = $this->prepare($oStatement);
+
+        $aRow = $this->getFirstRow($oStatement, $aBindings, PDO::FETCH_ASSOC);
+        while(!empty($aRow)) {
+        	$aResult[$aRow[$sFieldKey]] = $aRow[$sFieldValue];
+
+        	$aRow = $this->getNextRow(PDO::FETCH_ASSOC);
+        }
+
+        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($bResult);
+
+		//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 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) {
+            $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)
+				$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);
+}
+
+/** @} */

+ 93 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolDesigns.php

@@ -0,0 +1,93 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolDesigns extends BxDolFactory implements iBxDolSingleton
+{
+    protected $sDesign;
+    protected $oDesign;
+
+    protected $aParams;
+
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__])) {
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolDesigns();
+            $GLOBALS['bxDolClasses'][__CLASS__]->init();
+        }
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    protected function init()
+    {
+    	$this->sDesign = BxDolTemplate::getInstance()->getCode();
+
+    	$aDesign = BxDolModuleQuery::getInstance()->getModuleByUri($this->sDesign);
+    	if(!empty($aDesign) && is_array($aDesign))
+    		$this->oDesign = BxDolModule::getInstance($aDesign['name']);
+
+    	//--- Init site's logo params.
+    	if($this->oDesign instanceof BxDolModule && method_exists($this->oDesign->_oConfig, 'getLogoParams'))
+    		list(
+    			$this->aParams['logo'], 
+    			$this->aParams['logo_alt'], 
+    			$this->aParams['logo_width'], 
+    			$this->aParams['logo_height']
+    		) = $this->oDesign->_oConfig->getLogoParams();
+    }
+
+    public function getSiteLogo()
+    {
+    	return $this->getSiteLogoParam('logo');
+    }
+	public function getSiteLogoAlt()
+    {
+    	return $this->getSiteLogoParam('logo_alt');
+    }
+	public function getSiteLogoWidth()
+    {
+    	return $this->getSiteLogoParam('logo_width');
+    }
+	public function getSiteLogoHeight()
+    {
+    	return $this->getSiteLogoParam('logo_height');
+    }
+    protected function getSiteLogoParam($sName)
+    {
+    	if(!empty($this->aParams[$sName])) {
+    		$sResult = getParam($this->aParams[$sName]);
+    		if(!empty($sResult))
+    			return $sResult;
+    	}
+
+    	return getParam('sys_site_' . $sName);
+    }
+}
+
+/** @} */

+ 162 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolEditor.php

@@ -0,0 +1,162 @@
+<?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);
+
+/**
+ * @page objects
+ * @section editor Editor
+ * @ref BxDolEditor
+ */
+
+/**
+ * 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);
+    }
+
+}
+
+/** @} */

+ 37 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolEditorQuery.php

@@ -0,0 +1,37 @@
+<?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 editor objects.
+ * @see BxDolEditor
+ */
+class BxDolEditorQuery extends BxDolDb
+{
+    protected $_aObject;
+
+    public function __construct($aObject)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+    }
+
+    static public function getEditorObject ($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_editor` WHERE `object` = ?", $sObject);
+        $aObject = $oDb->getRow($sQuery);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        return $aObject;
+    }
+
+}
+
+/** @} */

+ 194 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolEmailTemplates.php

@@ -0,0 +1,194 @@
+<?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');
+
+/**
+ * @page objects
+ * @section email_templates Email Templates
+ * @ref BxDolEmailTemplates
+ */
+
+/**
+ * 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);
+    }
+}
+
+/** @} */

+ 68 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolEmailTemplatesQuery.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
+ * @{
+ */
+
+/**
+ * Database queries for BxDolEmailTemplates object.
+ * @see BxDolEmailTemplates
+ */
+class BxDolEmailTemplatesQuery extends BxDolDb implements iBxDolSingleton
+{
+    public static function getInstance()
+    {
+        if (!isset($GLOBALS['bxDolClasses'][__CLASS__])) {
+            $o = new BxDolEmailTemplatesQuery();
+            $GLOBALS['bxDolClasses'][__CLASS__] = $o;
+        }
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    public function getTemplate ($sTemplateName, $iLangAccount, $iLangDefault, $iLangFallback)
+    {
+        // get email templates keys
+        $sSql = $this->prepare("SELECT `Subject`, `Body` FROM `sys_email_templates` WHERE `Name`=? LIMIT 1", $sTemplateName);
+        $a = $this->getRow($sSql);
+        if (!$a)
+            return false;
+
+        // build languages idsto search for translation in
+        $aLangs = array ($iLangAccount);
+        if ($iLangDefault != $iLangAccount)
+            $aLangs[] = $iLangDefault;
+        if ($iLangFallback != $iLangAccount && $iLangFallback != $iLangDefault)
+            $aLangs[] = $iLangFallback;
+
+        // get translation for subject
+        $sSubject = false;
+        foreach ($aLangs as $iLang)
+            if ($sSubject = $this->getLangTranslation($a['Subject'], $iLang))
+                break;
+        if (!$sSubject)
+            return false;
+
+        // get translation for body
+        $sBody = false;
+        foreach ($aLangs as $iLang)
+            if ($sBody = $this->getLangTranslation($a['Body'], $iLang))
+                break;
+        if (!$sBody)
+            return false;
+
+        // return result
+        return array ('Subject' => $sSubject, 'Body' => $sBody);
+    }
+
+    public function getLangTranslation ($sKey, $iLangId)
+    {
+        $sSql = $this->prepare("SELECT `s`.`String` FROM `sys_localization_strings` as `s` INNER JOIN `sys_localization_keys` AS `k` ON (`s`.`IDKey` = `k`.`ID`) WHERE `k`.`Key` = ? AND `s`.`IDLanguage` = ? LIMIT 1", $sKey, $iLangId);
+        return $this->getOne($sSql);
+    }
+}
+
+/** @} */

+ 18 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolFactory.php

@@ -0,0 +1,18 @@
+<?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
+ * @{
+ */
+
+/**
+ * Base class for all classes with protected constructor
+ */
+class BxDolFactory
+{
+    protected function __construct () {}
+}
+
+/** @} */

+ 242 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolFavorite.php

@@ -0,0 +1,242 @@
+<?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);
+
+/**
+ * @page objects
+ * @section favorites Favorites
+ * @ref BxDolFavorite
+ */
+
+/**
+ * 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->_oQuery->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'));
+
+        $iId = (int)$this->_oQuery->{($bPerformUndo ? 'un' : '') . 'doFavorite'}($iObjectId, $iAuthorId);
+        if($iId == 0)
+            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_id' => $iId, 'favorite_author_id' => $iAuthorId, 'object_author_id' => $iObjectAuthorId));
+        bx_alert('favorite', ($bPerformUndo ? 'un' : '') . 'do', $iId, $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, 
+        	'id' => $iId,
+        	'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() ?  'star-o' : 'star';
+    }
+
+    protected function _getTitleDoFavorite($bPerformed)
+    {
+    	return $bPerformed && $this->isUndo() ? '_favorite_do_unfavorite' : '_favorite_do_favorite';
+    }
+}
+
+/** @} */

+ 60 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolFavoriteQuery.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 BxDolFavorite
+ */
+class BxDolFavoriteQuery extends BxDolObjectQuery
+{
+    public function __construct(&$oModule)
+    {
+        parent::__construct($oModule);
+
+        $this->_sMethodGetEntry = 'getFavorite';
+    }
+
+    public function isPerformed($iObjectId, $iAuthorId)
+    {
+        $sQuery = $this->prepare("SELECT `date` FROM `{$this->_sTableTrack}` WHERE `object_id` = ? AND `author_id` = ? LIMIT 1", $iObjectId, $iAuthorId);
+        return (int)$this->getOne($sQuery) > 0;
+    }
+
+	public function getPerformedBy($iObjectId)
+    {
+        $sQuery = $this->prepare("SELECT `author_id` FROM `{$this->_sTableTrack}` WHERE `object_id`=? ORDER BY `date` DESC", $iObjectId);
+        return $this->getAll($sQuery);
+    }
+
+	public function getFavorite($iObjectId)
+    {
+        return array('count' => $this->getObjectCount($iObjectId));
+    }
+
+    public function doFavorite($iObjectId, $iAuthorId)
+    {
+        $sQuery = $this->prepare("SELECT `date` FROM `{$this->_sTableTrack}` WHERE `object_id` = ? AND `author_id` = ? LIMIT 1", $iObjectId, $iAuthorId);
+        if((int)$this->getOne($sQuery) > 0)
+        	return true;
+
+        $sQuery = $this->prepare("INSERT IGNORE INTO `{$this->_sTableTrack}` SET `object_id` = ?, `author_id` = ?, `date` = ?", $iObjectId, $iAuthorId, time());
+        return (int)$this->query($sQuery) > 0;
+    }
+
+	public function undoFavorite($iObjectId, $iAuthorId)
+    {
+        $sQuery = $this->prepare("SELECT `date` FROM `{$this->_sTableTrack}` WHERE `object_id` = ? AND `author_id` = ? LIMIT 1", $iObjectId, $iAuthorId);
+        if((int)$this->getOne($sQuery) == 0)
+        	return true;
+
+        $sQuery = $this->prepare("DELETE FROM `{$this->_sTableTrack}` WHERE `object_id` = ? AND `author_id` = ? LIMIT 1", $iObjectId, $iAuthorId);
+        return (int)$this->query($sQuery) > 0;
+    }
+}
+
+/** @} */

+ 205 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolFile.php

@@ -0,0 +1,205 @@
+<?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 BxDolFile extends BxDolFactory implements iBxDolSingleton
+{
+    protected $_sPathFrom;
+    protected $_sPathTo;
+
+    protected function __construct()
+    {
+        parent::__construct();
+
+        $this->_sPathFrom = BX_DIRECTORY_PATH_ROOT;
+        $this->_sPathTo = BX_DIRECTORY_PATH_ROOT;
+    }
+
+    /**
+     * 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 BxDolFile();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    function copy($sFilePathFrom, $sFilePathTo)
+    {
+    	if(substr($sFilePathFrom, 0, strlen($this->_sPathFrom)) != $this->_sPathFrom)
+        	$sFilePathFrom = $this->_sPathFrom . $sFilePathFrom;
+
+        if(substr($sFilePathTo, 0, strlen($this->_sPathTo)) != $this->_sPathTo)
+        	$sFilePathTo = $this->_sPathTo . $sFilePathTo;
+
+        return $this->_copyFile($sFilePathFrom, $sFilePathTo);
+    }
+
+    function delete($sPath)
+    {
+        $sPath = $this->_sPathTo . $sPath;
+        return $this->_deleteDirectory($sPath);
+    }
+
+    function getPermissions($sPath)
+    {
+        $sPath = $this->_sPathTo . $sPath;
+        return $this->_getPermissions($sPath);
+    }
+
+    function setPermissions($sPath, $sMode)
+    {
+        $sPath = $this->_sPathTo . $sPath;
+        return $this->_setPermissions($sPath, $sMode);
+    }
+
+    protected function _copyFile($sFilePathFrom, $sFilePathTo)
+    {
+        if(substr($sFilePathFrom, -1) == '*')
+            $sFilePathFrom = substr($sFilePathFrom, 0, -1);
+
+        //--- Copy file to file/folder
+        if(is_file($sFilePathFrom)) {
+        	if(is_dir($sFilePathTo)) {
+                $aFileParts = $this->_parseFile($sFilePathFrom);
+				return @copy($sFilePathFrom, $this->_validatePath($sFilePathTo) . $aFileParts[1]);
+            }
+            else {
+                $aFileParts = $this->_parseFile($sFilePathTo);
+                if(isset($aFileParts[0]))
+                    $this->_mkDirR($aFileParts[0]);
+
+                return @copy($sFilePathFrom, $sFilePathTo);
+            }
+        }
+
+        //--- Copy directory to directory
+        if(is_dir($sFilePathFrom)) {
+        	$bFilePathTo = file_exists($sFilePathTo);
+        	if($bFilePathTo && !is_dir($sFilePathTo))
+        		return false;
+
+        	if(!$bFilePathTo && !$this->_mkDirR($sFilePathTo))
+            	return false;
+
+            $bResult = true;
+            $aInnerFiles = $this->_readDirectory($sFilePathFrom);
+            foreach($aInnerFiles as $sFile)
+                $bResult = $bResult && $this->_copyFile($this->_validatePath($sFilePathFrom, false) . $sFile, $this->_validatePath($sFilePathTo, false) . $sFile);
+
+			return $bResult;
+        }
+
+        return false;
+    }
+
+    protected function _readDirectory($sFilePath)
+    {
+        if(!is_dir($sFilePath) || !($rSource = opendir($sFilePath)))
+            return false;
+
+        $aResult = array();
+        while(($sFile = readdir($rSource)) !== false) {
+            if($sFile == '.' || $sFile =='..')
+                continue;
+            $aResult[] = $sFile;
+        }
+        closedir($rSource);
+
+        return $aResult;
+    }
+
+    protected function _deleteDirectory($sPath)
+    {
+        if(!file_exists($sPath))
+            return true;
+
+        if(is_dir($sPath)) {
+        	$sPath = $this->_validatePath($sPath, false);
+
+            $aFiles = $this->_readDirectory($sPath);
+            if(is_array($aFiles) && !empty($aFiles))
+                foreach($aFiles as $sFile)
+                    $this->_deleteDirectory($sPath . $sFile);
+
+            if(!@rmdir($sPath))
+                return false;
+        } else if(!@unlink($sPath))
+            return false;
+
+        return true;
+    }
+
+    protected function _validatePath($sPath, $bAbstract = true)
+    {
+        return $sPath . ($sPath && !$this->_isEndWithSlash($sPath) && ($bAbstract ? $this->_isDirectory($sPath) : is_dir($sPath)) ? '/' : '');
+    }
+
+    protected function _parseFile($sFilePath)
+    {
+        return array(dirname($sFilePath), basename($sFilePath));
+    }
+
+    protected function _isFile($sFilePath)
+    {
+    	if($this->_isEndWithSlash($sFilePath))
+    		return false;
+
+    	$aInfo = pathinfo($sFilePath);
+    	if(!isset($aInfo['extension']))
+    		return false;
+
+        return true;
+    }
+
+    protected function _isDirectory($sFilePath)
+    {
+        return !$this->_isFile($sFilePath);
+    }
+
+    protected function _isEndWithSlash($sFilePath)
+    {
+    	return in_array(substr($sFilePath, -1), array('/', '\\'));
+    }
+
+    protected function _getPermissions($sPath)
+    {
+        clearstatcache();
+
+        $hPerms = @fileperms($sPath);
+        if($hPerms == false)
+            return false;
+
+        return substr(decoct($hPerms), -3);
+    }
+
+    protected function _setPermissions($sPath, $sMode)
+    {
+        $aConvert = array('writable' => 0666, 'executable' => 0777);
+
+        return @chmod($sPath, $aConvert[$sMode]);
+    }
+
+	protected function _mkDirR($sPath)
+    {
+        return @mkdir($sPath, 0777, true);
+    }
+}
+
+/** @} */

+ 1629 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolForm.php

@@ -0,0 +1,1629 @@
+<?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
+
+/**
+ * @page objects
+ * @section forms Forms
+ * @ref BxDolForm
+ */
+
+/**
+ * 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);
+
+        // 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);
+
+            if (!$this->_initCheckerNestedForms ())
+                $this->_isValid = false;
+
+        }
+    }
+
+    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'));
+    }
+
+
+    // 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 $_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 enableFormCsrfChecking($bFormCsrfChecking)
+    {
+        $this->_bFormCsrfChecking = $bFormCsrfChecking;
+    }
+
+    // check function
+    function check (&$aInputs)
+    {
+        $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'] : '';
+
+            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)
+    {
+        $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;
+        }
+
+        // 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);
+        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;
+    }
+}
+
+/** @} */

+ 56 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolFormNested.php

@@ -0,0 +1,56 @@
+<?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
+ * @{
+ */
+
+/**
+ * Nested form. It is used in file uploaders to show uploaded file as nested form.
+ * @see BxDolUploader
+ * @see BxDolForm
+ */
+class BxDolFormNested extends BxTemplFormView
+{
+    /**
+     * Constructor
+     * @param $sName the name of form field from main form, where this nested form is inserted.
+     * @param $aForm form array, actually only form inout are needed, all other attributes are taken from parent form automatically.
+     * @param $sSubmitName main form submit_name; field name of submit form input to determine if form is submitted or not.
+     * @param $oTemplate optional template object
+     */
+    function __construct($sName, $aForm, $sSubmitName = false, $oTemplate = false)
+    {
+        if (!isset($aForm['params']['nested_form_template']) || !$aForm['params']['nested_form_template'])
+            $aForm['params']['nested_form_template'] = 'uploader_nested_form_wrapper.html';
+
+        $aForm['form_attrs']['id'] = $sName . '_{file_id}';
+        $aForm['form_attrs']['method'] = 'specific';
+        $aForm['params']['remove_form'] = true;
+        $aForm['params']['csrf']['disable'] = true;
+        if ($sSubmitName)
+            $aForm['params']['db']['submit_name'] = $sSubmitName;
+        if (!isset($aForm['inputs'][$sName])) {
+            $aForm['inputs'][$sName] = array(
+                'type' => 'hidden',
+                'name' => $sName . '[]',
+                'value' => '{file_id}',
+            );
+        }
+
+        parent::__construct($aForm, $oTemplate);
+    }
+
+    function genForm()
+    {
+        $sNestedForm = parent::genForm();
+        $a = array (
+            'nested_form' => $sNestedForm,
+        );
+        return $this->oTemplate->parseHtmlByName($this->aParams['nested_form_template'], $a);
+    }
+}
+
+/** @} */

+ 198 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolFormQuery.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    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' => _t($a['caption']),
+                'info' => $a['info'] ? _t($a['info']) : '',
+                'required' => $a['required'] ? 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'])) {
+                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;
+    }
+
+    public function getFormInputs()
+    {
+        $sQuery = $this->prepare("SELECT * FROM `sys_form_inputs` WHERE `object` = ? ORDER BY `order` ASC", $this->_aObject['object']);
+        return $this->getAll($sQuery);
+    }
+
+}
+
+/** @} */

+ 145 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolFtp.php

@@ -0,0 +1,145 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolFtp extends BxDolFile
+{
+    protected $_sHost;
+    protected $_sLogin;
+    protected $_sPassword;
+    protected $_rStream;
+
+    public function __construct($sHost, $sLogin, $sPassword, $sPath = '/')
+    {
+        parent::__construct();
+        $this->_sHost = $sHost;
+        $this->_sLogin = $sLogin;
+        $this->_sPassword = $sPassword;
+
+        $this->_sPathFrom = '';
+        $this->_sPathTo = $sPath . ('/' == substr($sPath, -1) ? '' : '/');
+    }
+
+    function connect()
+    {
+        $this->_rStream = ftp_connect($this->_sHost);
+        return @ftp_login($this->_rStream, $this->_sLogin, $this->_sPassword);
+    }
+
+    function isUna()
+    {
+        return @ftp_size($this->_rStream, $this->_sPathTo . 'inc/header.inc.php') > 0;
+    }
+
+    protected function _copyFile($sFilePathFrom, $sFilePathTo)
+    {
+        if(substr($sFilePathFrom, -1) == '*')
+            $sFilePathFrom = substr($sFilePathFrom, 0, -1);
+
+        $bResult = false;
+        if(is_file($sFilePathFrom)) {
+            if($this->_isFile($sFilePathTo)) {
+                $aFileParts = $this->_parseFile($sFilePathTo);
+                if(isset($aFileParts[0]))
+                    $this->_mkDirR($aFileParts[0]);
+
+                $bResult = @ftp_put($this->_rStream, $sFilePathTo, $sFilePathFrom, FTP_BINARY);
+            } else if($this->_isDirectory($sFilePathTo)) {
+                $this->_mkDirR($sFilePathTo);
+
+                $aFileParts = $this->_parseFile($sFilePathFrom);
+                if(isset($aFileParts[1]))
+                    $bResult = @ftp_put($this->_rStream, $this->_validatePath($sFilePathTo) . $aFileParts[1], $sFilePathFrom, FTP_BINARY);
+            }
+        } else if(is_dir($sFilePathFrom) && $this->_isDirectory($sFilePathTo)) {
+            $this->_mkDirR($sFilePathTo);
+
+            $aInnerFiles = $this->_readDirectory($sFilePathFrom);
+            foreach($aInnerFiles as $sFile)
+                $bResult = $this->_copyFile($this->_validatePath($sFilePathFrom) . $sFile, $this->_validatePath($sFilePathTo) . $sFile);
+        } else
+            $bResult = false;
+
+        return $bResult;
+    }
+
+    protected function _deleteDirectory($sPath)
+    {
+        if($this->_isDirectory($sPath)) {
+            if(substr($sPath, -1) != '/')
+                $sPath .= '/';
+
+            if(($aFiles = @ftp_nlist($this->_rStream, $sPath)) !== false)
+                foreach($aFiles as $sFile)
+                    if($sFile != '.' && $sFile != '..')
+                        $this->_deleteDirectory(false === strpos($sFile, '/') ? $sPath . $sFile : $sFile);
+
+                if(!@ftp_rmdir($this->_rStream, $sPath))
+                    return false;
+
+        } else if(!@ftp_delete($this->_rStream, $sPath))
+            return false;
+
+        return true;
+    }
+    protected function _parseFile($sFilePath)
+    {
+        $aParts = array();
+        preg_match("/^([a-zA-Z0-9@~_\.\\\\\/:-]+[\\\\\/])([a-zA-Z0-9~_-]+\.[a-zA-Z]{2,8})$/", $sFilePath, $aParts);
+        return count($aParts) > 1 ? array_slice($aParts, 1) : false;
+    }
+	protected function _isFile($sFilePath)
+    {
+        return preg_match("/^([a-zA-Z0-9@~_\.\\\\\/:-]+)\.([a-zA-Z]){2,8}$/", $sFilePath) ? true : false;
+    }
+    protected function _isDirectory($sFilePath)
+    {
+        return preg_match("/^([a-zA-Z0-9@~_\.\\\\\/:-]+)[\\\\\/]([a-zA-Z0-9~_-]+)[\\\\\/]?$/", $sFilePath) ? true : false;
+    }
+    protected function _setPermissions($sPath, $sMode)
+    {
+        $aConvert = array(
+            'writable' => $this->_isDirectory($sPath) ? 0777 : 0666,
+            'executable' => 0777
+        );
+
+        if(@ftp_chmod($this->_rStream, $aConvert[$sMode], $sPath) === false)
+            return false;
+
+        return true;
+    }
+    protected function _mkDirR($sPath)
+    {
+        $sPwd = ftp_pwd ($this->_rStream);
+        $aParts = explode("/", $sPath);
+        $sPathFull = '';
+        if ('/' == $sPath[0]) {
+            $sPathFull = '/';
+            ftp_chdir($this->_rStream, '/');
+        }
+        foreach ($aParts as $sPart) {
+            if (!$sPart)
+                continue;
+            $sPathFull .= $sPart;
+            if ('..' == $sPart) {
+                @ftp_cdup($this->_rStream);
+            } elseif (!@ftp_chdir($this->_rStream, $sPart)) {
+                if (!@ftp_mkdir($this->_rStream, $sPart)) {
+                    ftp_chdir($this->_rStream, $sPwd);
+                    return false;
+                }
+                @ftp_chdir($this->_rStream, $sPart);
+            }
+            $sPathFull .= '/';
+        }
+        ftp_chdir($this->_rStream, $sPwd);
+        return true;
+    }
+}
+
+/** @} */

+ 550 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolGrid.php

@@ -0,0 +1,550 @@
+<?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
+ * @{
+ */
+
+/**
+ * @page objects
+ * @section grid Grid
+ * @ref BxDolGrid
+ */
+
+/**
+ * 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 $_sDefaultSortingOrder = 'ASC';
+
+    /**
+     * Constructor
+     * @param $aOptions array of grid options
+     */
+    protected function __construct($aOptions)
+    {
+        parent::__construct();
+
+        $this->_sObject = $aOptions['object'];
+        $this->_aOptions = $aOptions;
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * 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 ";
+
+        // 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 ');
+            }
+
+            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']);
+    }
+}
+
+/** @} */

+ 172 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolGridConnections.php

@@ -0,0 +1,172 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+
+class BxDolGridConnections extends BxTemplGrid
+{
+    protected $_bOwner = false;
+    protected $_sContentModule = 'bx_persons';
+    protected $_sObjectConnections = 'sys_profiles_friends';
+    protected $_oConnection;
+
+    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->getConnectedInitiatorsAsSQLParts('p', 'id', $oProfile->id(), $this->_bOwner ? false : true);
+
+        $this->addMarkers(array(
+            'profile_id' => $oProfile->id(),
+            'join_connections' => $aSQLParts['join'],
+            'content_module' => $this->_sContentModule,
+        ));
+    }
+
+    /**
+     * 'accept' action handler
+     */
+    public function performActionAccept()
+    {
+        list ($iId, $iViewedId) = $this->_prepareIds();
+
+        if (!$iId) {
+            echoJson(array('msg' => _t('_sys_txt_error_occured')));
+            exit;
+        }
+
+        $a = $this->_oConnection->actionAdd($iId, $iViewedId);
+        if (isset($a['err']) && $a['err'])
+            echoJson(array('msg' => $a['msg']));
+        else
+            echoJson(array('grid' => $this->getCode(false), 'blink' => $iId));
+    }
+
+    /**
+     * 'add friend' action handler
+     */
+    public function performActionAddFriend()
+    {
+        return $this->performActionAccept();
+    }
+
+    protected function _delete ($mixedId)
+    {
+        list ($iId, $iViewedId) = $this->_prepareIds();
+
+        if ($this->_oConnection->isConnected($iViewedId, $iId, true))
+            $a = $this->_oConnection->actionRemove($iId, $iViewedId);
+        else
+            $a = $this->_oConnection->actionReject($iId, $iViewedId);
+
+        if (isset($a['err']) && $a['err'])
+            return false;
+
+        return true;
+    }
+
+    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 _getCellInfo ($mixedValue, $sKey, $aField, $aRow)
+    {
+        $s = '';
+
+        // for friend requests display mutual friends
+        if ($this->_bOwner && !$aRow['mutual']) {
+            $a = $this->_oConnection->getCommonContent($aRow['id'], bx_get_logged_profile_id(), true);
+            $i = count($a);
+            if (1 == $i) {
+                $iProfileId = array_pop($a);
+                $oProfile = BxDolProfile::getInstance($iProfileId);
+                $s = _t('_sys_txt_one_mutual_friend', $oProfile->getUrl(), $oProfile->getDisplayName());
+            } elseif ($i) {
+                $s = _t('_sys_txt_n_mutual_friends', $i);
+            }
+        }
+
+        // display friends number if no other info is available
+        if (!$s) {
+            $a = $this->_oConnection->getConnectedContent($aRow['id'], true);
+            $i = count($a);
+            $s = _t('_sys_txt_n_friends', $i);
+        }
+
+        return parent::_getCellDefault ($s, $sKey, $aField, $aRow);
+    }
+
+    protected function _getActionDelete ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if (!$this->_bOwner)
+            return '';
+        return parent::_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getActionAccept ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if (!$this->_bOwner || $aRow['mutual'])
+            return '';
+
+        return parent::_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getActionAddFriend ($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = array())
+    {
+        if ($this->_bOwner || $aRow['id'] == bx_get_logged_profile_id())
+            return '';
+
+        if ($this->_oConnection->isConnected($aRow['id'], bx_get_logged_profile_id()) || $this->_oConnection->isConnected(bx_get_logged_profile_id(), $aRow['id']))
+            return '';
+
+        return parent::_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _prepareIds ()
+    {
+        $iViewedId = false;
+        $iId = 0;
+        $aIds = bx_get('ids');
+        if ($aIds && is_array($aIds))
+            $mixedId = array_pop($aIds);
+
+        if (false === strpos($mixedId, ':')) {
+            $iId = (int)$mixedId;
+        }
+        else {
+            list ($iId, $iViewedId) = explode (':', $mixedId);
+            $iId = (int)$iId;
+            $iViewedId = (int)$iViewedId;
+        }
+        return array($iId, $iViewedId);
+    }
+}
+
+/** @} */

+ 88 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolGridQuery.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
+ * @{
+ */
+
+/**
+ * Database queries for grid.
+ * @see BxDolGrid
+ */
+class BxDolGridQuery extends BxDolDb
+{
+    protected $_aObject;
+
+    public function __construct($aObject)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+    }
+
+    static public function getGridObject ($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_grid` WHERE `object` = ?", $sObject);
+        $aObject = $oDb->getRow($sQuery);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        // paginate
+        if (!empty($aObject['paginate_url']) && 0 !== strncasecmp($aObject['paginate_url'], 'http://', 7) && 0 !== strncasecmp($aObject['paginate_url'], 'https://', 8))
+            $aObject['paginate_url'] = BX_DOL_URL_ROOT . $aObject['paginate_url'];
+
+        // filter
+        if ($aObject['filter_fields'])
+            $aObject['filter_fields'] = array_map('trim', explode(',', $aObject['filter_fields']));
+
+        if ($aObject['filter_fields_translatable'])
+            $aObject['filter_fields_translatable'] = array_map('trim', explode(',', $aObject['filter_fields_translatable']));
+
+        // sorting
+        if ($aObject['sorting_fields'])
+            $aObject['sorting_fields'] = array_map('trim', explode(',', $aObject['sorting_fields']));
+
+        if ($aObject['sorting_fields_translatable']) {
+            $aObject['sorting_fields_translatable'] = array_map('trim', explode(',', $aObject['sorting_fields_translatable']));
+            $aObject['sorting_fields'] = array_merge ($aObject['sorting_fields'], $aObject['sorting_fields_translatable']);
+        }
+
+        // get fields
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_grid_fields` WHERE `object` = ? ORDER BY `order`", $sObject);
+        $aFields = $oDb->getAllWithKey($sQuery, 'name');
+        if (!$aFields || !is_array($aFields)) // it is impossible to have grid without any fields
+            return false;
+        foreach ($aFields as $sKey => $aRow) {
+            $aObject['fields'][$sKey] = array(
+                'title' => _t($aRow['title']),
+                'width' => $aRow['width'],
+                'translatable' => $aRow['translatable'],
+                'chars_limit' => $aRow['chars_limit'],
+            );
+            if (empty($aRow['params']))
+                continue;
+            $aAdd = unserialize($aRow['params']);
+            if (!empty($aAdd) && is_array($aAdd))
+                $aObject['fields'][$sKey] = array_merge($aObject['fields'][$sKey], $aAdd);
+        }
+        // get actions
+        $a = array('bulk','single','independent');
+        foreach ($a as $sActionType) {
+            $sActionField = 'actions_' . $sActionType;
+            $aObject[$sActionField] = array();
+            $sQuery = $oDb->prepare("SELECT * FROM `sys_grid_actions` WHERE `object` = ? AND `type` = ? ORDER BY `order`", $sObject, $sActionType);
+            $aActions = $oDb->getAllWithKey($sQuery, 'name');
+            if (!$aActions || !is_array($aActions))
+                continue;
+            foreach ($aActions as $sKey => $aRow)
+                $aObject[$sActionField][$sKey] = $aRow['title'] || $aRow['icon'] ? array('title' => _t($aRow['title']), 'icon' => $aRow['icon'], 'confirm' => $aRow['confirm'], 'icon_only' => $aRow['icon_only']) : array();
+        }
+
+        return $aObject;
+    }
+
+}
+
+/** @} */

+ 141 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolGzip.php

@@ -0,0 +1,141 @@
+<?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 BxDolGzip extends BxDol
+{
+    protected $_sType;
+    protected $_bGzip;
+    protected $_sEncoding;
+    protected $_iExpirationOffset;
+
+    protected $_sInFile;
+
+    protected $_sOutFile;
+    protected $_sOutContent;
+    protected $_sOutContentZipped;
+
+    function __construct($sFile)
+    {
+        parent::__construct();
+
+        $this->_iExpirationOffset = 3600 * 24 * 10;
+
+        $this->_sInFile = BX_DIRECTORY_PATH_CACHE_PUBLIC . $sFile;
+        $this->_sOutFile = BX_DIRECTORY_PATH_CACHE_PUBLIC . $sFile . '.gz';
+
+        $aMatches = array();
+        if(!preg_match("/^([a-z0-9_-]+)\.(js|css)$/", $sFile, $aMatches))
+            die('BxDolGzip: file is not js/css');
+
+        switch($aMatches[2]) {
+            case 'css':
+                $this->_sType = 'css';
+                break;
+            case 'js':
+                $this->_sType = 'javascript';
+                break;
+        }
+
+        $this->_sOutContent = "";
+        $this->_sOutContentZipped = "";
+    }
+
+    static function load($sFile)
+    {
+        $oLoader = new BxDolGzip($sFile);
+
+        $oLoader->prepare();
+        $oLoader->read();
+        $oLoader->output();
+    }
+
+    function prepare()
+    {
+        header("Content-type: text/" . $this->_sType);
+        header("Vary: Accept-Encoding");
+        header("Expires: " . gmdate("D, d M Y H:i:s", time() + $this->_iExpirationOffset) . " GMT");
+
+        $encodings = array ();
+        if (isset($_SERVER['HTTP_ACCEPT_ENCODING']))
+            $encodings = explode(',', strtolower(preg_replace("/\s+/", "", $_SERVER['HTTP_ACCEPT_ENCODING'])));
+
+        if ((in_array('gzip', $encodings) || in_array('x-gzip', $encodings) || isset($_SERVER['---------------'])) && function_exists('ob_gzhandler') && !ini_get('zlib.output_compression')) {
+            $this->_sEncoding = in_array('x-gzip', $encodings) ? "x-gzip" : "gzip";
+            $this->_bGzip = true;
+        }
+    }
+
+    function read()
+    {
+        if($this->_bGzip && file_exists($this->_sOutFile))
+            $this->_sOutContentZipped = $this->getFileContents($this->_sOutFile);
+        else
+            $this->_sOutContent = $this->getFileContents($this->_sInFile);
+    }
+
+    function output()
+    {
+        if(!$this->_bGzip) {
+            echo $this->_sOutContent;
+            return;
+        }
+
+        header("Content-Encoding: " . $this->_sEncoding);
+        if(!empty($this->_sOutContentZipped)) {
+            echo $this->_sOutContentZipped;
+            return;
+        }
+
+        if (!$this->_sOutContent)
+            return;
+
+        $this->_sOutContentZipped = gzencode($this->_sOutContent, 9, FORCE_GZIP);
+        $this->putFileContents($this->_sOutFile, $this->_sOutContentZipped);
+
+        echo $this->_sOutContentZipped;
+    }
+
+    function getFileContents($sPath)
+    {
+        $sPath = realpath($sPath);
+
+        if(!$sPath || !@is_file($sPath))
+            return "";
+
+        if(function_exists("file_get_contents"))
+            return @file_get_contents($sPath);
+
+        $sContent = "";
+        if(!($rHandler = @fopen($sPath, "r")))
+            return "";
+
+        while(!feof($rHandler))
+            $sContent .= fgets($rHandler);
+
+        fclose($rHandler);
+
+        return $sContent;
+    }
+
+    function putFileContents($sPath, $sContent)
+    {
+        if (function_exists("file_put_contents"))
+            return @file_put_contents($sPath, $sContent);
+
+        if(!($rHandler = @fopen($sPath, "wb")))
+            return 0;
+
+        $iResult = (int)fwrite($rHandler, $sContent);
+        fclose($rHandler);
+
+        return $iResult;
+    }
+}
+
+/** @} */

+ 70 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolIO.php

@@ -0,0 +1,70 @@
+<?php defined('BX_DOL') or defined('BX_DOL_INSTALL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+class BxDolIO extends BxDol
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    public static function isRealOwner()
+    {
+    	if(defined('BX_DOL_CRON_EXECUTE'))
+    		trigger_error('Function can\'t be called under cron', E_USER_ERROR);
+
+		$sName = time() . rand(0, 999999999);
+		$sFilePath = BX_DIRECTORY_PATH_TMP . $sName . '.txt';
+		if(!$rHandler = fopen($sFilePath, 'w'))
+            return false;
+
+		if(!fwrite($rHandler, $sName))
+            return false;
+
+		fclose($rHandler);
+
+		$bResult = fileowner(BX_DIRECTORY_PATH_INC . 'utils.inc.php') === fileowner($sFilePath);
+		@unlink($sFilePath);
+
+		return $bResult;
+    }
+
+    public static function isExecutable($sFile)
+    {
+        clearstatcache();
+
+        $aPathInfo = pathinfo(__FILE__);
+        $sFile = $aPathInfo['dirname'] . '/../../' . $sFile;
+
+        return (is_file($sFile) && is_executable($sFile));
+    }
+
+    public static function isWritable($sFile)
+    {
+        clearstatcache();
+
+        $aPathInfo = pathinfo(__FILE__);
+        $sFile = $aPathInfo['dirname'] . '/../../' . $sFile;
+
+        return is_readable($sFile) && is_writable($sFile);
+    }
+
+    public static function getPermissions($sFileName)
+    {
+        $sPath = isset($GLOBALS['logged']['admin']) && $GLOBALS['logged']['admin'] ? BX_DIRECTORY_PATH_ROOT : '../';
+
+        clearstatcache();
+        $hPerms = @fileperms($sPath . $sFileName);
+        if($hPerms == false) return false;
+        $sRet = substr( decoct( $hPerms ), -3 );
+        return $sRet;
+    }
+}
+
+/** @} */

+ 301 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolImageResize.php

@@ -0,0 +1,301 @@
+<?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
+ * @{
+ */
+
+spl_autoload_register(function ($sClass) {
+    // autoload Intervention Image files
+    $sClass = trim($sClass, '\\');
+    if (0 === strncmp('Intervention', $sClass, 12)) {
+        $sFile = BX_DIRECTORY_PATH_PLUGINS . 'intervention-image/' . str_replace('\\', '/', $sClass) . '.php';
+        if (file_exists($sFile))
+            require_once($sFile);
+    }
+});
+
+define('IMAGE_ERROR_SUCCESS', 0); ///< operation was successfull
+define('IMAGE_ERROR_WRONG_TYPE', 2); ///< operation failed, most probably because incorrect image format(or not image file) was provided
+
+class BxDolImageResize extends BxDolFactory implements iBxDolSingleton
+{
+    protected $w = 64, $h = 64; ///< size of destination image
+    protected $_isAutoCrop = false;
+    protected $_iJpegQuality = 90; ///< jpeg quality
+    protected $_isSquareResize = false; ///< use smart resize, destination image will be exact Width x Height size
+    protected $_isUseGD; ///< use GD library or command line ImagMagic utilites
+    protected $_oManager; ///< Intervention Image Manager
+    protected $_sError; ///< Intervention Image Manager error string
+
+    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->_isUseGD = getParam('enable_gd') == 'on' && extension_loaded('gd') ? true : false;
+
+        $this->_oManager = new Intervention\Image\ImageManager(array('driver' => $this->_isUseGD ? 'gd' : 'imagick'));
+    }
+
+    /**
+     * 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 BxDolImageResize();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    function getManager ()
+    {
+        return $this->_oManager;
+    }
+
+    function getError ()
+    {
+        return $this->_sError;
+    }
+
+    function isAllowedImage ($sSrcImage)
+    {
+        try {
+            $this->_oManager->make($sSrcImage);
+        }
+        catch (Exception $e) {
+            return false;
+        }
+
+        return true;
+    }
+
+    function resize ($mixedImage, $sDstImage = '')
+    {
+        if (is_array($mixedImage)) {
+            $aRet = array();
+            foreach ($mixedImage as $s) {
+                $aRet[] = $this->_resize ($s, $s);
+            }
+            return $aRet;
+        } else {
+            return $this->_resize ($mixedImage, $sDstImage);
+        }
+    }
+
+    function applyWatermark ($mixedImage, $sDstImage, $sWtrImage, $iTransparency, $sPosition = 'bottom-right', $sPositionOffsetX = 0, $sPositionOffsetY = 0, $sScaleFactor = 0.2 )
+    {
+        if (is_array($mixedImage)) {
+            $aRet = array();
+            foreach ($mixedImage as $s)
+                $aRet[] = $this->_applyWatermark ($s, $s, $sWtrImage, $iTransparency, $sPosition, $sPositionOffsetX, $sPositionOffsetY, $sScaleFactor);
+            return $aRet;
+        } else {
+            return $this->_applyWatermark ($mixedImage, $sDstImage, $sWtrImage, $iTransparency, $sPosition, $sPositionOffsetX, $sPositionOffsetY, $sScaleFactor);
+        }
+    }
+
+    function grayscale ($mixedImage, $sDstImage = '')
+    {
+        if (is_array($mixedImage)) {
+            $aRet = array();
+            foreach ($mixedImage as $s) {
+                $aRet[] = $this->_grayscale ($s, $s);
+            }
+            return $aRet;
+        } else {
+            return $this->_grayscale ($mixedImage, $sDstImage);
+        }
+    }
+
+    function setSize ($w, $h)
+    {
+        $this->w = $w;
+        $this->h = $h;
+    }
+
+    function removeCropOptions ()
+    {
+        $this->_isAutoCrop = false;
+    }
+
+    /**
+     * Crop image to destination size with filling whole area of destination size
+     */
+    function setAutoCrop ($b)
+    {
+        $this->_isAutoCrop = $b;
+    }
+
+    function setJpegQuality ($i)
+    {
+        $this->_iJpegQuality = $i;
+    }
+
+    function setSquareResize ($b)
+    {
+        $this->_isSquareResize = ($b ? true : false);
+    }
+
+    function isUsedGD ()
+    {
+        return $this->_isUseGD;
+    }
+
+    static function getImageSize($sPath)
+    {
+        $o = self::getInstance();
+        return $o->_getImageSize($sPath);
+    }
+
+    function _getImageSize($sPath)
+    {
+        $this->_sError = '';
+        try {        
+            $o = $this->_oManager->make($sPath);
+        } 
+        catch (Exception $e) {
+            $this->_sError = $e->getMessage();
+            return false;
+        }
+        return array ('w' => $o->width(), 'h' => $o->height());
+    }
+
+    function getExifInfo($sSrcImage, $bCreateLocalFileIfUrl = true)
+    {
+        $this->_sError = '';
+        $sTmpFileName = false;
+        $mixedRet = false;
+
+        if ($bCreateLocalFileIfUrl && preg_match('/^https?:\/\//', $sSrcImage)) {
+            $sTmpFileName = tempnam(BX_DIRECTORY_PATH_TMP, '');
+            file_put_contents($sTmpFileName, file_get_contents($sSrcImage));
+        }
+
+        try {
+            $mixedRet = $this->_oManager
+                ->make($sTmpFileName ? $sTmpFileName : $sSrcImage)
+                ->exif();
+        }
+        catch (Exception $e) {
+            $this->_sError = $e->getMessage();
+        }
+
+        if ($sTmpFileName)
+            @unlink($sTmpFileName);
+
+        return $mixedRet;
+    }
+
+    function getAverageColor($sSrcImage)
+    {
+        $this->_sError = '';
+        try {
+            $a = $this->_oManager
+                ->make($sSrcImage)
+                ->resize(1, 1)
+                ->pickColor(0, 0, 'array');
+
+            return array('r' => $a[0], 'g' => $a[1], 'b' => $a[2]);
+        }
+        catch (Exception $e) {
+            $this->_sError = $e->getMessage();
+            return false;
+        }
+    }
+
+    // private functions are below -------------------------------
+
+    function _grayscale ($sSrcImage, $sDstImage = '')
+    {
+        $this->_sError = '';
+        try {
+            $this->_oManager
+                ->make($sSrcImage)
+                ->greyscale()
+                ->save($sDstImage ? $sDstImage : $sSrcImage, $this->_iJpegQuality);
+
+            chmod($sDstImage ? $sDstImage : $sSrcImage, BX_DOL_FILE_RIGHTS);
+        }
+        catch (Exception $e) {
+            $this->_sError = $e->getMessage();
+            return IMAGE_ERROR_WRONG_TYPE;
+        }
+
+        return IMAGE_ERROR_SUCCESS;
+    }
+
+    function _resize ($sSrcImage, $sDstImage = '')
+    {       
+        $this->_sError = '';
+        try {
+            if ($this->_isAutoCrop || $this->_isSquareResize) {
+                $this->_oManager
+                    ->make($sSrcImage)
+                    ->orientate()
+                    ->fit($this->w, $this->_isSquareResize ? $this->w : $this->h)
+                    ->save($sDstImage ? $sDstImage : $sSrcImage, $this->_iJpegQuality);
+            } 
+            else {
+                $this->_oManager
+                    ->make($sSrcImage)
+                    ->orientate()
+                    ->resize($this->w, $this->h, function ($constraint) {
+                        $constraint->aspectRatio();
+                        $constraint->upsize();
+                    })
+                    ->save($sDstImage ? $sDstImage : $sSrcImage, $this->_iJpegQuality);
+            }
+            chmod($sDstImage ? $sDstImage : $sSrcImage, BX_DOL_FILE_RIGHTS);
+        }
+        catch (Exception $e) {
+            $this->_sError = $e->getMessage();
+            return IMAGE_ERROR_WRONG_TYPE;
+        }
+
+        return IMAGE_ERROR_SUCCESS;
+    }
+
+    function _applyWatermark( $sSrcImage, $sDstImage, $sWtrImage, $iTransparency, $sPosition = 'bottom-right', $sPositionOffsetX = 0, $sPositionOffsetY = 0, $sScaleFactor = 0.2)
+    {
+        $this->_sError = '';
+        try {
+            $oImageOrig = $this->_oManager->make($sSrcImage);
+
+            $oImageOrig
+                ->insert($this->_oManager
+                    ->make($sWtrImage)
+                    ->resize(round($oImageOrig->width() * $sScaleFactor), round($oImageOrig->height() * $sScaleFactor),  function ($constraint) {
+                        $constraint->aspectRatio();
+                        $constraint->upsize();
+                    })
+                    ->opacity($iTransparency), 
+                $sPosition, $sPositionOffsetX, $sPositionOffsetY)
+                ->save($sDstImage ? $sDstImage : $sSrcImage, $this->_iJpegQuality);
+
+            chmod($sDstImage ? $sDstImage : $sSrcImage, BX_DOL_FILE_RIGHTS);
+        }
+        catch (Exception $e) {
+            $this->_sError = $e->getMessage();
+            return IMAGE_ERROR_WRONG_TYPE;
+        }
+
+        return IMAGE_ERROR_SUCCESS;        
+    }
+}
+
+/** @} */

+ 126 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolInformer.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
+ * @{
+ */
+
+define('BX_INFORMER_ALERT', 1);
+define('BX_INFORMER_INFO', 2);
+define('BX_INFORMER_ERROR', 3);
+
+/**
+ * @page objects
+ * @section informer Informer
+ * @ref BxDolInformer
+ */
+
+/**
+ * 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));
+    }
+
+}
+
+/** @} */

+ 85 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolInstallerHasher.php

@@ -0,0 +1,85 @@
+<?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_SYSTEM_MODULE_ID', 0);
+
+/**
+ * Functions for hashing system files.
+ * Hashing functions for module file are in @see BxDolInstallerUtils class.
+ */
+class BxDolInstallerHasher extends BxDolInstallerUtils
+{
+    protected $_aSystemFiles = array(
+
+        // slash at the end is necessary for directories
+        'inc/',   
+        'modules/base/',
+        'periodic/',
+        'studio/',
+        'template/',
+        
+        'cmts.php',
+        'conn.php',
+        'get_rss_feed.php',
+        'grid.php',
+        'gzip_loader.php',
+        'image_transcoder.php',
+        'index.php',
+        'logout.php',
+        'member.php',
+        'menu.php',
+        'page.php',
+        'searchKeyword.php',
+        'searchKeywordContent.php',
+        'splash.php',
+        'storage.php',
+        'storage_uploader.php',
+        'vote.php',
+        'modules/index.php',
+        'README.md',
+        'license.txt',
+    );
+
+    public function getSystemFilesHash () 
+    {
+        $this->_aNonHashableFiles = array(
+            'inc/header.inc.php',
+        );
+        
+        $aFilesHashed = array();
+
+        foreach ($this->_aSystemFiles as $sFile)
+            $this->hashFiles(BX_DIRECTORY_PATH_ROOT . $sFile, $aFilesHashed);
+
+        return $aFilesHashed;
+    }
+
+    public function hashSystemFiles()
+    {
+        $aFiles = $this->getSystemFilesHash ();
+        if (!$aFiles)
+            return false;
+
+        $oDb = bx_instance('BxDolStudioInstallerQuery');
+
+        foreach($aFiles as $aFile)
+            $oDb->insertModuleTrack(BX_SYSTEM_MODULE_ID, $aFile);
+
+        return true;
+    }
+
+    public function checkSystemFilesHash (&$fChangedFilesPercent) 
+    {
+        $aFiles = $this->getSystemFilesHash ();
+        list($aFilesChanged, $fChangedFilesPercent) = $this->hashCheck($aFiles, BX_SYSTEM_MODULE_ID);
+        return $aFilesChanged;
+    }
+}
+
+/** @} */

+ 170 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolInstallerUtils.php

@@ -0,0 +1,170 @@
+<?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_FORCE_AUTOUPDATE_MAX_CHANGED_FILES_PERCENT', 0.05);
+define('BX_FORCE_USE_FTP_FILE_TRANSFER', false);
+
+class BxDolInstallerUtils extends BxDolIO
+{
+    protected $_aNonHashableFiles = array();
+
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    static public function isAllowUrlInclude()
+    {
+        if (version_compare(phpversion(), "5.2", ">") == 1) {
+            $sAllowUrlInclude = ini_get('allow_url_include');
+            return !($sAllowUrlInclude == 0);
+        };
+        return false;
+    }
+
+    static public function getModuleConfig($mixed)
+    {
+    	$sConfig = '';
+    	if(is_array($mixed) && !empty($mixed['path']))
+			$sConfig = BX_DIRECTORY_PATH_MODULES . $mixed['path'] . 'install/config.php';
+		else if(is_string($mixed))
+			$sConfig = $mixed;
+		else 
+			return array();
+
+    	if(!file_exists($sConfig))
+            return array();
+
+        include($sConfig);
+
+        return $aConfig;
+    }
+
+    static public function isModuleInstalled($sUri)
+    {
+        return BxDolModuleQuery::getInstance()->isModule($sUri);
+    }
+
+    /**
+     * Set module for delayed uninstall
+     */
+    static public function setModulePendingUninstall($sUri, $bPendingUninstall = true)
+    {
+        return BxDolModuleQuery::getInstance()->setModulePendingUninstall($sUri, $bPendingUninstall);
+    }
+
+    /**
+     * Check if module is pending for uninstall
+     */
+    static public function isModulePendingUninstall($sUri)
+    {
+        $a = BxDolModuleQuery::getInstance()->getModuleByUri($sUri);
+        return $a['pending_uninstall'];
+    }
+
+    /**
+     * Check all pending for uninstallation modules and uninstall them if no pending for deletion files are found
+     */
+    static public function checkModulesPendingUninstall()
+    {
+        $a = BxDolModuleQuery::getInstance()->getModules();
+        foreach ($a as $aModule) {
+
+            // after we make sure that all pending for deletion files are deleted
+            if (!$aModule['pending_uninstall'] || BxDolStorage::isQueuedFilesForDeletion($aModule['name']))
+                continue;
+
+            // remove pending uninstall flag
+            self::setModulePendingUninstall($aModule['uri'], false);
+
+            // perform uninstallation
+            $aResult = BxDolStudioInstallerUtils::getInstance()->perform($aModule['path'], 'uninstall');
+
+            // send email nofitication
+            $aTemplateKeys = array(
+                'Module' => $aModule['title'],
+                'Result' => _t('_Success'),
+                'Message' => '',
+            );
+
+            if ($aResult['code'] > 0) {
+                $aTemplateKeys['Result'] = _t('_Failed');
+                $aTemplateKeys['Message'] = $aResult['message'];
+            }
+
+            $aMessage = BxDolEmailTemplates::getInstance()->parseTemplate('t_DelayedModuleUninstall', $aTemplateKeys);
+            sendMail (getParam('site_email'), $aMessage['Subject'], $aMessage['Body'], 0, array(), BX_EMAIL_SYSTEM);
+        }
+    }
+
+    /**
+     * Generate hash for module files.
+     * @param $sPath module's root folder
+     * @param $aFiles array to fill with files hashes
+     */
+    public function hashFiles($sPath, &$aFiles)
+    {
+        $aExcludes = array('.', '..', 'error_log', 'php.ini', '.DS_Store', 'Thumbs.db');
+        if (file_exists($sPath) && is_dir($sPath) && ($rSource = opendir($sPath))) {
+            while (($sFile = readdir($rSource)) !== false) {
+                if ('.' == $sFile[0] || in_array($sFile, $aExcludes))
+                    continue;
+                
+                if (in_array($this->filePathWithoutBase($sPath . $sFile), $this->_aNonHashableFiles))
+                    continue;                
+
+                if (is_dir($sPath . $sFile))
+                    $this->hashFiles($sPath . $sFile . '/', $aFiles);
+                else
+                    $aFiles[] = $this->hashInfo($sPath . $sFile);
+            }
+            closedir($rSource);
+        } elseif (file_exists($sPath) && is_file($sPath)) {
+            $aFiles[] = $this->hashInfo($sPath);
+        }
+    }
+
+    /** 
+     * Check module's files hashes. For system files use @see BxDolInstallerHasher class.
+     * @param $aFiles current files checksums 
+     * @param $iModuleId module id
+     * @return empty array on success, or array of files which checksum was changed 
+     */
+    public function hashCheck($aFiles, $iModuleId)
+    {
+        $oDb = bx_instance('BxDolStudioInstallerQuery');
+
+        $aFilesOrig = $oDb->getModuleTrackFiles($iModuleId);
+        $aFailesChanged = array();
+        foreach ($aFiles as $aFile)
+            if(!isset($aFilesOrig[$aFile['file']]) || $aFilesOrig[$aFile['file']]['hash'] != $aFile['hash'])
+                $aFailesChanged[] = $aFile['file'];
+
+		$fChangedPercent = 0;
+        if(count($aFilesOrig) != 0)
+            $fChangedPercent = count($aFailesChanged) / count($aFilesOrig);
+
+        return array($aFailesChanged, $fChangedPercent);
+    }
+    
+    protected function hashInfo($sPath)
+    {
+        return array(
+            'file' => $this->filePathWithoutBase($sPath),
+            'hash' => md5_file($sPath)
+        );
+    }
+
+    protected function filePathWithoutBase($sPath)
+    {
+        return bx_ltrim_str($sPath, BX_DIRECTORY_PATH_ROOT);
+    }
+}
+
+/** @} */

+ 132 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolKey.php

@@ -0,0 +1,132 @@
+<?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
+ * @{
+ */
+
+/**
+ * @page objects
+ * @section key Keys
+ * @ref BxDolKey
+ */
+
+/**
+ * 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();
+    }
+
+}
+
+/** @} */

+ 52 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolKeyQuery.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    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Database queries for BxDolKey object.
+ * @see BxDolKey
+ */
+class BxDolKeyQuery extends BxDolDb
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    public function insert ($sKey, $sData, $iExpire)
+    {
+        $sQuery = $this->prepare("INSERT INTO `sys_keys` SET `key` = ?, `data` = ?, `expire` = ?", $sKey, $sData, time() + $iExpire);
+        return $this->query($sQuery);
+    }
+
+    public function remove ($sKey)
+    {
+        $sQuery = $this->prepare("DELETE FROM `sys_keys` WHERE `key` = ?", $sKey);
+        return $this->query($sQuery);
+    }
+
+    public function get ($sKey)
+    {
+        $sQuery = $this->prepare("SELECT `key` FROM `sys_keys` WHERE `key` = ?", $sKey);
+        return $this->getOne($sQuery);
+    }
+
+    public function getData ($sKey)
+    {
+        $sQuery = $this->prepare("SELECT `data` FROM `sys_keys` WHERE `key` = ?", $sKey);
+        return $this->getOne($sQuery);
+    }
+
+    public function prune ()
+    {
+        $sQuery = $this->prepare("DELETE FROM `sys_keys` WHERE `expire` < ?", time());
+        return $this->query($sQuery);
+    }
+}
+
+/** @} */

+ 355 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolLanguages.php

@@ -0,0 +1,355 @@
+<?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_LANGUAGE_DEFAULT', 'en');
+
+define('BX_DOL_LANGUAGE_CATEGORY_SYSTEM', 1);
+define('BX_DOL_LANGUAGE_CATEGORY_CUSTOM', 2);
+
+class BxDolLanguages extends BxDolFactory implements iBxDolSingleton
+{
+    protected $oDb;
+    protected $sCurrentLanguage;
+
+    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 = BxDolLanguagesQuery::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 BxDolLanguages();
+            $GLOBALS['bxDolClasses'][__CLASS__]->init();
+        }
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    function getCurrentLangName($bSetCookie = true)
+    {
+        $sLang = '';
+
+        if(!$sLang && !empty($_GET['lang']))
+            $sLang = $this->tryToGetLang($_GET['lang'], $bSetCookie);
+
+        if(!$sLang && !empty($_POST['lang']))
+            $sLang = $this->tryToGetLang($_POST['lang'], $bSetCookie);
+
+        if(!$sLang && !empty($_COOKIE['lang']))
+            $sLang = $this->tryToGetLang($_COOKIE['lang']);
+
+        if(!$sLang && !empty($_SERVER['HTTP_ACCEPT_LANGUAGE']))
+            $sLang = $this->tryToGetLang($_SERVER['HTTP_ACCEPT_LANGUAGE']);
+
+        if(!$sLang)
+            $sLang = $this->tryToGetLang(getParam('lang_default'));
+
+        if(!$sLang)
+            $sLang = $this->tryToGetLang(BX_DOL_LANGUAGE_DEFAULT);
+
+        setlocale(LC_TIME, $sLang . '_' . strtoupper($sLang) . '.utf-8', $sLang . '_' . strtoupper($sLang) . '.utf8', $sLang . '.utf-8', $sLang . '.utf8', $sLang);
+        return $sLang;
+    }
+
+    function getCurrentLangId()
+    {
+        return $this->getLangId($this->getCurrentLanguage());
+    }
+
+    function getLangId($sLang)
+    {
+        return $this->oDb->getLanguageId($sLang);
+    }
+
+    function getLangFlag($sLang = '')
+    {
+        if (!$sLang)
+            $sLang = $this->getCurrentLanguage();
+        return $this->oDb->getLanguageFlag($sLang);
+    }
+
+    function getDefaultLangName()
+    {
+        return getParam('lang_default');
+    }
+
+    function getLanguages($bIdAsKey = false, $bActiveOnly = false)
+    {
+        return $this->oDb->getLanguages($bIdAsKey, $bActiveOnly);
+    }
+
+    function getLanguageCategory($sName)
+    {
+        $iId = 0;
+        $this->oDb->getCategoriesBy(array('type' => 'id_by_name', 'value' => $sName), $iId);
+
+        return (int)$iId;
+    }
+
+    /**
+     * It tries to match current language with provided languages array, if nothing is found $sFallbackLanguage is returned.
+     * @param $aLangs - array of languages, example: array('ru' => 1, 'kg' => 1);
+     * @param $sFallbackLanguage - language to return of nothis is found
+     * @return language code string
+     */
+    function detectLanguageFromArray($aLangs, $sFallbackLanguage = 'en')
+    {
+        return isset($aLangs[$GLOBALS['sCurrentLanguage']]) ? $GLOBALS['sCurrentLanguage'] : $sFallbackLanguage;
+    }
+
+    /**
+     * Get current language.
+     */
+    function getCurrentLanguage()
+    {
+        return $GLOBALS['sCurrentLanguage'];
+    }
+
+    function _t()
+    {
+        global $LANG;
+
+        $key = func_get_arg(0);
+        if(isset($LANG[$key])) {
+            $str = $LANG[$key];
+
+            if(($iNumArgs = func_num_args()) > 1)
+                for($i = 1; $i < $iNumArgs; $i++)
+                    $str = str_replace('{' . ($i - 1) . '}', func_get_arg($i), $str);
+
+            return $str;
+        } else
+            return $key;
+    }
+
+    function _t_err()
+    {
+        return MsgBox(call_user_func_array(array($this, '_t'), func_get_args()));
+    }
+
+    function _t_action()
+    {
+        return MsgBox(call_user_func_array(array($this, '_t'), func_get_args()));
+    }
+
+    function _t_ext($key, $args)
+    {
+        global $LANG;
+
+        if(isset($LANG[$key])) {
+            $str = $LANG[$key];
+
+            if(!is_array($args))
+                return str_replace('{0}', $args, $str);
+
+            foreach ($args as $key => $val)
+                $str = str_replace('{'.$key.'}', $val, $str);
+
+            return $str;
+        } else
+            return $key;
+    }
+
+    function _t_format_size ($iSize)
+    {
+        $a = array (
+            1024 => '_sys_format_size_b',
+            1024*1024 => '_sys_format_size_kb',
+            1024*1024*1024 => '_sys_format_size_mb',
+            1024*1024*1024*1024 => '_sys_format_size_gb',
+            1024*1024*1024*1024*1024 => '_sys_format_size_tb',
+        );
+        foreach($a as $i => $sKey)
+            if($iSize < $i)
+                return $this->_t($sKey, round($iSize / ($i / 1024), 1));
+        return $this->_t('_sys_format_size_b', 0);
+    }
+
+    function _t_format_currency ($fPrice)
+    {
+        return $this->_t('_sys_currency', getParam('currency_sign'), sprintf("%.2f", (float)$fPrice));
+    }
+
+    function _t_format_extensions ($mixedExtensions)
+    {
+        if (!is_array($mixedExtensions))
+            $a = explode(',', $mixedExtensions);
+        else
+            $a = $mixedExtensions;
+        if (!$a)
+            return '';
+        return '.' . implode(', .', $a);
+    }
+
+    protected function init()
+    {
+        /**
+         * Trying to initialize default language.
+         */
+        $GLOBALS['sCurrentLanguage'] = $GLOBALS['bxDolClasses'][__CLASS__]->getCurrentLangName(false);
+        if($GLOBALS['sCurrentLanguage'] != '') {
+            $sPath = BX_DIRECTORY_PATH_CACHE . 'lang-' . $GLOBALS['sCurrentLanguage'] . '.php';
+            if(!file_exists($sPath))
+                BxDolStudioLanguagesUtils::getInstance()->compileLanguage();
+
+            require($sPath);
+        }
+
+        $GLOBALS['bxDolClasses'][__CLASS__]->getCurrentLangName(true);
+        if(isset($_GET['lang'])) {
+            if(BxDolPermalinks::getInstance()->redirectIfNecessary(array('lang')))
+                exit;
+        }
+    }
+
+    protected function tryToGetLang($sLangs, $bSetCookie = false)
+    {
+        $sLangs = trim($sLangs);
+        if(!$sLangs)
+            return '';
+
+        $sLangs = preg_replace( '/[^a-zA-Z0-9,;-]/m', '', $sLangs ); // we do not need 'q=0.3'. we are using live queue :)
+        $sLangs = strtolower($sLangs);
+
+        if(!$sLangs)
+            return '';
+
+        $aLangs = explode(',', $sLangs); // ru,en-us;q=0.7,en;q=0.3 => array( 'ru' , 'en-us;q=0.7' , 'en;q=0.3' );
+        foreach($aLangs as $sLang) {
+            if(!$sLang)
+                continue;
+
+            list($sLang) = explode(';', $sLang, 2); // en-us;q=0.7 => en-us
+            if(!$sLang)
+                continue;
+
+            // check with country
+            if($this->checkLangExists($sLang)) {
+                if( $bSetCookie && (!isset($_COOKIE['lang']) || $_COOKIE['lang'] != $sLang) && (!isset($GLOBALS['glLangSet']) || $GLOBALS['glLangSet'] != $sLang)) {
+                    $this->setLangCookie( $sLang );
+                    $GLOBALS['glLangSet'] = $sLang;
+                }
+                return $sLang;
+            }
+
+            //drop country
+            if(strpos($sLang, '-') === false)
+                continue;
+
+            list($sLang, $sCntr) = explode('-', $sLang, 2); // en-us => en
+            if(!$sLang or !$sCntr)
+                continue; //no lang or nothing changed
+
+            //check again. without country
+            if($this->checkLangExists($sLang)) {
+                if($bSetCookie)
+                    $this->setLangCookie($sLang);
+                return $sLang;
+            }
+        }
+        return '';
+    }
+    protected function checkLangExists( $sLang )
+    {
+        if(!preg_match('/^[A-Za-z0-9_]+$/', $sLang))
+            return false;
+
+        $iLangId = $this->oDb->getLanguageId($sLang, false);
+        if(!$iLangId)
+            return false;
+
+        if(file_exists( BX_DIRECTORY_PATH_CACHE . "lang-{$sLang}.php"))
+            return true;
+
+        if(BxDolStudioLanguagesUtils::getInstance()->compileLanguage($iLangId))
+            return true;
+
+        return false;
+    }
+
+    protected function setLangCookie( $sLang )
+    {
+        $sLang = bx_process_input($sLang);
+
+        if(isLogged()) {
+            $iLangId = $this->oDb->getLanguageId($sLang, false);
+            if(!$iLangId)
+                $iLangId = 0;
+
+            $iAccountId = getLoggedId();
+            $oAccountQuery = BxDolAccountQuery::getInstance();
+            $oAccountQuery->updateLanguage($iAccountId, $iLangId);
+        }
+
+        setcookie( 'lang', '',     time() - 60*60*24,    '/' );
+        setcookie( 'lang', $sLang, time() + 60*60*24*365, '/' );
+    }
+}
+
+if (!function_exists('_t')) {
+    function _t()
+    {
+        return call_user_func_array(array(BxDolLanguages::getInstance(), '_t'), func_get_args());
+    }
+}
+
+function _t_err()
+{
+    return call_user_func_array(array(BxDolLanguages::getInstance(), '_t_err'), func_get_args());
+}
+
+function _t_action()
+{
+    return call_user_func_array(array(BxDolLanguages::getInstance(), '_t_action'), func_get_args());
+}
+
+function _t_ext($key, $args)
+{
+    return BxDolLanguages::getInstance()->_t_ext($key, $args);
+}
+
+function _t_format_size($iSize)
+{
+    return BxDolLanguages::getInstance()->_t_format_size($iSize);
+}
+
+function _t_format_currency($fPrice)
+{
+    return BxDolLanguages::getInstance()->_t_format_currency($fPrice);
+}
+
+function _t_format_extensions($mixedExtensions)
+{
+    return BxDolLanguages::getInstance()->_t_format_extensions($mixedExtensions);
+}
+
+function bx_lang_name()
+{
+    return BxDolLanguages::getInstance()->getCurrentLanguage();
+}
+
+/** @} */

+ 386 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolLanguagesQuery.php

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

+ 236 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolLiveUpdates.php

@@ -0,0 +1,236 @@
+<?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
+ * @{
+ */
+
+/**
+ * @page objects
+ * @section live_updates Live Updates
+ * @ref BxDolLiveUpdates
+ */
+
+class BxDolLiveUpdates extends BxDolFactory implements iBxDolSingleton
+{
+	protected $_oQuery;
+    protected $_aSystems;
+    protected $_aSystemsActive;
+
+    protected $_iInterval;
+
+	protected $_iCacheTTL;  
+	protected $_sCacheKey;
+
+    protected $_sJsClass;
+    protected $_sJsObject;
+
+    protected function __construct()
+    {
+        parent::__construct();
+
+        $this->_oQuery = new BxDolLiveUpdatesQuery();
+
+        $this->_iInterval = (int)$this->_oQuery->getParam('sys_live_updates_interval');
+
+        $this->_iCacheTTL = 86400;
+        $this->_sCacheKey = 'sys_live_updates_' . bx_get_logged_profile_id();
+
+        $this->_sJsClass = 'BxDolLiveUpdates';
+    	$this->_sJsObject = 'oLiveUpdates';
+
+    	$this->_aSystemsActive = array();
+    	$this->_aSystems = $this->_getCachedSystems();
+    }
+
+    /**
+     * 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(!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;
+    }
+}
+
+/** @} */

+ 36 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolLiveUpdatesQuery.php

@@ -0,0 +1,36 @@
+<?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 BxDolLiveUpdates
+ */
+class BxDolLiveUpdatesQuery extends BxDolDb
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+	public function getSystems()
+    {
+        if(!isset($GLOBALS['bx_dol_live_updates_systems']))
+            $GLOBALS['bx_dol_live_updates_systems'] = BxDolDb::getInstance()->fromCache('sys_objects_live_updates', 'getAllWithKey', '
+                SELECT
+                    `id` as `id`,
+                    `name` AS `name`,
+                    `frequency` AS `frequency`,
+                    `service_call` AS `service_call`,
+                    `active` AS `active`
+                FROM `sys_objects_live_updates`', 'name');
+
+        return $GLOBALS['bx_dol_live_updates_systems'];
+    }
+}
+
+/** @} */

+ 278 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolMenu.php

@@ -0,0 +1,278 @@
+<?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
+ * @{
+ */
+
+/**
+ * @page objects
+ * @section menu Menu
+ * @ref BxDolMenu
+ */
+
+/**
+ * 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;
+    }
+
+    /**
+     * 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;
+    }
+
+}
+
+/** @} */

+ 76 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolMenuQuery.php

@@ -0,0 +1,76 @@
+<?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_MENU_LAST_ITEM_ORDER', 9999);
+
+/**
+ * Database queries for menus.
+ * @see BxDolMenu
+ */
+class BxDolMenuQuery extends BxDolDb
+{
+    protected $_aObject;
+
+    public function __construct($aObject)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+    }
+
+    static public function getMenuObject ($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT `o`.*, `t`.`template` FROM `sys_objects_menu` AS `o` INNER JOIN `sys_menu_templates` AS `t` ON (`t`.`id` = `o`.`template_id`) WHERE `o`.`object` = ?", $sObject);
+        $aObject = $oDb->getRow($sQuery);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        return $aObject;
+    }
+
+    static public function getMenuTriggers($sTriggerName)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_menu_items` WHERE `set_name` = ? ORDER BY `order` DESC", $sTriggerName);
+        return $oDb->getAll($sQuery);
+    }
+
+    static public function addMenuItemToSet($aMenuItem)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        if (empty($aMenuItem['set_name']))
+            return false;
+
+        if (empty($aMenuItem['order'])) {
+            $sQuery = $oDb->prepare("SELECT `order` FROM `sys_menu_items` WHERE `set_name` = ? AND `active` = 1 AND `order` != ? ORDER BY `order` DESC LIMIT 1", $aMenuItem['set_name'], BX_MENU_LAST_ITEM_ORDER);
+            $iProfileMenuOrder = (int)$oDb->getOne($sQuery);
+            $aMenuItem['order'] = $iProfileMenuOrder + 1;
+        }
+
+        $sQuery = $oDb->prepare("DELETE FROM `sys_menu_items` WHERE `set_name` = ? AND `name` = ?", $aMenuItem['set_name'], $aMenuItem['name']);
+        $oDb->query($sQuery);
+
+        unset($aMenuItem['id']);
+        return $oDb->query("INSERT INTO `sys_menu_items` SET " . $oDb->arrayToSQL($aMenuItem));
+    }
+
+    public function getMenuItems()
+    {
+        return $this->getMenuItemsFromSet($this->_aObject['set_name']);
+    }
+
+    public function getMenuItemsFromSet($sSetName)
+    {
+        $sQuery = $this->prepare("SELECT * FROM `sys_menu_items` WHERE `set_name` = ? ORDER BY `order` ASC", $sSetName);
+        return $this->getAll($sQuery);
+    }
+}
+
+/** @} */

+ 598 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolMetatags.php

@@ -0,0 +1,598 @@
+<?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);
+
+/**
+ * @page objects
+ * @section metatags Meta-tags
+ * @ref BxDolMetatags
+ */
+
+// 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;
+    }
+
+}
+
+/** @} */

+ 98 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolMetatagsQuery.php

@@ -0,0 +1,98 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Database queries for metatags objects.
+ * @see BxDolMetatag
+ */
+class BxDolMetatagsQuery extends BxDolDb
+{
+    protected $_aObject;
+
+    public function __construct($aObject)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+    }
+
+    static public function getMetatagsObject ($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_metatags` WHERE `object` = ?", $sObject);
+        $aObject = $oDb->getRow($sQuery);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        return $aObject;
+    }
+
+
+
+    public function keywordsAdd($mixedContentId, $aKeywords, $bDeletePreviousKeywords = true)
+    {
+        if ($bDeletePreviousKeywords)
+            $this->keywordsDelete($mixedContentId);
+
+        $i = 0;
+        foreach ($aKeywords as $sKeyword) {
+            $sQuery = $this->prepare("INSERT INTO `{$this->_aObject['table_keywords']}` SET `object_id` = ?, `keyword` = ?", $mixedContentId, trim($sKeyword, '#'));
+            $i += ($this->query($sQuery) ? 1 : 0);
+        }
+        return $i;
+    }
+
+    public function keywordsDelete($mixedContentId)
+    {
+        return $this->metaDelete($this->_aObject['table_keywords'], $mixedContentId);
+    }
+
+    public function keywordsGet($mixedContentId)
+    {
+        $sQuery = $this->prepare("SELECT `keyword` FROM `{$this->_aObject['table_keywords']}` WHERE `object_id` = ?", $mixedContentId);
+        return $this->getColumn($sQuery);
+    }
+
+    public function keywordsPopularList($iLimit)
+    {
+        $sQuery = $this->prepare("SELECT `keyword`, COUNT(*) as `count` FROM `{$this->_aObject['table_keywords']}` GROUP BY `keyword` ORDER BY `count` DESC LIMIT ?", $iLimit);
+        return $this->getPairs($sQuery, 'keyword', 'count');
+    }    
+
+
+
+    public function locationsAdd($mixedContentId, $fLat, $fLng, $sCountryCode, $sState, $sCity, $sZip, $sStreet, $sStreetNumber)
+    {
+        $this->locationsDelete($mixedContentId);
+        if (!$fLat && !$fLng)
+            return true;
+
+        $sQuery = $this->prepare("INSERT INTO `{$this->_aObject['table_locations']}` SET `object_id` = ?, `lat` = ?, `lng` = ?, `country` = ?, `state` = ?, `city` = ?, `zip` = ?, `street` = ?, `street_number` = ?", $mixedContentId, $fLat, $fLng, $sCountryCode, $sState, $sCity, $sZip, $sStreet, $sStreetNumber);
+        return $this->query($sQuery);
+    }
+    
+    public function locationsDelete($mixedContentId)
+    {
+        return $this->metaDelete($this->_aObject['table_locations'], $mixedContentId);
+    }
+
+    public function locationGet($mixedContentId)
+    {
+        $sQuery = $this->prepare("SELECT * FROM `{$this->_aObject['table_locations']}` WHERE `object_id` = ?", $mixedContentId);
+        return $this->getRow($sQuery);
+    }
+
+
+    protected function metaDelete($sTable, $mixedContentId)
+    {
+        $sQuery = $this->prepare("DELETE FROM `{$sTable}` WHERE `object_id` = ?", $mixedContentId);
+        return $this->query($sQuery);
+    }
+}
+
+/** @} */

+ 61 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolMinify.php

@@ -0,0 +1,61 @@
+<?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
+ * @{
+ */
+
+use MatthiasMullie\Minify;
+
+require_once(BX_DIRECTORY_PATH_PLUGINS . 'matthiasmullie/path-converter/Converter.php');
+require_once(BX_DIRECTORY_PATH_PLUGINS . 'matthiasmullie/minify/Minify.php');
+require_once(BX_DIRECTORY_PATH_PLUGINS . 'matthiasmullie/minify/CSS.php');
+require_once(BX_DIRECTORY_PATH_PLUGINS . 'matthiasmullie/minify/JS.php');
+
+class BxDolMinify extends BxDolFactory implements iBxDolSingleton
+{
+    protected function __construct()
+    {
+        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 BxDolMinify();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+	public function minifyJs($s)
+	{
+		$oMinifier = new Minify\JS();
+    	$oMinifier->add($s);
+
+    	return $oMinifier->minify();
+	}
+
+	public function minifyCss($s)
+	{
+		$oMinifier = new Minify\CSS();
+    	$oMinifier->add($s);
+
+    	return $oMinifier->minify();
+	}
+}
+
+/** @} */

+ 169 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolModule.php

@@ -0,0 +1,169 @@
+<?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
+ * @{
+ */
+
+/**
+ * Base class for Module classes in modules engine.
+ *
+ * The object of the class contains major objects of the whole module. They are:
+ * a. An object of config class
+ * @see BxDolModuleConfig
+ *
+ * b. An object of database class.
+ * @see BxDolModuleQuery
+ *
+ * c. An object of template class.
+ * @see BxDolTemplate
+ *
+ *
+ * Example of usage:
+ * refer to any BoonEx module
+ *
+ *
+ * Static Methods:
+ *
+ * Get an instance of a module's class.
+ * @see BxDolModule::getInstance($sClassName)
+ *
+ *
+ * Memberships/ACL:
+ * Doesn't depend on user's membership.
+ *
+ *
+ * Alerts:
+ * no alerts available
+ *
+ */
+class BxDolModule extends BxDol
+{
+    public $_aModule;
+    public $_oDb;
+    public $_oTemplate;
+    public $_oConfig;
+
+    /**
+     * constructor
+     */
+    public function __construct($aModule)
+    {
+        parent::__construct();
+
+        $this->_aModule = $aModule;
+
+        $sClassPrefix = $aModule['class_prefix'];
+        $sClassPath = BX_DIRECTORY_PATH_MODULES . $aModule['path'] . 'classes/';
+
+        $sClassName = $sClassPrefix . 'Config';
+        require_once($sClassPath . $sClassName . '.php');
+        $this->_oConfig = new $sClassName($aModule);
+
+        $sClassName = $sClassPrefix . 'Db';
+        require_once($sClassPath . $sClassName . '.php');
+        $this->_oDb = new $sClassName($this->_oConfig);
+
+        $sClassName = $sClassPrefix . 'Template';
+        require_once($sClassPath . $sClassName . '.php');
+        $this->_oTemplate = new $sClassName($this->_oConfig, $this->_oDb);
+        $this->_oTemplate->loadTemplates();
+    }
+
+    /**
+     * Static method to get an instance of a module's class.
+     *
+     * @param $sName module name.
+     */
+    public static function getInstance($sName)
+    {
+        if (empty($sName))
+            return null;
+
+        $aModule = BxDolModuleQuery::getInstance()->getModuleByName($sName);
+        if (empty($aModule) || !is_array($aModule))
+            return null;
+
+        $sClassName = $aModule['class_prefix'] . 'Module';
+
+        if ('system' != $sName) {
+
+            if (isset($GLOBALS['bxDolClasses'][$sClassName]))
+                return $GLOBALS['bxDolClasses'][$sClassName];
+
+            $sClassPath = BX_DIRECTORY_PATH_MODULES . $aModule['path'] . '/classes/' . $sClassName . '.php';
+            if (!file_exists($sClassPath))
+                return null;
+            require_once($sClassPath);
+
+            $GLOBALS['bxDolClasses'][$sClassName] = new $sClassName($aModule);
+            return $GLOBALS['bxDolClasses'][$sClassName];
+
+        } else {
+
+            $sClassName = 'BxTemplServices';
+            return bx_instance($sClassName, array($aModule));
+
+        }
+    }
+
+    public static function getTitle($sUri)
+    {
+        return _t(self::getTitleKey($sUri));
+    }
+
+    public static function getTitleKey($sUri)
+    {
+        return '_sys_module_' . strtolower(str_replace(' ', '_', $sUri));
+    }
+
+	/**
+     * is module enabled
+     */
+    function isEnabled()
+    {
+        return (int)$this->_aModule['enabled'] == 1;
+    }
+
+    /**
+     * get module name
+     */
+    function getName()
+    {
+        return $this->_aModule['name'];
+    }
+
+    /**
+     * Check whether user logged in or not.
+     *
+     * @return boolean result of operation.
+     */
+    public function isLogged()
+    {
+        return isLogged();
+    }
+
+    /**
+     * Get currently logged in user ID.
+     *
+     * @return integer user ID.
+     */
+    public function getUserId()
+    {
+        return getLoggedId();
+    }
+
+    /**
+     * Get currently logged in user password.
+     *
+     * @return string user password.
+     */
+    public function getUserPassword ()
+    {
+        return getLoggedPassword();
+    }
+}
+
+/** @} */

+ 129 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolModuleConfig.php

@@ -0,0 +1,129 @@
+<?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
+ * @{
+ */
+
+/**
+ * Base class for Config classes in modules engine.
+ *
+ * The object of the class contains different basic configuration settings which are necessary for all modules.
+ *
+ *
+ * Example of usage:
+ * refer to any BoonEx module
+ *
+ *
+ * Memberships/ACL:
+ * Doesn't depend on user's membership.
+ *
+ *
+ * Alerts:
+ * no alerts available
+ *
+ */
+
+class BxDolModuleConfig extends BxDol
+{
+    protected $_iId;
+
+    protected $_sName;
+
+    protected $_sVendor;
+
+    protected $_sClassPrefix;
+
+    protected $_sDbPrefix;
+
+    protected $_sDirectory;
+
+    protected $_sUri;
+
+    protected $_sHomePath;
+
+    protected $_sClassPath;
+
+    protected $_sHomeUrl;
+
+    /**
+     * constructor
+     */
+    public function __construct($aModule)
+    {
+        parent::__construct();
+
+        $this->_iId = empty($aModule['id']) ? 0 : (int)$aModule['id'];
+        $this->_sName = isset($aModule['name']) ? $aModule['name'] : '';
+        $this->_sVendor = $aModule['vendor'];
+        $this->_sClassPrefix = $aModule['class_prefix'];
+        $this->_sDbPrefix = $aModule['db_prefix'];
+
+        $this->_sDirectory = $aModule['path'];
+        $this->_sHomePath = BX_DIRECTORY_PATH_MODULES . $this->_sDirectory;
+        $this->_sClassPath = $this->_sHomePath . 'classes/';
+
+        $this->_sUri = $aModule['uri'];
+        $this->_sHomeUrl = BX_DOL_URL_MODULES . $this->_sDirectory;
+    }
+    function getId()
+    {
+        return $this->_iId;
+    }
+    function getName()
+    {
+        return $this->_sName;
+    }
+    function getClassPrefix()
+    {
+        return $this->_sClassPrefix;
+    }
+    function getDbPrefix()
+    {
+        return $this->_sDbPrefix;
+    }
+    function getDirectory()
+    {
+        return $this->_sDirectory;
+    }
+    function getHomePath()
+    {
+        return $this->_sHomePath;
+    }
+    function getClassPath()
+    {
+        return $this->_sClassPath;
+    }
+    /**
+     * Get unique URI.
+     *
+     * @return string with unique URI.
+     */
+    function getUri()
+    {
+        return $this->_sUri;
+    }
+    /**
+     * Get base URI which depends on the Permalinks mechanism.
+     *
+     * example /modules/?r=module_uri or /m/module_uri
+     * @return string with base URI.
+     */
+    function getBaseUri()
+    {
+        return BxDolPermalinks::getInstance()->permalink('modules/?r=' . $this->_sUri . '/');
+    }
+    /**
+     * Get full URL.
+     *
+     * @return string with full URL.
+     */
+    function getHomeUrl()
+    {
+        return $this->_sHomeUrl;
+    }
+}
+
+/** @} */

+ 37 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolModuleDb.php

@@ -0,0 +1,37 @@
+<?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
+ * @{
+ */
+
+/**
+ * Base class for all modules DB classes
+ */
+class BxDolModuleDb extends BxDolModuleQuery
+{
+    protected $_sPrefix;
+
+    /**
+     * Constructor
+     */
+    public function __construct($oConfig)
+    {
+        parent::__construct();
+
+        if (is_a($oConfig,'BxDolModuleConfig'))
+            $this->_sPrefix = $oConfig->getDbPrefix();
+        else
+            trigger_error ('It is impossible to create BxDolModuleDb class instance without prefix: ' . get_class($this), E_USER_ERROR);
+    }
+
+    public function getPrefix()
+    {
+        return $this->_sPrefix;
+    }
+
+}
+
+/** @} */

+ 204 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolModuleQuery.php

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

+ 102 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/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', 'addLocation', 'removeLocation', '', $mixedFiles, $bDynamic, true);
+    }
+
+    function addJs($mixedFiles, $bDynamic = false)
+    {
+        return $this->_addFiles(BxDolTemplate::getInstance(), 'addJs', 'addLocationJs', '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', 'addLocation', 'removeLocation', '', $mixedFiles, $bDynamic, $bSearchInModule);
+    }
+
+    function addStudioJs($mixedFiles, $bDynamic = false, $bSearchInModule = true)
+    {
+        return $this->_addFiles(BxDolStudioTemplate::getInstance(), 'addJs', 'addLocationJs', 'removeLocationJs', 'js/', $mixedFiles, $bDynamic, $bSearchInModule);
+    }
+
+    function _addFiles($oTemplate, $sFuncAddFiles, $sFuncAddLocation, $sFuncRemoveLocation, $sPath, $mixedFiles, $bDynamic = false, $bSearchInModule = true)
+    {
+        $sLocationKey = $bSearchInModule ? $oTemplate->$sFuncAddLocation($this->_oConfig->getName(), $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;
+        }
+    }
+}
+
+/** @} */

+ 288 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolObject.php

@@ -0,0 +1,288 @@
+<?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
+ * @{
+ */
+
+/**
+ * Base class for all "Object" classes.
+ * Child classes usually represents high level programming constructions to generate ready 'objects' functionality, like Comments, Votings, Forms.
+ */
+class BxDolObject extends BxDolFactory implements iBxDolReplaceable
+{
+    protected $_oTemplate = null;
+	protected $_oQuery = null;
+
+    protected $_iId = 0; ///< item id the action to be performed with
+    protected $_sSystem = ''; ///< current system name
+    protected $_aSystem = array(); ///< current system array
+
+    protected $_aMarkers = array ();
+
+    protected function __construct($sSystem, $iId, $iInit = true, $oTemplate = false)
+    {
+        parent::__construct();
+
+        $aSystems = $this->getSystems();
+        if(!isset($aSystems[$sSystem]))
+            return;
+
+        $this->_sSystem = $sSystem;
+        $this->_aSystem = $aSystems[$sSystem];
+
+        if ($oTemplate)
+            $this->_oTemplate = $oTemplate;
+        else
+            $this->_oTemplate = BxDolTemplate::getInstance();
+
+        if(!$this->isEnabled())
+            return;
+
+        if($iInit)
+            $this->init($iId);
+    }
+
+    public function init($iId)
+    {
+        if(!$this->isEnabled())
+            return false;
+
+        if(empty($this->_iId) && $iId)
+            $this->setId($iId);
+
+		$this->addMarkers(array(
+            'object_id' => $this->getId(),
+            'user_id' => $this->_getAuthorId()
+        ));
+
+        return true;
+    }
+
+    public function getSystemId()
+    {
+        return $this->_aSystem['id'];
+    }
+
+    public function getSystemName()
+    {
+        return $this->_sSystem;
+    }
+
+    public function getSystemInfo()
+    {
+        return $this->_aSystem;
+    }
+
+    public function getId()
+    {
+        return $this->_iId;
+    }
+
+    public function setId($iId)
+    {
+        if($iId == $this->getId())
+            return;
+
+        $this->_iId = $iId;
+    }
+
+    public function isEnabled ()
+    {
+        return $this->_aSystem && (int)$this->_aSystem['is_on'] == 1;
+    }
+
+	/**
+	 * Interface functions for outer usage
+	 */
+    public function getConditions($sMainTable, $sMainField)
+    {
+        if(!$this->isEnabled())
+            return array();
+
+        $sTable = isset($this->_aSystem['table_main']) ? $this->_aSystem['table_main'] : '';
+        if(empty($sTable) || empty($sMainTable) || empty($sMainField))
+            return array();
+
+        return array(
+            'join' => array (
+                'objects_' . $this->_sSystem => array(
+                    'type' => 'INNER',
+                    'table' => $sTable,
+                    'mainTable' => $sMainTable,
+                    'mainField' => $sMainField,
+                    'onField' => 'object_id',
+                    'joinFields' => array('count'),
+                ),
+            ),
+        );
+    }
+
+    public function getConditionsTrack($sMainTable, $sMainField, $iAuthorId = 0)
+    {
+        if(!$this->isEnabled())
+            return array();
+
+        $sTableTrack = isset($this->_aSystem['table_track']) ? $this->_aSystem['table_track'] : '';
+        if(empty($sTableTrack) || empty($sMainTable) || empty($sMainField))
+            return array();
+
+        return array(
+            'restriction' => array (
+                'objects_' . $this->_sSystem . '_author' => array(
+                    'value' => $iAuthorId,
+                    'field' => 'author_id',
+                    'operator' => '=',
+                    'table' => $sTableTrack,
+                ),
+            ),
+            'join' => array (
+                'objects_' . $this->_sSystem => array(
+                    'type' => 'INNER',
+                    'table' => $sTableTrack,
+                    'mainTable' => $sMainTable,
+                    'mainField' => $sMainField,
+                    'onField' => 'object_id',
+                    'joinFields' => array('author_id'),
+                ),
+            ),
+
+        );
+    }
+    
+	public function getSqlParts($sMainTable, $sMainField)
+    {
+        if(!$this->isEnabled())
+            return array();
+
+        return $this->_oQuery->getSqlParts($sMainTable, $sMainField);
+    }
+
+    public function getSqlPartsTrack($sMainTable, $sMainField, $iAuthorId = 0)
+    {
+        if(!$this->isEnabled())
+            return array();
+
+        return $this->_oQuery->getSqlPartsTrack($sMainTable, $sMainField, $iAuthorId);
+    }
+
+    public function addMarkers($aMarkers)
+    {
+        if(empty($aMarkers) || !is_array($aMarkers))
+			return false;
+
+        $this->_aMarkers = array_merge($this->_aMarkers, $aMarkers);
+        return true;
+    }
+
+	/**
+     * 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] : '';
+    }
+
+	/**
+     * Actions' response functions
+     */
+	public function onObjectDelete($iObjectId = 0)
+    {
+        $this->_oQuery->deleteObjectEntries($iObjectId ? $iObjectId : $this->getId());
+    }
+
+	/**
+     * 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)
+    {
+    	if($iAuthorId == 0)
+    		return BxDolProfileUndefined::getInstance();
+
+        $oProfile = BxDolProfile::getInstance($iAuthorId);
+        if(!$oProfile)
+			$oProfile = BxDolProfileUndefined::getInstance();
+
+        return $oProfile;
+    }
+
+    /**
+     * Update Trigger table using data which is automatically gotten from object's internal table.
+     */
+	protected function _trigger()
+    {
+        if(!$this->_aSystem['trigger_table'])
+            return false;
+
+        $iId = $this->getId();
+        if(!$iId)
+            return false;
+
+        return $this->_oQuery->updateTriggerTable($iId);
+    }
+
+    /**
+     * Update (increment/decrement) Trigger table using provided value.
+     */
+    protected function _triggerValue($iValue)
+    {
+        if(!$this->_aSystem['trigger_table'])
+            return false;
+
+        $iId = $this->getId();
+        if(!$iId)
+            return false;
+
+        return $this->_oQuery->updateTriggerTableValue($iId, $iValue);
+    }
+
+    /**
+     * 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);
+    }
+}
+
+/** @} */

+ 137 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolObjectQuery.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    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * @see BxDolObject
+ */
+class BxDolObjectQuery extends BxDolDb
+{
+    protected $_oModule;
+
+    protected $_sTable;
+    protected $_sTableTrack;
+    protected $_sTriggerTable;
+    protected $_sTriggerFieldId;
+    protected $_sTriggerFieldAuthor;
+    protected $_sTriggerFieldCount;
+
+    protected $_sMethodGetEntry;
+
+    public function __construct(&$oModule)
+    {
+        parent::__construct();
+
+        $this->_oModule = $oModule;
+
+        $aSystem = $this->_oModule->getSystemInfo();
+        $this->_sTable = isset($aSystem['table_main']) ? $aSystem['table_main'] : '';
+        $this->_sTableTrack = isset($aSystem['table_track']) ? $aSystem['table_track'] : '';
+        $this->_sTriggerTable = isset($aSystem['trigger_table']) ? $aSystem['trigger_table'] : '';
+        $this->_sTriggerFieldId = isset($aSystem['trigger_field_id']) ? $aSystem['trigger_field_id'] : '';
+        $this->_sTriggerFieldAuthor = isset($aSystem['trigger_field_author']) ? $aSystem['trigger_field_author'] : '';
+        $this->_sTriggerFieldCount = isset($aSystem['trigger_field_count']) ? $aSystem['trigger_field_count'] : '';
+    }
+
+    /**
+     * Get SQL parts for main table. 
+     */
+    public function getSqlParts($sMainTable, $sMainField)
+    {
+        if(empty($this->_sTable) || empty($sMainTable) || empty($sMainField))
+            return array();
+
+        return array (
+            'fields' => ", `{$this->_sTable}`.`count` as `count` ",
+            'join' => " LEFT JOIN `{$this->_sTable}` ON (`{$this->_sTable}`.`object_id` = `{$sMainTable}`.`{$sMainField}`) ",
+        );
+    }
+
+    /**
+     * Get SQL parts for track table. 
+     */
+    public function getSqlPartsTrack($sMainTable, $sMainField, $iAuthorId = 0)
+    {
+        if(empty($this->_sTableTrack) || empty($sMainTable) || empty($sMainField))
+            return array();
+
+        return array (
+            'fields' => ", `{$this->_sTableTrack}`.`author_id` as `favorer_id` ",
+            'where' => $this->prepareAsString(" AND `{$this->_sTableTrack}`.`author_id` = ?", $iAuthorId),
+            'join' => " LEFT JOIN `{$this->_sTableTrack}` ON (`{$this->_sTableTrack}`.`object_id` = `{$sMainTable}`.`{$sMainField}`) ",
+        );
+    }
+
+	public function isPerformed($iObjectId, $iAuthorId)
+    {
+        $sQuery = $this->prepare("SELECT `object_id` FROM `{$this->_sTableTrack}` WHERE `object_id` = ? AND `author_id` = ? LIMIT 1", $iObjectId, $iAuthorId);
+        return (int)$this->getOne($sQuery) != 0;
+    }
+
+    public function getPerformedBy($iObjectId)
+    {
+        $sQuery = $this->prepare("SELECT `author_id` FROM `{$this->_sTableTrack}` WHERE `object_id`=?", $iObjectId);
+        return $this->getColumn($sQuery);
+    }
+
+    public function deleteObjectEntries($iObjectId)
+    {
+    	if(!empty($this->_sTable)) {
+	        $sQuery = $this->prepare("DELETE FROM {$this->_sTable} WHERE `object_id` = ?", $iObjectId);
+	        if($this->query($sQuery))
+	        	$this->query("OPTIMIZE TABLE {$this->_sTable}");
+    	}
+
+    	if(!empty($this->_sTableTrack)) {
+	        $sQuery = $this->prepare("DELETE FROM {$this->_sTableTrack} WHERE `object_id` = ?", $iObjectId);
+	        if($this->query($sQuery))
+	        	$this->query ("OPTIMIZE TABLE {$this->_sTableTrack}");
+    	}
+    }
+
+	public function getObjectAuthorId($iId)
+    {
+        if(empty($this->_sTriggerFieldAuthor))
+            return 0;
+
+        $sQuery = $this->prepare("SELECT `{$this->_sTriggerFieldAuthor}` FROM `{$this->_sTriggerTable}` WHERE `{$this->_sTriggerFieldId}` = ? LIMIT 1", $iId);
+        return (int)$this->getOne($sQuery);
+    }
+
+    public function getObjectCount($iId)
+    {
+        $sQuery = $this->prepare("SELECT `{$this->_sTriggerFieldCount}` FROM `{$this->_sTriggerTable}` WHERE `{$this->_sTriggerFieldId}` = ? LIMIT 1", $iId);
+        return (int)$this->getOne($sQuery);
+    }
+
+    public function updateTriggerTable($iObjectId)
+    {
+    	if(empty($this->_sMethodGetEntry))
+    		return false;
+
+        $aEntry = $this->{$this->_sMethodGetEntry}($iObjectId);
+        if(empty($aEntry) || !is_array($aEntry))
+            return false;
+
+        return $this->_updateTriggerTable($iObjectId, $aEntry);
+    }
+
+    public function updateTriggerTableValue($iObjectId, $iValue)
+    {
+        $sQuery = $this->prepare("UPDATE `{$this->_sTriggerTable}` SET `{$this->_sTriggerFieldCount}` = `{$this->_sTriggerFieldCount}` + ? WHERE `{$this->_sTriggerFieldId}` = ?", (int)$iValue, $iObjectId);
+        return (int)$this->query($sQuery) > 0;
+    }
+
+    protected function _updateTriggerTable($iObjectId, $aEntry)
+    {
+    	$sQuery = $this->prepare("UPDATE `{$this->_sTriggerTable}` SET `{$this->_sTriggerFieldCount}` = ? WHERE `{$this->_sTriggerFieldId}` = ?", $aEntry['count'], $iObjectId);
+        return (int)$this->query($sQuery) > 0;
+    }
+}
+
+/** @} */

+ 265 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolPage.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
+ * @{
+ */
+
+/**
+ * @page objects
+ * @section page Page
+ * @ref BxDolPage
+ */
+
+/**
+ * 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']);
+    }
+
+}
+
+/** @} */

+ 93 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolPageQuery.php

@@ -0,0 +1,93 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * Database queries for pages.
+ * @see BxDolPage
+ */
+class BxDolPageQuery extends BxDolDb
+{
+    protected $_aObject;
+
+    public function __construct($aObject)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+    }
+
+    static public function getPageObject ($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT `o`.*, `l`.`template`, `l`.`cells_number` FROM `sys_objects_page` AS `o` INNER JOIN `sys_pages_layouts` AS `l` ON (`l`.`id` = `o`.`layout_id`) WHERE `o`.`object` = ?", $sObject);
+        $aObject = $oDb->getRow($sQuery);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        return $aObject;
+    }
+
+    static public function getPageObjectNameByURI($sURI)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT `object` FROM `sys_objects_page` WHERE `uri` = ?", $sURI);
+        return $oDb->getOne($sQuery);
+    }
+
+	static public function getPageTriggers($sTriggerName)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_pages_blocks` WHERE `object` = ? ORDER BY `id` ASC", $sTriggerName);
+        return $oDb->getAll($sQuery);
+    }
+
+    static public function addPageBlockToPage($aPageBlock)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        if (empty($aPageBlock['object']))
+            return false;
+
+        if (empty($aPageBlock['order'])) {
+        	$iCellId = !empty($aPageBlock['cell_id']) ? (int)$aPageBlock['cell_id'] : 1;
+            $sQuery = $oDb->prepare("SELECT `order` FROM `sys_pages_blocks` WHERE `object` = ? AND `cell_id` = ? AND `active` = 1 ORDER BY `order` DESC LIMIT 1", $aPageBlock['object'], $iCellId);
+            $aPageBlock['order'] = (int)$oDb->getOne($sQuery) + 1;
+        }
+
+        $sQuery = $oDb->prepare("DELETE FROM `sys_pages_blocks` WHERE `object` = ? AND `type` = ? AND `title` = ?", $aPageBlock['object'], $aPageBlock['type'], $aPageBlock['title']);
+        $oDb->query($sQuery);
+
+        unset($aPageBlock['id']);
+        return $oDb->query("INSERT INTO `sys_pages_blocks` SET " . $oDb->arrayToSQL($aPageBlock));
+    }
+
+    public function getPageBlocks()
+    {
+        $aRet = array ();
+        for ($i = 1 ; $i <= $this->_aObject['cells_number'] ; ++$i) {
+            $sQuery = $this->prepare("SELECT * FROM `sys_pages_blocks` WHERE `object` = ? AND `cell_id` = ? AND `active` = 1 ORDER BY `order` ASC", $this->_aObject['object'], $i);
+            $aRet['cell_'.$i] = $this->getAll($sQuery);
+        }
+        return $aRet;
+    }
+
+    public function getPageBlock($iBlockId)
+    {
+        $sQuery = $this->prepare("SELECT * FROM `sys_pages_blocks` WHERE `object` = ? AND `id` = ?", $this->_aObject['object'], $iBlockId);
+        return $this->getRow($sQuery);
+    }
+
+    public function getPageBlockContent($iId)
+    {
+        $sQuery = $this->prepare("SELECT `content` FROM `sys_pages_blocks` WHERE `id` = ?", $iId);
+        return $this->getOne($sQuery);
+    }
+
+}
+
+/** @} */

+ 358 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolPaginate.php

@@ -0,0 +1,358 @@
+<?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_PAGINATE_PER_PAGE_DEFAULT', 10);
+
+/**
+ * Paginage for any content.
+ * It is used to create paginate, configuring it via input parameters.
+ * Paginate don't support total number of pages, moreover it is not recommended to count all records - it slows down the site.
+ * To correctly determine last page we need to pass number of available records on the current page plus one - so we always know if next page is available.
+ *
+ *
+ * Two types of paginate presentation is supported:
+ * - getPaginate() - to get default paginate, it is better to use it on the whole page.
+ * - getSimplePaginate() - to get limited paginate, it is better to use in some boxes, where availabel space is limited or for ajax paginate.
+ *
+ *
+ * The list of available input parameters:
+ *
+ *
+ * Parameters:
+ * - start - position of the first item.
+ * - num - number of available items on the page, it should be number of items per page + 1 (+1 is needed to correctly determine last page). It is possible to set this value automatically @see setNumFromDataArray.
+ * - per_page - number of items displayed on the page.
+ * - page_url - page URL to go through pages, special markers are automatically replaced.
+ * - on_change_page - JavaScript code to be called on change page.
+ * - info - display info.
+ * - view_all_url - URL for 'view all' page. This url is not showed by default. It is convinient to use with @see getSimplePaginate.
+ * - view_all_caption - optional caption for 'view all' link.
+ *
+ *
+ * Available markers to replace in 'page_url' and 'on_change_page' parameters:
+ * - {per_page} - current number of items to display per page.
+ * - {start} - the number to display items starting from.
+ *
+ *
+ * Example of usage:
+ * @code
+ * $oPaginate = new BxDolPaginate(array(
+ *      'start' => 0,
+ *      'num' => 11,
+ *      'per_page' => 10,
+ *      'on_change_page' => 'changePage({start}, {per_page})'
+ * ));
+ * $oPaginate->getPaginate();
+ * @endcode
+ *
+ *
+ * Memberships/ACL:
+ * Doesn't depend on user's membership.
+ *
+ *
+ * Alerts:
+ * no alerts available
+ *
+ */
+abstract class BxDolPaginate extends BxDol
+{
+    protected static $_isCssAdded = false;
+
+    protected $_oTemplate;
+
+    protected $_iStart; ///< start display items from this number
+    protected $_iNum; ///< available results, you need to query per_page + 1 results, so paginate can determine last page
+    protected $_iPerPage; ///< results per page
+
+    protected $_sPageUrl; ///< page url of next/prev page, special markers will be replaced here automatically
+    protected $_sOnChangePage; ///< on click of next/prev page, special markers will be replaced here automatically
+    protected $_bInfo; ///< show displayed items info
+
+    protected $_sViewAllUrl; ///< view "all results" url, for "simple" paginate
+    protected $_sViewAllCaption; ///< 'view all' link caption
+
+    protected $_sButtonsClass; ///< add this class to buttons class attribute
+    protected $_sPaginateClass; ///< add this class to whole paginate container div
+
+    /**
+     * Constructor
+     */
+    public function __construct($aParams, $oTemplate = null)
+    {
+        parent::__construct();
+
+        if ($oTemplate)
+            $this->_oTemplate = $oTemplate;
+        else
+            $this->_oTemplate = BxDolTemplate::getInstance();
+
+        if (isset($aParams['count']))
+           trigger_error ('Paginate "count" is deprecated - use "num" instead: ' . get_class($this), E_USER_ERROR);
+
+        //--- Main settings ---//
+        $this->_iStart = isset($aParams['start']) && (int)$aParams['start'] > 0 ? (int)$aParams['start'] : 0;
+        $this->_iNum = isset($aParams['num']) ? (int)$aParams['num'] : 0;
+        $this->_iPerPage = isset($aParams['per_page']) && (int)$aParams['per_page'] > 0 ? (int)$aParams['per_page'] : BX_DOL_PAGINATE_PER_PAGE_DEFAULT;
+
+        $this->_bInfo = isset($aParams['info']) ? (bool)$aParams['info'] : true;
+        $this->_sButtonsClass = isset($aParams['buttons_class']) ? $aParams['buttons_class'] : '';
+        $this->_sViewAllUrl = isset($aParams['view_all_url']) ? $aParams['view_all_url'] : false;
+        $this->_sViewAllCaption = isset($aParams['view_all_caption']) ? $aParams['view_all_caption'] : _t('_sys_paginate_view_all');
+
+        $this->_sPaginateClass = isset($aParams['paginate_class']) ? $aParams['paginate_class'] : '';
+
+        // page url
+        $this->_sPageUrl = isset($aParams['page_url']) ? $aParams['page_url'] : BX_DOL_URL_ROOT;
+
+        // on click (js mode)
+        $this->_sOnChangePage = isset($aParams['on_change_page']) ? $aParams['on_change_page'] : '';
+    }
+
+    /**
+     * Get number of available results per page. If it is not last page,
+     * then this number is number of result per page plus one.
+     * @return integer
+     */
+    public function getNum()
+    {
+        return $this->_iNum;
+    }
+
+    /**
+     * Set number of available items on the page directly from data array.
+     * Since data array should contain additional record - we will pop last item from array automatically.
+     * @return nothing.
+     */
+    public function setNumFromDataArray(&$a, $isAutoPopLastElement = true)
+    {
+        if ($a && is_array($a))
+            $this->_iNum = count($a);
+        else
+            $this->_iNum = 0;
+
+        if ($this->_iNum > $this->_iPerPage && $isAutoPopLastElement)
+            array_pop($a);
+    }
+
+    /**
+     * Set number of available items on the page,
+     * it should be number of items per page + 1 (+1 is needed to correctly determine last page).
+     * It is possible to set this value automatically @see setNumFromDataArray.
+     * @param $i positive integer.
+     * @return true on success or false if $i param isn't correct.
+     */
+    public function setNum($i)
+    {
+        if ($i >= 0) {
+            $this->_iNum = (int)$i;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Position, the data is showing from.
+     * @return integer.
+     */
+    public function getStart()
+    {
+        return $this->_iStart;
+    }
+
+    /**
+     * Set the starting position, the data is showing from.
+     * @param $i positive integer.
+     * @return true on siccess or false on error.
+     */
+    public function setStart($i)
+    {
+        if ($i >= 0) {
+            $this->_iStart = (int)$i;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Get number of records per page.
+     * @return integer.
+     */
+    public function getPerPage()
+    {
+        return $this->_iPerPage;
+    }
+
+    /**
+     * Set number of records per page.
+     * @param $i positive integer.
+     * @return integer.
+     */
+    public function setPerPage($i)
+    {
+        if ((int)$i > 0) {
+            $this->_iPerPage = $iPerPage;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Set string to pass to 'onclick' for change page button.
+     * The following markers are replaced automatically:
+     * - {per_page}
+     * - {start}
+     *
+     * @param $i positive integer.
+     * @return integer.
+     */
+    public function setOnChangePage($s)
+    {
+        $this->_sOnChangePage = $s;
+    }
+
+    /**
+     * @return true if previous page is available, or false if not
+     */
+    public function isPrevAvail ()
+    {
+        return $this->_iStart > 0 ? true : false;
+    }
+
+    /**
+     * @return true if next page is available, or false if not
+     */
+    public function isNextAvail ()
+    {
+        return $this->_iNum > $this->_iPerPage ? true : false;
+    }
+
+    /**
+     * Get default paginate, it is better to use it on the whole page.
+     * @param $iStart - @see setStart.
+     * @param $iNum - @see setNum and @see setNumFromDataArray.
+     * @param $iPerPage - @see setPerPage.
+     * @return HTML string.
+     */
+    public function getPaginate($iStart = -1, $iNum = -1, $iPerPage = -1)
+    {
+        $this->setNum($iNum);
+        if (!$this->_iNum)
+            return '';
+
+        $this->setStart($iStart);
+        $this->setPerPage($iPerPage);
+
+        if (0 == $this->getStart() && !$this->isNextAvail ())
+            return '';
+
+        $aReplacement = $this->_getReplacement();
+
+        //--- Previous Page button ---//
+        $sPrevLnkUrl = 'javascript:void(0);';
+        $sPrevLnkClick = '';
+        $sPrevClassAdd = ' bx-btn-disabled';
+        if ($this->isPrevAvail()) {
+            $iStartPrev = $this->_iStart - $this->_iPerPage > 0 ? $this->_iStart - $this->_iPerPage : 0;
+            $aReplacementLink = array_merge($aReplacement, array('start' => $iStartPrev));
+            $sPrevLnkUrl = $this->_getPageChangeUrl($aReplacementLink);
+            $sPrevLnkClick = $this->_getPageChangeOnClick($aReplacementLink);
+            $sPrevClassAdd = '';
+        }
+
+        //--- Next Page button ---//
+        $sNextLnkUrl = 'javascript:void(0);';
+        $sNextLnkClick = '';
+        $sNextClassAdd = ' bx-btn-disabled';
+        if ($this->isNextAvail()) {
+            $aReplacementLink = array_merge($aReplacement, array('start' => $this->_iStart + $this->_iPerPage));
+            $sNextLnkUrl = $this->_getPageChangeUrl($aReplacementLink);
+            $sNextLnkClick = $this->_getPageChangeOnClick($aReplacementLink);
+            $sNextClassAdd = '';
+        }
+
+        $sClassAdd = ($this->_sButtonsClass ? ' ' . $this->_sButtonsClass : '');
+
+        $aVariables = array(
+            'bx_if:info' => array (
+                'condition' => (bool)$this->_bInfo,
+                'content' => array(
+                    'text' => _t('_sys_paginate_info', $this->_iStart + 1, $this->_iStart + ($this->isNextAvail () ? $this->_iPerPage : $this->_iNum)),
+                ),
+            ),
+            'bx_if:view_all' => array(
+                'condition' => (bool)$this->_sViewAllUrl,
+                'content' => array(
+                    'lnk_url' => $this->_sViewAllUrl,
+                    'lnk_title' => $this->_sViewAllCaption,
+                    'lnk_content' => $this->_sViewAllCaption,
+                ),
+            ),
+            'btn_prev' => '<a href="' . $sPrevLnkUrl . '" ' . $sPrevLnkClick . ' class="bx-paginate-btn-prev bx-btn' . $sClassAdd . $sPrevClassAdd . '"><i class="sys-icon sys-icon-bigger angle-double-left"></i></a>',
+            'btn_next' => '<a href="' . $sNextLnkUrl . '" ' . $sNextLnkClick . ' class="bx-paginate-btn-next bx-btn' . $sClassAdd . $sNextClassAdd . '"><i class="sys-icon sys-icon-bigger angle-double-right"></i></a>',
+            'paginate_class' => $this->_sPaginateClass,
+			'bx_repeat:attrs' => array(
+                array('key' => 'bx-data-start', 'value' => $this->_iStart),
+                array('key' => 'bx-data-perpage', 'value' => $this->_iPerPage)
+            )
+        );
+
+        $this->addCssJs();
+        return $this->_oTemplate->parseHtmlByName('paginate.html', $aVariables);
+    }
+
+    /**
+     * Get limited paginate, it is better to use in some boxes, where availabel space is limited or for ajax paginate.
+     * @param $sViewAllUrl - url to page for 'view all' link.
+     * @param $iStart - @see setStart.
+     * @param $iNum - @see setNum and @see setNumFromDataArray.
+     * @param $iPerPage - @see setPerPage.
+     * @return HTML string.
+     */
+    public function getSimplePaginate($sViewAllUrl = '', $iStart = -1, $iNum = -1, $iPerPage = -1)
+    {
+        if ($sViewAllUrl)
+            $this->_sViewAllUrl = $sViewAllUrl;
+
+        $this->_bInfo = false;
+        $this->_sButtonsClass .= ($this->_sButtonsClass ? ' ' : '') . 'bx-btn-small bx-btn-symbol-small';
+        $this->_sPaginateClass = 'bx-paginate-simple';
+
+        return $this->getPaginate($iStart, $iNum, $iPerPage);
+    }
+
+    public function addCssJs ()
+    {
+        if (self::$_isCssAdded)
+            return false;
+        $this->_oTemplate->addCss('paginate.css');
+        self::$_isCssAdded = true;
+        return true;
+    }
+
+    protected function _getReplacement()
+    {
+        return array(
+            'start' => $this->_iStart,
+            'per_page' => $this->_iPerPage,
+        );
+    }
+
+    protected function _getPageChangeUrl($aReplacement)
+    {
+        return $this->_oTemplate->parseHtmlByContent($this->_sPageUrl, $aReplacement, array('{', '}'));
+    }
+
+    protected function _getPageChangeOnClick($aReplacement)
+    {
+        return !empty($this->_sOnChangePage) ? 'onclick="javascript:' . $this->_oTemplate->parseHtmlByContent($this->_sOnChangePage, $aReplacement, array('{', '}')) . '; return false;"' : '';
+    }
+
+}
+
+/** @} */

+ 284 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolPayments.php

@@ -0,0 +1,284 @@
+<?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_new', 'Orders'))
+    		return array();
+
+		$aSrvParams = array($sType);
+		return BxDolService::call($this->_sActive, 'get_orders_count_new', $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);
+    }
+}
+
+/** @} */

+ 32 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolPaymentsQuery.php

@@ -0,0 +1,32 @@
+<?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 BxDolPayments
+ */
+class BxDolPaymentsQuery extends BxDolDb
+{
+	public function __construct()
+    {
+    	parent::__construct();
+    }
+
+    public function getObjects()
+    {
+    	$sQuery = $this->prepare("SELECT * FROM `sys_objects_payments` WHERE 1");
+    	
+        $aObjects = $this->getAll($sQuery);
+        if(empty($aObjects) || !is_array($aObjects))
+            return array();
+
+        return $aObjects;
+    }
+}
+
+/** @} */

+ 259 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolPermalinks.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
+ * @{
+ */
+
+/**
+ * @page objects
+ * @section permalinks Permalinks
+ * @ref BxDolPermalinks
+ */
+
+/**
+ * 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;
+
+            return bx_append_url_params($this->aLinksStandard[$sKey]['permalink'] . $sPage, $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 _fixUrl($sLink)
+    {
+        if (strncmp($sLink, 'modules/index.php?r=', 20) === 0)
+            $sLink = str_replace('modules/index.php?r=', 'modules/?r=', $sLink);
+        return $sLink;
+    }
+}
+
+/** @} */

+ 339 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolPrivacy.php

@@ -0,0 +1,339 @@
+<?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);
+
+/**
+ * @page objects
+ * @section privacy Privacy
+ * @ref BxDolPrivacy
+ */
+
+/**
+ * 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->_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 = array();
+        $aGroups = $oPrivacy->_oDb->getGroupsBy(array('type' => 'active'));
+        foreach($aGroups as $aGroup) {
+            if((int)$aGroup['active'] == 0)
+               continue;
+
+            $aValues[] = array('key' => $aGroup['id'], 'value' => _t($aGroup['title']));
+        }
+
+        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);
+    }
+
+	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;
+    }
+}
+
+/** @} */

+ 186 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolPrivacyQuery.php

@@ -0,0 +1,186 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    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($aObject = array())
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+
+        if(!empty($this->_aObject)) {
+            $this->_sTable = $this->_aObject['table'];
+            $this->_sFieldId = $this->_aObject['table_field_id'];
+            $this->_sFieldOwnerId = $this->_aObject['table_field_author'];
+        }
+
+        $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;
+    }
+
+    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);
+    }
+}
+
+/** @} */

+ 484 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolProfile.php

@@ -0,0 +1,484 @@
+<?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']));
+    }
+
+    /**
+     * 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);
+        }
+    }
+}
+
+/** @} */

+ 45 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolProfileForms.php

@@ -0,0 +1,45 @@
+<?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
+ * @{
+ */
+
+/**
+ * Functions to automate profile creation/editing/deletion forms.
+ */
+class BxDolProfileForms extends BxDolFactory
+{
+    /**
+     * Constructor
+     */
+    protected function __construct ()
+    {
+        parent::__construct ();
+    }
+
+    protected function _redirectAndExit ($sUrl, $isPermalink = true, $aMarkers = false)
+    {
+        if ($isPermalink)
+            $sUrl = BxDolPermalinks::getInstance()->permalink($sUrl);
+
+        header('Location: ' . BX_DOL_URL_ROOT . $this->_replaceMarkers($sUrl, $aMarkers));
+        exit;
+    }
+
+    /**
+     * Replace provided markers in a string, markers are surrounded by '{' and '}' signs, for example: a.php?x={id}
+     * @param $mixed string or array to replace markers in
+     * @param $aMarkers araay or markers for replacement, for example: array ('num' => 123, 'name' => 'Ivan');
+     * @return string where all markers are replaced
+     */
+    protected function _replaceMarkers ($mixed, $aMarkers)
+    {
+        return bx_replace_markers($mixed, $aMarkers);
+    }
+
+}
+
+/** @} */

+ 261 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolProfileQuery.php

@@ -0,0 +1,261 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * All queries related to profiles
+ */
+class BxDolProfileQuery extends BxDolDb implements iBxDolSingleton
+{
+    protected function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolProfileQuery();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    /**
+     * Get all account profiles.
+     * @param  string  $iAccountId account id
+     * @return profile array
+     */
+    public function getProfilesByAccount ($iAccountId)
+    {
+        if (!$iAccountId)
+            return array();
+
+        $oAccountQuery = BxDolAccountQuery::getInstance();
+        $aAccountInfo = $oAccountQuery->getInfoById($iAccountId);
+
+        $sSql = $this->prepare("SELECT * FROM `sys_profiles` WHERE `account_id` = ? ORDER BY `id` = ? DESC", $iAccountId, $aAccountInfo['profile_id']);
+        return $this->getAllWithKey($sSql, 'id');
+    }
+
+    /**
+     * Get profile by content id, type and account.
+     * @param  string $iAccountId account id
+     * @return array  if aprofile ids, key is profile id
+     */
+    public function getProfileByContentTypeAccount ($iContentId, $sType, $iAccountId)
+    {
+        $sSql = $this->prepare("SELECT * FROM `sys_profiles` WHERE `account_id` = ? AND `type` = ? AND `content_id` = ?", $iAccountId, $sType, $iContentId);
+        return $this->getRow($sSql);
+    }
+
+    /**
+     * Get profile by content id and type.
+     * @param  string $iAccountId account id
+     * @return array  if aprofile ids, key is profile id
+     */
+    public function getProfileByContentAndType ($iContentId, $sType)
+    {
+        $sSql = $this->prepare("SELECT * FROM `sys_profiles` WHERE `content_id` = ? AND `type` = ?", $iContentId, $sType);
+        return $this->getRow($sSql);
+    }
+
+    /**
+     * Insert account and content id association. Also if currect profile id is not defined - it updates current profile id in account.
+     * @param $iAccountId account id
+     * @param $iContentId content id
+     * @param $sStatus profile status
+     * @param $sType profile content type
+     * @return inserted profile's id
+     */
+    public function insertProfile ($iAccountId, $iContentId, $sStatus, $sType = 'system')
+    {
+        $sSql = $this->prepare("INSERT INTO `sys_profiles` SET `account_id` = ?, `type` = ?, `content_id` = ?, `status` = ?", $iAccountId, $sType, $iContentId, $sStatus);
+        if (!$this->query($sSql))
+            return false;
+        $iProfileId = $this->lastId();
+        $this->getCurrentProfileByAccount($iAccountId); // it updates current profile id automatically if it is not defined
+        return $iProfileId;
+    }
+
+    /**
+     * Update profile's status
+     * @param $iProfileId profile id
+     * @param $sStatus profile status
+     * @return true on success or false otherwise
+     */
+    public function changeStatus ($iProfileId, $sStatus)
+    {
+        return $this->_updateField ($iProfileId, 'status', $sStatus);
+    }
+
+    /**
+     * Get current account profile.
+     * @param  string  $iAccountId account id
+     * @return current account's profile id
+     */
+    public function getCurrentProfileByAccount ($iAccountId)
+    {
+        $sSql = $this->prepare("SELECT `profile_id` FROM `sys_accounts` WHERE `id` = ? LIMIT 1", $iAccountId);
+        $iProfileId = $this->getOne($sSql);
+        if (!$iProfileId) {
+            $sSql = $this->prepare("SELECT `id` FROM `sys_profiles` WHERE `account_id` = ? LIMIT 1", $iAccountId);
+            $iProfileId = $this->getOne($sSql);
+            if (!$iProfileId)
+                return false;
+
+            if (!BxDolAccountQuery::getInstance()->updateCurrentProfile($iAccountId, $iProfileId))
+                return false;
+        }
+        return $iProfileId;
+    }
+
+    /**
+     * Get profile by specified field name and value.
+     * It is for internal usage only.
+     * Use other funtions to get profile info, like getInfoById, etc.
+     * @param  string $sField database field name
+     * @param  mixed  $sValue database field value
+     * @return array  with porfile info
+     */
+    protected function _getDataByField ($sField, $sValue)
+    {
+        $sSql = $this->prepare("SELECT * FROM `sys_profiles` WHERE `$sField` = ? LIMIT 1", $sValue);
+        return $this->getRow($sSql);
+    }
+
+    /**
+     * get profile id by id
+     */
+    public function getIdById($iId)
+    {
+        return (int)$this->_getFieldByField('id', 'id', $iId);
+    }
+
+    /**
+     * get account info by profile id
+     */
+    public function getAccountInfoByProfileId($iId)
+    {
+        $sSql = $this->prepare("SELECT `a`.* FROM `sys_accounts` AS `a` INNER JOIN `sys_profiles` AS `p` ON (`p`.`account_id` = `a`.`id`) WHERE `p`.`id` = ? LIMIT 1", $iId);
+        return $this->getRow($sSql);
+    }
+
+    /**
+     * get account email by profile id
+     */
+    public function getEmailById($iId)
+    {
+        $a = $this->getAccountInfoByProfileId($iId);
+        if (!$a || empty($a['email']))
+            return false;
+        return $a['email'];
+    }
+
+    /**
+     * Get profile info by id
+     * @param  int   $iID profile id
+     * @return array with profile info
+     */
+    public function getInfoById( $iID )
+    {
+        return $this->_getDataByField('id', (int)$iID);
+    }
+
+	/**
+     * Is profile online by id
+     * @param  int $iId profile id
+     * @return profile online status
+     */
+    public function isOnline($iId)
+    {
+        $sSql = $this->prepare("SELECT 
+        		`tp`.`id` 
+        	FROM `sys_profiles` AS `tp` 
+        	INNER JOIN `sys_accounts` AS `ta` ON `tp`.`account_id`=`ta`.`id` 
+        	INNER JOIN `sys_sessions` AS `ts` ON `tp`.`account_id`=`ts`.`user_id` 
+        	WHERE 
+        		`tp`.`id` = ? AND 
+        		`ta`.`profile_id`=`tp`.`id` AND 
+        		`ts`.`date` > (UNIX_TIMESTAMP() - 60 * ?) 
+        	LIMIT 1", $iId, (int)getParam('sys_account_online_time'));
+        return (int)$this->getOne($sSql) > 0;
+    }
+
+    /**
+     * Get profile field by specified field name and value.
+     * In most cases it is for internal usage only.
+     * Use other funtions to get profile info, like getIdByEmail, etc.
+     * @param  string    $sFieldRequested database field name to return
+     * @param  string    $sFieldSearch    database field name to search for
+     * @param  mixed     $sValue          database field value
+     * @return specified profile field value
+     */
+    protected function _getFieldByField ($sFieldRequested, $sFieldSearch, $sValue)
+    {
+        $sSql = $this->prepare("SELECT `$sFieldRequested` FROM `sys_profiles` WHERE `$sFieldSearch` = ? LIMIT 1", $sValue);
+        return $this->getOne($sSql);
+    }
+
+    /**
+     * Update some field by profile id
+     * In most cases it is for internal usage only.
+     * Use other funtions to get profile info, like updateLogged, etc.
+     * @param  string    $sFieldRequested database field name to return
+     * @param  string    $sFieldSearch    database field name to search for
+     * @param  mixed     $sValue          database field value
+     * @return specified profile field value
+     */
+    protected function _updateField ($iId, $sFieldForUpdate, $sValue)
+    {
+        $sSql = $this->prepare("UPDATE `sys_profiles` SET `$sFieldForUpdate` = ? WHERE `id` = ? LIMIT 1", $sValue, $iId);
+        return $this->query($sSql);
+    }
+
+    /**
+     * Delete profile info by id
+     * @param  int      $iID profile id
+     * @return affected rows
+     */
+    public function delete($iID)
+    {
+        $sSql = $this->prepare("DELETE FROM `sys_profiles` WHERE `id` = ? LIMIT 1", $iID);
+        return $this->query($sSql);
+    }
+
+    /**
+     * Reset deleted profile ids and assign system profile ids.
+     * Should be called after profiles module deletion.
+     * It can be called automatically if 
+     * @code
+     * 'process_deleted_profiles' => 1 
+     * @endcode
+     * is specified in module config.php file in 'uninstall' section.
+     */
+    public function processDeletedProfiles ()
+    {
+        $this->query("UPDATE  `sys_accounts` AS  `a` LEFT OUTER JOIN  `sys_profiles` AS  `p` ON  `a`.`profile_id` =  `p`.`id` SET  `a`.`profile_id` =0 WHERE  `p`.`id` IS NULL"); // reset deleted profiles
+        return $this->query("UPDATE  `sys_accounts` AS  `a` INNER JOIN  `sys_profiles` AS  `p` ON  `a`.`id` =  `p`.`account_id` AND  `p`.`type` =  'system' SET  `a`.`profile_id` =  `p`.`id`"); // assign system profile to reset accounts
+    }
+}
+
+/** @} */

+ 145 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolProfileUndefined.php

@@ -0,0 +1,145 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * 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 '';
+    }
+
+    /**
+     * Check if profile is active
+     */
+    public function isActive()
+    {
+        return true;
+    }
+}
+
+/** @} */

+ 239 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolReport.php

@@ -0,0 +1,239 @@
+<?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);
+
+/**
+ * @page objects
+ * @section reports Reports
+ * @ref BxDolReport
+ */
+
+/**
+ * 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);
+    }
+}
+
+/** @} */

+ 71 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolReportQuery.php

@@ -0,0 +1,71 @@
+<?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 BxDolReport
+ */
+class BxDolReportQuery extends BxDolObjectQuery
+{
+    public function __construct(&$oModule)
+    {
+        parent::__construct($oModule);
+
+        $this->_sMethodGetEntry = 'getReport';
+    }
+
+    public function getSqlParts($sMainTable, $sMainField)
+    {
+    	$aResult = parent::getSqlParts($sMainTable, $sMainField);
+        if(empty($aResult))
+            return $aResult;
+
+		$aResult['fields'] = ", `{$this->_sTable}`.`count` as `report_count` ";
+        return $aResult;
+    }
+
+	public function isPerformed($iObjectId, $iAuthorId)
+    {
+    	/*
+    	 * 'false' is returned everytime to allow multiple reports for everybody.
+    	 */
+        return false;
+    }
+
+	public function getPerformedBy($iObjectId)
+    {
+        $sQuery = $this->prepare("SELECT `author_id`, `type`, `text` FROM `{$this->_sTableTrack}` WHERE `object_id`=? ORDER BY `date` DESC", $iObjectId);
+        return $this->getAll($sQuery);
+    }
+
+	public function getReport($iObjectId)
+    {
+        $sQuery = $this->prepare("SELECT `count` as `count` FROM {$this->_sTable} WHERE `object_id` = ? LIMIT 1", $iObjectId);
+
+        $aResult = $this->getRow($sQuery);
+        if(empty($aResult) || !is_array($aResult))
+            $aResult = array('count' => 0);
+
+        return $aResult;
+    }
+
+	public function putReport($iObjectId)
+    {
+        $sQuery = $this->prepare("SELECT `object_id` FROM `{$this->_sTable}` WHERE `object_id` = ? LIMIT 1", $iObjectId);
+        $bExists = (int)$this->getOne($sQuery) != 0;
+
+        if(!$bExists)
+            $sQuery = $this->prepare("INSERT INTO `{$this->_sTable}` SET `object_id` = ?, `count` = '1'", $iObjectId);
+        else
+            $sQuery = $this->prepare("UPDATE `{$this->_sTable}` SET `count` = `count` + 1 WHERE `object_id` = ?", $iObjectId);
+
+        return (int)$this->query($sQuery) != 0;
+    }
+}
+
+/** @} */

+ 154 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolRequest.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
+ * @{
+ */
+
+define('BX_DOL_REQUEST_ERROR_MODULE_NOT_FOUND', 1);
+define('BX_DOL_REQUEST_ERROR_PAGE_NOT_FOUND', 2);
+
+class BxDolRequest extends BxDol
+{
+    function __construct()
+    {
+        parent::__construct();
+    }
+    public static function processAsFile($aModule, &$aRequest)
+    {
+        if(empty($aRequest) || ($sFileName = array_shift($aRequest)) == "")
+            $sFileName = 'index';
+
+        $sFile = BX_DIRECTORY_PATH_MODULES . $aModule['path'] . $sFileName . '.php';
+        if(!file_exists($sFile))
+            BxDolRequest::pageNotFound($sFileName, $aModule['uri']);
+        else {
+            if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->beginModule('file', ($sPrHash = uniqid(rand())), $aModule, $sFileName);
+            include($sFile);
+            if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->endModule('file', $sPrHash);
+        }
+    }
+    public static function processAsAction($aModule, &$aRequest, $sClass = "Module")
+    {
+        $sAction = empty($aRequest) || (isset($aRequest[0]) && empty($aRequest[0])) ? 'Home' : array_shift($aRequest);
+        $sMethod = 'action' . bx_gen_method_name($sAction);
+
+        if(isset($GLOBALS['bx_profiler']))
+            $GLOBALS['bx_profiler']->beginModule('action', ($sPrHash = uniqid(rand())), $aModule, $sClass, $sMethod);
+
+        $mixedRet = BxDolRequest::_perform($aModule, $sClass, $sMethod, $aRequest);
+
+        if(isset($GLOBALS['bx_profiler']))
+            $GLOBALS['bx_profiler']->endModule('action', $sPrHash);
+
+        return $mixedRet;
+    }
+    public static function processAsService($aModule, $sMethod, $aParams, $sClass = "Module")
+    {
+        if (isset($aModule['name']) && 'system' == $aModule['name'] && 'Module' == $sClass)
+            $sClass = 'BaseServices';
+        $sMethod = 'service' . bx_gen_method_name($sMethod);
+
+        if(isset($GLOBALS['bx_profiler']))
+            $GLOBALS['bx_profiler']->beginModule('service', ($sPrHash = uniqid(rand())), $aModule, $sClass, $sMethod);
+
+        $mixedRet = BxDolRequest::_perform($aModule, $sClass, $sMethod, $aParams, false);
+
+        if(isset($GLOBALS['bx_profiler']))
+            $GLOBALS['bx_profiler']->endModule('service', $sPrHash);
+
+        return $mixedRet;
+    }
+    public static function serviceExists($mixedModule, $sMethod, $sClass = "Module")
+    {
+        return BxDolRequest::_methodExists($mixedModule, 'service', $sMethod, $sClass);
+    }
+    public static function actionExists($mixedModule, $sMethod, $sClass = "Module")
+    {
+        return BxDolRequest::_methodExists($mixedModule, 'action', $sMethod, $sClass);
+    }
+    public static function moduleNotFound($sModule)
+    {
+        BxDolRequest::_error('module', $sModule);
+    }
+    public static function pageNotFound($sPage, $sModule)
+    {
+        BxDolRequest::_error('page', $sPage, $sModule);
+    }
+    public static function methodNotFound($sMethod, $sModule)
+    {
+        BxDolRequest::_error('method', $sMethod, $sModule);
+    }
+
+    protected static function _perform($aModule, $sClass, $sMethod, $aParams, $bTerminateOnError = true)
+    {
+        $sClass = $aModule['class_prefix'] . $sClass;
+
+        $oModule = BxDolRequest::_require($aModule, $sClass);
+        if($oModule === false && $bTerminateOnError)
+            BxDolRequest::methodNotFound($sMethod, $aModule['uri']);
+        else if($oModule === false && !$bTerminateOnError)
+            return false;
+
+        $bMethod = method_exists($oModule, $sMethod);
+        if($bMethod)
+            return call_user_func_array(array($oModule, $sMethod), $aParams);
+        else if(!$bMethod && $bTerminateOnError)
+            BxDolRequest::methodNotFound($sMethod, $aModule['uri']);
+        else if(!$bMethod && !$bTerminateOnError)
+            return false;
+    }
+    protected static function _require($aModule, $sClass)
+    {
+        if(isset($GLOBALS['bxDolClasses'][$sClass]))
+            return $GLOBALS['bxDolClasses'][$sClass];
+
+        if($aModule['path']) {
+            $sFile = BX_DIRECTORY_PATH_MODULES . $aModule['path'] . 'classes/' . $sClass . '.php';
+            if(!file_exists($sFile))
+                return false;
+
+            require_once($sFile);
+        } 
+
+        if(!class_exists($sClass))
+            return false;
+
+        $GLOBALS['bxDolClasses'][$sClass] = new $sClass($aModule);
+        return $GLOBALS['bxDolClasses'][$sClass];
+    }
+    protected static function _methodExists($mixedModule, $sMethodType, $sMethodName, $sClass = "Module")
+    {
+        $aModule = $mixedModule;
+        if(is_string($mixedModule)) 
+            $aModule = BxDolModuleQuery::getInstance()->getModuleByName($mixedModule);
+
+        if (!$aModule)
+            return false;
+
+        $sClass = $aModule['class_prefix'] . $sClass;
+        if(($oModule = BxDolRequest::_require($aModule, $sClass)) === false)
+            return false;
+
+        $sMethod = $sMethodType . bx_gen_method_name($sMethodName);
+        return method_exists($oModule, $sMethod);
+    }
+    protected static function _error($sType, $sParam1 = '', $sParam2 = '')
+    {
+        header('Status: 404 Not Found');
+        header('HTTP/1.0 404 Not Found');
+
+        require_once(BX_DIRECTORY_PATH_INC . "design.inc.php");
+        bx_import('BxDolLanguages');
+        $oTemplate = BxDolTemplate::getInstance();
+        $oTemplate->setPageNameIndex(BX_PAGE_DEFAULT);
+        $oTemplate->setPageHeader(_t("_sys_request_" . $sType . "_not_found_cpt"));
+        $oTemplate->setPageContent('page_main_code', DesignBoxContent('', MsgBox(_t("_sys_request_" . $sType . "_not_found_cnt", bx_process_output($sParam1), bx_process_output($sParam2))), BX_DB_PADDING_NO_CAPTION));
+        $oTemplate->getPageCode();
+        exit;
+    }
+}
+
+/** @} */

+ 96 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolRss.php

@@ -0,0 +1,96 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    UnaCore UNA Core
+ * @{
+ */
+
+/**
+ * @page objects
+ * @section rss Rss
+ * @ref BxDolRss
+ */
+
+/**
+ * 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) {}
+}
+
+/** @} */

+ 31 - 0
upgrade/files/9.0.0.B2-9.0.0.B3/files/inc/classes/BxDolRssBoonEx.php

@@ -0,0 +1,31 @@
+<?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 BxDolRssBoonEx extends BxTemplRss
+{
+	protected $aFeeds;
+
+    public function __construct($aObject)
+    {
+        parent::__construct($aObject);
+
+        $this->aFeeds = array (
+		    'boonex_news' => 'http://www.boonex.com/unity/blog/featured_posts/?rss=1'
+		);
+    }
+
+    public function getUrl($mixedId) {
+		if(!isset($this->aFeeds[$mixedId]))
+			return false;
+
+		return $this->aFeeds[$mixedId];
+    }
+}
+
+/** @} */

Неке датотеке нису приказане због велике количине промена