Browse Source

Upgrade Patch 8.0.0-A10 to 8.0.0-A11

Alex Trofimov 11 years ago
parent
commit
af747401c2
100 changed files with 23774 additions and 0 deletions
  1. 8 0
      upgrade/files/8.0.0.A10-8.0.0.A11/check.php
  2. 16 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/.htaccess
  3. 47 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/cmts.php
  4. 34 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/conn.php
  5. 29 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/get_rss_feed.php
  6. 31 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/grid.php
  7. 15 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/gzip_loader.php
  8. 59 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/image_transcoder.php
  9. 359 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolAccount.php
  10. 224 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolAccountQuery.php
  11. 702 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolAcl.php
  12. 279 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolAclQuery.php
  13. 162 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolAlerts.php
  14. 96 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCacheAPC.php
  15. 94 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCacheEAccelerator.php
  16. 133 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCacheFile.php
  17. 68 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCacheFileHtml.php
  18. 96 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCacheMemcache.php
  19. 85 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCacheXCache.php
  20. 134 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCaptcha.php
  21. 37 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCaptchaQuery.php
  22. 1004 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCmts.php
  23. 380 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCmtsQuery.php
  24. 682 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolConnection.php
  25. 202 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolConnectionQuery.php
  26. 121 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCronPruning.php
  27. 78 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCronQuery.php
  28. 22 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCronStorage.php
  29. 25 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCronTranscoder.php
  30. 27 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCronUpgradeCheck.php
  31. 21 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCronUpgradeModulesCheck.php
  32. 861 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolDb.php
  33. 154 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolEditor.php
  34. 37 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolEditorQuery.php
  35. 187 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolEmailTemplates.php
  36. 68 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolEmailTemplatesQuery.php
  37. 205 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolFile.php
  38. 1533 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolForm.php
  39. 56 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolFormNested.php
  40. 195 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolFormQuery.php
  41. 145 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolFtp.php
  42. 549 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolGrid.php
  43. 156 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolGridConnections.php
  44. 88 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolGridQuery.php
  45. 274 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolImageResize.php
  46. 120 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolInformer.php
  47. 85 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolInstallerHasher.php
  48. 170 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolInstallerUtils.php
  49. 132 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolKey.php
  50. 52 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolKeyQuery.php
  51. 348 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolLanguages.php
  52. 293 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolLanguagesQuery.php
  53. 233 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolLiveUpdates.php
  54. 36 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolLiveUpdatesQuery.php
  55. 269 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolMenu.php
  56. 70 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolMenuQuery.php
  57. 535 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolMetatags.php
  58. 96 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolMetatagsQuery.php
  59. 169 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolModule.php
  60. 129 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolModuleConfig.php
  61. 179 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolModuleQuery.php
  62. 102 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolModuleTemplate.php
  63. 253 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolPage.php
  64. 354 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolPaginate.php
  65. 259 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolPermalinks.php
  66. 303 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolPrivacy.php
  67. 186 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolPrivacyQuery.php
  68. 419 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolProfile.php
  69. 45 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolProfileForms.php
  70. 238 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolProfileQuery.php
  71. 126 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolProfileUndefined.php
  72. 154 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolRequest.php
  73. 96 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolRss.php
  74. 31 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolRssBoonEx.php
  75. 32 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolRssPageBlock.php
  76. 33 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolRssQuery.php
  77. 858 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolSearch.php
  78. 109 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolService.php
  79. 170 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolSession.php
  80. 56 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolSessionQuery.php
  81. 127 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolSocialSharing.php
  82. 28 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolSocialSharingQuery.php
  83. 1219 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolStorage.php
  84. 147 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolStorageLocal.php
  85. 297 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolStorageQuery.php
  86. 196 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolStorageS3.php
  87. 2242 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolTemplate.php
  88. 825 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolTranscoder.php
  89. 170 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolTranscoderImage.php
  90. 24 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolTranscoderImageQuery.php
  91. 76 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolTranscoderProxy.php
  92. 278 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolTranscoderQuery.php
  93. 271 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolTranscoderVideo.php
  94. 24 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolTranscoderVideoQuery.php
  95. 224 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolUpgrader.php
  96. 421 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolUploader.php
  97. 37 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolUploaderQuery.php
  98. 187 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolView.php
  99. 77 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolViewQuery.php
  100. 386 0
      upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolVote.php

+ 8 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/check.php

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

+ 16 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/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 ^storage/([a-zA-Z0-9]+)/([a-zA-Z0-9\.]+)  storage.php?o=$1&f=$2   [QSA,L]
+</IfModule>
+
+AddType image/svg+xml svg

+ 47 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/cmts.php

@@ -0,0 +1,47 @@
+<?php
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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) {
+        $sObjectTitle = $oCmts->getObjectTitle($iObjectId);
+
+        $sHeader = _t('_cmt_page_view_header', $sObjectTitle);
+        $sTitle = _t('_cmt_page_view_title', $oCmts->getBaseUrl(), $sObjectTitle);
+        $sContent = DesignBoxContent($sTitle, $oCmts->getCommentBlock($iCmtId), BX_DB_PADDING_DEF);
+
+        $oTemplate = BxDolTemplate::getInstance();
+        $oTemplate->setPageNameIndex(BX_PAGE_DEFAULT);
+        $oTemplate->setPageHeader($sHeader);
+        $oTemplate->setPageContent('page_main_code', $sContent);
+        $oTemplate->getPageCode();
+    }
+}
+
+/** @} */

+ 34 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/conn.php

@@ -0,0 +1,34 @@
+<?php
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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();
+
+}
+
+/** @} */

+ 29 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/get_rss_feed.php

@@ -0,0 +1,29 @@
+<?php
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+require_once('./inc/header.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/8.0.0.A10-8.0.0.A11/files/grid.php

@@ -0,0 +1,31 @@
+<?php
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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/8.0.0.A10-8.0.0.A11/files/gzip_loader.php

@@ -0,0 +1,15 @@
+<?php
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+require_once('./inc/header.inc.php');
+
+$sFile = bx_process_input($_GET['file']);
+BxDolGzip::load($sFile);
+
+/** @} */

+ 59 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/image_transcoder.php

@@ -0,0 +1,59 @@
+<?php
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+if (isset($_GET['devicePixelRatio'])) {
+    $sDpr = $_GET['devicePixelRatio'];
+
+    if ('' . ceil(intval($sDpr)) !== $sDpr)
+        $sDpr = '1';
+
+    setcookie('devicePixelRatio', $sDpr, time()+60*60*24*365);
+    exit();
+}
+
+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 (!$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 ();
+}
+
+/** @} */

+ 359 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolAccount.php

@@ -0,0 +1,359 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+class BxDolAccount extends BxDol
+{
+    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)
+    {
+        $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();
+        $aPlus['email'] = $sEmail;
+        $aPlus['conf_code'] = $sConfirmationCode;
+        $aPlus['conf_link'] = $sConfirmationLink;
+        $aPlus['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 $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 avatar picture 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'];
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * 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;
+    }
+
+}
+
+/** @} */

+ 224 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolAccountQuery.php

@@ -0,0 +1,224 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * All queries related to account
+ */
+class BxDolAccountQuery extends BxDolDb implements iBxDolSingleton
+{
+    public 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);
+    }
+
+    /**
+     * 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);
+    }
+
+    /**
+     * Search account profile by keyword
+     */
+    public function searchByTerm($sTerm, $iLimit)
+    {
+        $aFieldsQuickSearch = array('name', 'email');
+        $sWhere = '';
+        foreach ($aFieldsQuickSearch as $sField)
+            $sWhere .= $this->prepare(" OR `c`.`$sField` LIKE ? ", '%' . $sTerm . '%');
+
+        $sOrderBy = $this->prepare(" ORDER BY `added` DESC LIMIT ?", (int)$iLimit);
+
+        $sQuery = $this->prepare("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` = ?) WHERE `p`.`status` = ?", 'system', BX_PROFILE_STATUS_ACTIVE);
+        return $this->getAll($sQuery . " AND (0 $sWhere) " . $sOrderBy);
+    }
+}
+
+/** @} */

+ 702 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolAcl.php

@@ -0,0 +1,702 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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);
+
+class BxDolAcl extends BxDol 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 = new BxDolAclQuery();
+    }
+
+    /**
+     * 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__];
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * Get pricing options for the given membership
+     *
+     * @param  int    $iLevelId membership to get prices for
+     * @return array( days1 => price1, days2 => price2, ...) if no prices set, then just array()
+     */
+    function getMembershipPrices($iLevelId)
+    {
+        $aPrice = array();
+        $this->oDb->getPrices(array('type' => 'by_level_id_pair', 'value' => $iLevelId), $aPrice, false);
+        return $aPrice;
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * Buy a membership for a profile
+     *
+     * @param  int     $iProfileId     profile that is going to get the membership
+     * @param  int     $iMembershipId  bought membership
+     * @param  int     $sTransactionId internal key of the transaction (ID from Transactions table)
+     * @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 buyMembership($iProfileId, $iPriceId, $sTransactionId, $bStartsNow = false)
+    {
+        $aPrice = array();
+        $this->oDb->getPrices(array('type' => 'by_id', 'value' => (int)$iPriceId), $aPrice, false);
+        if(!is_array($aPrice) || empty($aPrice))
+            return false;
+
+        $aLevel = array();
+        $this->oDb->getLevels(array('type' => 'by_id', 'value' => $aPrice['level_id']), $aLevel, false);
+        if(!is_array($aLevel) || empty($aLevel))
+            return false;
+
+        $iLevelId = (int)$aLevel['id'];
+        if (isset($this->_aStandardMemberships[$iLevelId])) // check for predefined non-purchasable memberships
+            return false;
+
+        if($aLevel['active'] != 'yes' || $aLevel['purchasable'] != 'yes') // check if membership is active and purchasable
+            return false;
+
+        return $this->setMembership((int)$iProfileId, $iLevelId, $aPrice['days'], $bStartsNow, $sTransactionId);
+    }
+
+    /**
+     * 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  int     $iDays      number of days to set membership for if 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, $iDays = 0, $bStartsNow = false, $sTransactionId = '')
+    {
+        $iProfileId = (int)$iProfileId;
+        $iLevelId = (int)$iLevelId;
+        $iDays = (int)$iDays;
+        $bStartsNow = $bStartsNow ? true : false;
+
+        $iSecInDay = 86400;
+
+        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 ($iDays < 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(!is_null($aMembershipLatest['date_expires']))
+                $iDateStarts = $aMembershipLatest['date_expires'];
+            else if(is_null($aMembershipLatest['date_expires']) && $aMembershipLatest['id'] != MEMBERSHIP_ID_STANDARD)
+                return false;
+        } else
+            $this->oDb->deleteLevelByProfileId($iProfileId, true); ///< Delete any profile's membership level
+
+        // set lifetime membership if 0 days is used.
+        $iDateExpires = $iDays != 0 ? (int)$iDateStarts + $iDays * $iSecInDay : null;
+        if(!$this->oDb->insertLevelByProfileId($iProfileId, $iLevelId, $iDateStarts, $iDateExpires, $sTransactionId))
+           return false;
+
+        // raise membership alert
+        $oZ = new BxDolAlerts('profile', 'set_membership', '', $iProfileId, array('mlevel'=> $iLevelId, 'days' => $iDays, '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;
+    }
+
+    /**
+     * Define action, defining all names are translated the following way:
+     *  my action => BX_MY_ACTION
+     *
+     * @param $aActions array of actions from sys_acl_actions table, with default array keys (starting from 0) and text values
+     */
+    function defineMembershipActions($mixedActions, $sPrefix = 'BX_')
+    {
+        if (is_array($mixedActions))
+            $aActions = $mixedActions;
+        else
+            $aActions = array($mixedActions);
+
+        $aNames = array();
+        foreach ($aActions as $sName)
+            if(!defined($sPrefix . strtoupper(str_replace(' ', '_', $sName))))
+                $aNames[] = $sName;
+
+        if(!$aNames)
+            return;
+
+        $aActions = array();
+        $this->oDb->getActions(array('type' => 'by_names', 'value' => $aNames), $aActions, false);
+        foreach($aActions as $aAction)
+            define($sPrefix . strtoupper(str_replace(' ', '_', $aAction['name'])), $aAction['id']);
+    }
+
+    /**
+     * 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['DateExpires']) || (int)$aMembership['DateExpires'] == 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 BxDolAcl::getInstance()->checkAction($iProfileId, $iActionId, $bPerformAction);
+}
+
+/** @} */

+ 279 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolAclQuery.php

@@ -0,0 +1,279 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * Database queries for ACL
+ * @see BxDolAcl
+ */
+class BxDolAclQuery extends BxDolDb implements iBxDolSingleton
+{
+    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';
+                $sWhereClause .= $this->prepare("AND `tal`.`ID`=?", $aParams['value']);
+                $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':
+                $sWhereClause .= " AND `taa`.`Name` IN(" . $this->implode_escape($aParams['value']) . ") " . $this->prepare(" AND `taa`.`Module` = ? ", $aParams['module']);
+                break;
+            case 'by_names':
+                $sWhereClause .= " AND `taa`.`Name` IN(" . $this->implode_escape($aParams['value']) . ")";
+                break;
+            case 'by_level_id':
+                $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 .= $this->prepare("AND `tam`.`IDLevel`=? AND (`taa`.`DisabledForLevels`='0' OR `taa`.`DisabledForLevels`&?=0)", $aParams['value'], pow(2, ($aParams['value'] - 1)));
+                break;
+            case 'by_level_id_key_id':
+                $aMethod['name'] = 'getAllWithKey';
+                $aMethod['params'][1] = 'id';
+                $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 .= $this->prepare("AND `tam`.`IDLevel`=?", $aParams['value']);
+                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 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)
+    {
+        $sUpdateAddon = "";
+        if($iValidSince != 0)
+            $sUpdateAddon = $this->prepare(", ValidSince=FROM_UNIXTIME(?)", $iValidSince);
+
+        $sQuery = $this->prepare("UPDATE `sys_acl_actions_track` SET `ActionsLeft`=?" . $sUpdateAddon . " WHERE `IDAction`=? AND `IDMember`=?", $iActionsLeft, $iActionId, $iProfileId);
+        return (int)$this->query($sQuery) > 0;
+    }
+
+    function insertLevelByProfileId($iProfileId, $iMembershipId, $iDateStarts, $iDateExpires, $sTransactionId)
+    {
+        $sQuery = $this->prepare("INSERT `sys_acl_levels_members` (`IDMember`, `IDLevel`, `DateStarts`, `DateExpires`, `TransactionID`) VALUES (?, ?, FROM_UNIXTIME(?), FROM_UNIXTIME(?), ?)", $iProfileId, $iMembershipId, $iDateStarts, $iDateExpires, $sTransactionId);
+        return (int)$this->query($sQuery) > 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 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;
+    }
+}
+
+/** @} */

+ 162 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolAlerts.php

@@ -0,0 +1,162 @@
+<?php defined('BX_DOL') or defined('BX_DOL_INSTALL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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 (!empty($aHandler['file']) && !empty($aHandler['class']) && file_exists(BX_DIRECTORY_PATH_ROOT . $aHandler['file'])) {
+                    if(!class_exists($aHandler['class']))
+                        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');
+                }
+            }
+    }
+
+    /**
+     * 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` 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']);
+
+        return $aResult;
+    }
+}
+
+class BxDolAlertsResponse extends BxDol
+{
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    public function response($oAlert) {}
+}
+
+/** @} */

+ 96 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCacheAPC.php

@@ -0,0 +1,96 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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;
+    }
+}
+
+/** @} */

+ 94 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCacheEAccelerator.php

@@ -0,0 +1,94 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+class BxDolCacheEAccelerator 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)
+    {
+        $sData = eaccelerator_get($sKey);
+        return null === $sData ? null : unserialize($sData);
+    }
+
+    /**
+     * 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 = eaccelerator_put($sKey, serialize($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)
+    {
+        eaccelerator_lock($sKey);
+
+        eaccelerator_rm($sKey);
+
+        eaccelerator_unlock($sKey);
+
+        return true;
+    }
+
+    /**
+     * Check if eAccelerator is available
+     * @return boolean
+     */
+    function isAvailable()
+    {
+        return function_exists('eaccelerator_put');
+    }
+
+    /**
+     * Check if eaccelerator extension is loaded
+     * @return boolean
+     */
+    function isInstalled()
+    {
+        return extension_loaded('eaccelerator');
+    }
+
+    /**
+     * remove all data from cache by key prefix
+     * @return true on success
+     */
+    function removeAllByPrefix ($s)
+    {
+        $l = strlen($s);
+        $aKeys = eaccelerator_list_keys();
+        foreach ($aKeys as $aKey) {
+            $sKey = 0 === strpos($aKey['name'], ':') ? substr($aKey['name'], 1) : $aKey['name'];
+            if (0 == strncmp($sKey, $s, $l))
+                $this->delData($sKey);
+        }
+
+        return true;
+    }
+}
+
+/** @} */

+ 133 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCacheFile.php

@@ -0,0 +1,133 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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);
+
+        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;
+        }
+    }
+}
+
+/** @} */

+ 68 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCacheFileHtml.php

@@ -0,0 +1,68 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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);
+
+        return true;
+    }
+}
+
+/** @} */

+ 96 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCacheMemcache.php

@@ -0,0 +1,96 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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;
+    }
+}
+
+/** @} */

+ 85 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCacheXCache.php

@@ -0,0 +1,85 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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);
+    }
+}
+
+/** @} */

+ 134 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCaptcha.php

@@ -0,0 +1,134 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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 BxDol implements iBxDolFactoryObject
+{
+    protected $_sObject;
+    protected $_aObject;
+
+    /**
+     * Constructor
+     * @param $aObject array of captcha options
+     */
+    public 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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCaptchaQuery.php

@@ -0,0 +1,37 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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;
+    }
+
+}
+
+/** @} */

+ 1004 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCmts.php

@@ -0,0 +1,1004 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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 BxDol implements iBxDolReplaceable
+{
+    protected $_oQuery = null;
+
+    protected $_sFormObject;
+    protected $_sFormDisplayPost;
+    protected $_sFormDisplayEdit;
+
+    protected $_sTableImages;
+    protected $_sTableImages2Entries;
+
+    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 $_aMarkers = array ();
+
+    protected $_sDisplayType = '';
+    protected $_sDpSessionKey = '';
+    protected $_iDpMaxLevel = 0;
+
+    protected $_sBrowseType = '';
+    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
+     */
+    function __construct($sSystem, $iId, $iInit = 1)
+    {
+        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->_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';
+
+        if ($iInit)
+            $this->init($iId);
+    }
+
+    /**
+     * 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)
+    {
+        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);
+        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`,
+                    `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);
+        if(!$oVote || !$oVote->isEnabled())
+            return false;
+
+        return $oVote;
+    }
+
+    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 getObjectTitle ($iObjectId = 0)
+    {
+        return $this->_oQuery->getObjectTitle ($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()){
+            $this->_echoResultJson(array());
+            return;
+        }
+
+        $iCmtId = bx_process_input(bx_get('Cmt'), BX_DATA_INT);
+        $this->_echoResultJson($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);
+        $this->_echoResultJson(array(
+            'parent_id' => $aCmt['cmt_parent_id'],
+            'vparent_id' => $aCmt['cmt_vparent_id'],
+            'content' => $this->getComment($aCmt, array('type' => $sCmtBrowse), array('type' => $sCmtDisplay))
+        ));
+    }
+
+    public function actionGetCmts ()
+    {
+        if (!$this->isEnabled())
+            return '';
+
+        $aBp = $aDp = array();
+		$this->_getParams($aBp, $aDp);
+
+        return $this->getComments($aBp, $aDp);
+    }
+
+    public function actionSubmitPostForm()
+    {
+        if(!$this->isEnabled() || !$this->isPostReplyAllowed()) {
+            $this->_echoResultJson(array());
+            return;
+        }
+
+        $iCmtParentId = 0;
+        if(bx_get('cmt_parent_id') !== false)
+            $iCmtParentId = bx_process_input(bx_get('cmt_parent_id'), BX_DATA_INT);
+
+        $this->_echoResultJson($this->getFormPost($iCmtParentId));
+    }
+
+    public function actionSubmitEditForm()
+    {
+        if (!$this->isEnabled()) {
+            $this->_echoResultJson(array());
+            return;
+        };
+
+        $iCmtId = 0;
+        if(bx_get('cmt_id') !== false)
+            $iCmtId = bx_process_input(bx_get('cmt_id'), BX_DATA_INT);
+
+        $this->_echoResultJson($this->getFormEdit($iCmtId));
+    }
+
+    public function actionRemove()
+    {
+        if (!$this->isEnabled()) {
+            $this->_echoResultJson(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) {
+            $this->_echoResultJson(array('msg' => _t('_No such comment')));
+            return;
+        }
+
+        if ($aCmt['cmt_replies'] > 0) {
+            $this->_echoResultJson(array('msg' => _t('_Can not delete comments with replies')));
+            return;
+        }
+
+        $iCmtAuthorId = $this->_getAuthorId();
+        if(!$this->isRemoveAllowed($aCmt)) {
+            $this->_echoResultJson(array('msg' => $aCmt['cmt_author_id'] == $iCmtAuthorId ? strip_tags($this->msgErrRemoveAllowed()) : _t('_Access denied')));
+            return;
+        }
+
+        if($this->_oQuery->removeComment ($this->getId(), $aCmt['cmt_id'], $aCmt['cmt_parent_id'])) {
+            $this->_triggerComment();
+
+            $oStorage = BxDolStorage::getObjectInstance($this->getStorageObjectName());
+
+            $aImages = $this->_oQuery->getImages($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', $this->getId(), $iCmtAuthorId, array('comment_id' => $aCmt['cmt_id'], 'comment_author_id' => $aCmt['cmt_author_id']));
+            $oZ->alert();
+
+            $this->_echoResultJson(array('id' => $iCmtId));
+            return;
+        }
+
+        $this->_echoResultJson(array('msg' => _t('_cmt_err_cannot_perform_action')));
+    }
+
+    /**
+     * 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)
+    {
+    	$s = bx_process_output($s, BX_DATA_HTML);
+    	$s = bx_convert_links($s);
+
+        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'] = bx_process_output($aCmt['cmt_text']);
+        $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']);
+    }
+}
+
+/** @} */

+ 380 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCmtsQuery.php

@@ -0,0 +1,380 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * @see BxDolCmts
+ */
+class BxDolCmtsQuery extends BxDolDb
+{
+    protected $_oMain;
+
+    protected $_sTable;
+    protected $_sTriggerTable;
+    protected $_sTriggerFieldId;
+    protected $_sTriggerFieldTitle;
+    protected $_sTriggerFieldComments;
+
+    protected $_sTableImages;
+    protected $_sTableImages2Entries;
+
+    protected $_sTableIds;
+
+    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->_sTriggerFieldTitle = $aSystem['trigger_field_title'];
+        $this->_sTriggerFieldComments = $aSystem['trigger_field_comments'];
+
+        $this->_sTableImages = $aSystem['table_images'];
+        $this->_sTableImages2Entries = $aSystem['table_images2entries'];
+
+        $this->_sTableIds = $aSystem['table_ids'];
+
+        parent::__construct();
+    }
+
+    function getTableName ()
+    {
+        return $this->_sTable;
+    }
+
+    function getCommentsCount ($iId, $iCmtVParentId = -1, $iAuthorId = 0, $sFilter = '')
+    {
+        $sWhereClause = '';
+        if((int)$iCmtVParentId >= 0)
+            $sWhereClause .= $this->prepare(" AND `{$this->_sTable}`.`cmt_vparent_id` = ?", $iCmtVParentId);
+
+        $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:
+        		$sWhereClause .= $this->prepare(" AND `{$this->_sTable}`.`cmt_author_id` <> ?", $iAuthorId);
+        		break;
+        }
+
+        $sQuery = $this->prepare("SELECT
+                COUNT(*)
+            FROM `{$this->_sTable}`
+            $sJoinClause
+            WHERE `{$this->_sTable}`.`cmt_object_id` = ?" . $sWhereClause, $iId);
+
+        return (int)$this->getOne($sQuery);
+    }
+
+    function getComments ($iId, $iCmtVParentId = 0, $iAuthorId = 0, $sFilter = '', $aOrder = array(), $iStart = 0, $iCount = -1)
+    {
+        $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)
+            $sWhereParent = $this->prepare(" AND `{$this->_sTable}`.`cmt_vparent_id` = ?", $iCmtVParentId);
+
+        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->prepare(" LIMIT ?, ?", (int)$iStart, (int)$iCount) : '';
+
+        $sQuery = $this->prepare("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` = ?" . $sWhereParent . $sOder . $sLimit, $iId);
+
+        return $this->getAll($sQuery);
+    }
+
+	function getCommentsBy($aParams = array())
+    {
+    	$aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $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']))
+        	$sWhereClause .= $this->prepare(" AND `{$this->_sTable}`.`cmt_object_id` = ?" , (int)$aParams['object_id']);
+
+        switch($aParams['type']) {
+            case 'latest_ids':
+            	$aMethod['name'] = 'getColumn';
+				$sSelectClause = "`{$this->_sTable}`.`cmt_id`";
+                $sOrderClause = "ORDER BY `{$this->_sTable}`.`cmt_time` DESC";
+                $sLimitClause = isset($aParams['per_page']) ? "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->_sTableImages2Entries}` SET `system_id`=?, `cmt_id`=?, `image_id`=?", $iSystemId, $iCmtId, $iImageId);
+        return (int)$this->query($sQuery) > 0;
+    }
+
+    function getImages($iSystemId, $iCmtId, $iId = false)
+    {
+        $sJoin = '';
+        $sWhere = $this->prepare(" AND `i`.`system_id` = ? ", $iSystemId);
+
+        if (false !== $iCmtId) {
+            $sWhere .= $this->prepare(" AND `i`.`cmt_id` = ? ", $iCmtId);
+        }
+
+        if (false !== $iId) {
+            $sWhere .= $this->prepare(" AND `c`.`cmt_object_id` = ?", $iId);
+            $sJoin .= " INNER JOIN `{$this->_sTable}` AS `c` ON (`i`.`cmt_id` = `c`.`cmt_id`) ";
+        }
+
+        return $this->getAll("SELECT * FROM `{$this->_sTableImages2Entries}` AS `i` " . $sJoin . " WHERE 1 " . $sWhere);
+    }
+
+    function deleteImages($iSystemId, $iCmtId)
+    {
+        $sWhereAddon = '';
+        if (false !== $iCmtId)
+            $sWhereAddon = $this->prepare(" AND `cmt_id` = ? ", $iCmtId);
+
+        $sQuery = $this->prepare("DELETE FROM `{$this->_sTableImages2Entries}` WHERE `system_id` = ?", $iSystemId);
+        $sQuery .= $sWhereAddon;
+
+        return $this->query($sQuery);
+    }
+
+    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->getImages($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->getImages($aSystem['system_id'], false, $iObjectId));
+
+        if ($aFiles) {
+            $sQuery = $this->prepare("DELETE FROM {$this->_sTableImages2Entries} WHERE `system_id` = ? ", $aSystem['system_id']);
+            $sQuery .= " AND `image_id` IN(" . $this->implode_escape($aFiles) . ") ";
+            $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->getImages($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 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;
+    }
+}
+
+/** @} */

+ 682 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolConnection.php

@@ -0,0 +1,682 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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 BxDol 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)
+    {
+        if (!$iContent)
+            $iContent = bx_process_input($_POST['id'], BX_DATA_INT);
+
+        return $this->_action (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)
+    {
+        if (!$iContent)
+            $iContent = bx_process_input($_POST['id'], BX_DATA_INT);
+
+        $a = $this->actionReject($iContent);
+        if (false == $a['err'])
+            return $a;
+
+        return $this->_action (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)
+    {
+        if (!$iContent)
+            $iContent = bx_process_input($_POST['id'], BX_DATA_INT);
+
+        return $this->_action ($iContent, bx_get_logged_profile_id(), 'removeConnection', '_sys_conn_err_connection_does_not_exists');
+    }
+
+    protected function _action ($iInitiator, $iContent, $sMethod, $sErrorKey, $isMutual = false)
+    {
+        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 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');
+    }
+}
+
+/** @} */

+ 202 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolConnectionQuery.php

@@ -0,0 +1,202 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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->prepare(" AND `c`.`initiator` = ?", $iInitiator1);
+        $sWhereJoin2 = $this->prepare(" AND `c2`.`initiator` = ?", $iInitiator2);
+        if (false !== $isMutual) {
+            $sWhereJoin1 .= $this->prepare(" AND `c`.`mutual` = ?", $isMutual);
+            $sWhereJoin2 .= $this->prepare(" 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->prepare(" AND `c`.`initiator` = ?", $iInitiator);
+        if (false !== $isMutual)
+            $sWhere .= $this->prepare(" 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->prepare(" AND `c`.`content` = ?", $iInitiator);
+        if (false !== $isMutual)
+            $sWhere .= $this->prepare(" 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) ? $this->prepare(" AND `c2`.`mutual` = ?", $isMutual) : '';
+        $sJoin = $this->prepare("INNER JOIN `" . $this->_sTable . "` AS `c2` ON (`c2`.`initiator` = ? AND `c`.`content` = `c2`.`content` $sWhereJoin)", $iInitiator2);
+
+        $sWhere = $this->prepare(" AND `c`.`initiator` = ?", $iInitiator1);
+        $sQuery = $this->_getConnectionsQuery($sWhere, $sJoin, '`c`.`content`', $isMutual, $iStart, $iLimit, $iOrder);
+
+        return $this->getColumn($sQuery);
+    }
+
+    public function getConnectedContent ($iInitiator, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        $sWhere = $this->prepare(" AND `c`.`initiator` = ?", $iInitiator);
+        $sQuery = $this->_getConnectionsQuery($sWhere, '', '`c`.`content`', $isMutual, $iStart, $iLimit, $iOrder);
+        return $this->getColumn($sQuery);
+    }
+
+    public function getConnectedInitiators ($iContent, $isMutual = false, $iStart = 0, $iLimit = BX_CONNECTIONS_LIST_LIMIT, $iOrder = BX_CONNECTIONS_ORDER_NONE)
+    {
+        $sWhere = $this->prepare(" AND `c`.`content` = ?", $iContent);
+        $sQuery = $this->_getConnectionsQuery($sWhere, '', '`c`.`initiator`', $isMutual, $iStart, $iLimit, $iOrder);
+        return $this->getColumn($sQuery);
+    }
+
+    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->prepare(" AND `c`.`mutual` = ?", $isMutual) : '';
+
+        return $this->prepare("SELECT $sFields FROM `" . $this->_sTable . "` AS `c` $sJoin WHERE 1 $sWhere $sOrder LIMIT ?, ?", $iStart, $iLimit);
+    }
+
+    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;
+
+        $iMutual = 0;
+        $sMutualField = '';
+        if (BX_CONNECTIONS_TYPE_MUTUAL == $this->_sType) {
+            $aConnectionMutual = $this->getConnection($iContent, $iInitiator);
+            $iMutual = $aConnectionMutual ? 1 : 0;
+            $sMutualField = $this->prepare(", `mutual` = ?", $iMutual);
+        }
+
+        $sQuery = $this->prepare("INSERT INTO `" . $this->_sTable . "` SET `initiator` = ?, `content` = ?, `added` = ?", $iInitiator, $iContent, time());
+        $sQuery .= $sMutualField;
+        if (!$this->query($sQuery))
+            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 false;
+
+        $sQuery = $this->prepare("DELETE FROM `" . $this->_sTable . "` WHERE `initiator` = ? AND `content` = ?", $iInitiator, $iContent);
+        if (!$this->query($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->prepare(" AND `sys_profiles`.`type` = ? ", $sModuleName));
+    }
+
+    protected function onModuleDeleteCustom ($sTable, $sFieldId, $sField = 'initiator', $sWhere = '')
+    {
+        $sQuery = "DELETE `" . $this->_sTable . "` FROM `" . $this->_sTable . "` INNER JOIN `{$sTable}` WHERE `" . $this->_sTable . "`.`$sField` = `{$sTable}`.`{$sFieldId}` " . $sWhere;
+        return $this->query($sQuery);
+    }
+
+}
+
+/** @} */

+ 121 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCronPruning.php

@@ -0,0 +1,121 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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;
+
+        $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;
+
+        // clean old votes
+        $iDeletedVotes = BxDolVote::maintenance();
+
+        echo _t('_sys_pruning_db', $iDeleteMemLevels, $iSessions, $iDeletedViews, $iDeletedVotes, $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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCronQuery.php

@@ -0,0 +1,78 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * Database queries for Cron
+ * @see BxDolCron
+ */
+class BxDolCronQuery extends BxDolDb implements iBxDolSingleton
+{
+    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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCronStorage.php

@@ -0,0 +1,22 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCronTranscoder.php

@@ -0,0 +1,25 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCronUpgradeCheck.php

@@ -0,0 +1,27 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolCronUpgradeModulesCheck.php

@@ -0,0 +1,21 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+class BxDolCronUpgradeModulesCheck extends BxDolCron
+{
+    public function processing()
+    {
+        if('on' != getParam('sys_autoupdate_modules'))
+            return;
+
+		BxDolStudioInstallerUtils::getInstance()->performModulesUpgrade();
+    }
+}
+
+/** @} */

+ 861 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolDb.php

@@ -0,0 +1,861 @@
+<?php defined('BX_DOL') or defined('BX_DOL_INSTALL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+class BxDolDb extends BxDol implements iBxDolSingleton
+{
+    protected $_bErrorChecking = true;
+    protected $_sErrorMessage;
+    protected $_sHost, $_sPort, $_sSocket, $_sDbname, $_sUser, $_sPassword;
+    protected $_rLink, $_rCurrentRes, $_iCurrentResType;
+
+    protected $_oDbCacheObject = null;
+    protected $_aParams = null;
+    protected $_sParamsCacheName = 'sys_options';
+    protected $_sStorageEngine = 'MYISAM';
+
+    /**
+     * 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();
+
+        if (false === $aDbConf) {
+            $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'];
+            $this->_bErrorChecking = isset($aDbConf['error_checking']) ? $aDbConf['error_checking'] : true;
+        }
+
+        $this->_iCurrentResType = MYSQL_ASSOC;
+
+        // connect to db automatically
+        if (empty($GLOBALS['bx_db__rLink'])) {
+            $this->connect();
+            $GLOBALS['gl_db_cache'] = array();
+        } else {
+            $this->_rLink = $GLOBALS['bx_db__rLink'];
+        }
+    }
+
+    /**
+     * 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 (false === $aDbConf && !defined('BX_DATABASE_HOST'))
+                return null;
+            $o = new BxDolDb($aDbConf);
+            $sErrorMessage = $o->connect();
+            if ($sErrorMessage) {
+                if ($sError !== null)
+                    $sError = $sErrorMessage;
+                return null;
+            } else {
+                $GLOBALS['bxDolClasses'][__CLASS__] = $o;
+            }
+        }
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    /**
+     * connect to database with appointed parameters
+     */
+    function connect()
+    {
+        $full_host = $this->_sHost;
+        $full_host .= $this->_sPort ? ':'.$this->_sPort : '';
+        $full_host .= $this->_sSocket ? ':'.$this->_sSocket : '';
+
+        $this->_rLink = @mysql_pconnect($full_host, $this->_sUser, $this->_sPassword);
+        if (!$this->_rLink)
+            return 'Database connect failed';
+
+        if (!$this->select_db())
+            return 'Database select failed';
+
+        mysql_query("SET NAMES 'utf8'", $this->_rLink);
+        mysql_query("SET sql_mode = ''", $this->_rLink);
+        mysql_query("SET storage_engine=" . $this->_sStorageEngine, $this->_rLink);
+
+        $GLOBALS['bx_db__rLink'] = $this->_rLink;
+
+        return '';
+    }
+
+    function select_db()
+    {
+        return @mysql_select_db($this->_sDbname, $this->_rLink);
+    }
+
+    /**
+     * close mysql connection
+     */
+    function close()
+    {
+        mysql_close($this->_rLink);
+    }
+
+    /**
+     * get mysql server info
+     */
+    function getServerInfo()
+    {
+        return mysql_get_server_info($this->_rLink);
+    }
+
+    /**
+     * get mysql option
+     */
+    function getOption($sName)
+    {
+        return $this->getOne("SELECT @@{$sName}");
+    }
+
+    /**
+     * execute sql query and return one row result
+     */
+    function getRow($query, $arr_type = MYSQL_ASSOC)
+    {
+        if(!$query)
+            return array();
+        if($arr_type != MYSQL_ASSOC && $arr_type != MYSQL_NUM && $arr_type != MYSQL_BOTH)
+            $arr_type = MYSQL_ASSOC;
+        $res = $this->res ($query);
+        $arr_res = array();
+        if($res && mysql_num_rows($res)) {
+            $arr_res = mysql_fetch_array($res, $arr_type);
+            mysql_free_result($res);
+        }
+        return $arr_res;
+    }
+
+    /**
+     * execute sql query and return a column as result
+     */
+    function getColumn($sQuery)
+    {
+        if(!$sQuery)
+            return array();
+
+        $rResult = $this->res($sQuery);
+
+        $aResult = array();
+        if($rResult) {
+            while($aRow = mysql_fetch_array($rResult, MYSQL_NUM))
+                $aResult[] = $aRow[0];
+            mysql_free_result($rResult);
+        }
+        return $aResult;
+    }
+
+    /**
+     * execute sql query and return one value result
+     */
+    function getOne($query, $index = 0)
+    {
+        if(!$query)
+            return false;
+        $res = $this->res ($query);
+        $arr_res = array();
+        if($res && mysql_num_rows($res))
+            $arr_res = mysql_fetch_array($res);
+        if(count($arr_res))
+            return $arr_res[$index];
+        else
+            return false;
+    }
+
+    /**
+     * execute sql query and return the first row of result
+     * and keep $array type and poiter to all data
+     */
+    function getFirstRow($query, $arr_type = MYSQL_ASSOC)
+    {
+        if(!$query)
+            return array();
+        if($arr_type != MYSQL_ASSOC && $arr_type != MYSQL_NUM)
+            $this->_iCurrentResType = MYSQL_ASSOC;
+        else
+            $this->_iCurrentResType = $arr_type;
+        $this->_rCurrentRes = $this->res ($query);
+        $arr_res = array();
+        if($this->_rCurrentRes && mysql_num_rows($this->_rCurrentRes))
+            $arr_res = mysql_fetch_array($this->_rCurrentRes, $this->_iCurrentResType);
+        return $arr_res;
+    }
+
+    /**
+     * return next row of pointed last getFirstRow calling data
+     */
+    function getNextRow()
+    {
+        $arr_res = mysql_fetch_array($this->_rCurrentRes, $this->_iCurrentResType);
+        if($arr_res)
+            return $arr_res;
+        else {
+            mysql_free_result($this->_rCurrentRes);
+            $this->_iCurrentResType = MYSQL_ASSOC;
+            return array();
+        }
+    }
+
+    /**
+     * return number of affected rows in current mysql result
+     */
+    function getNumRows($res = false)
+    {
+        if ($res)
+            return (int)@mysql_num_rows($res);
+        elseif (!$this->_rCurrentRes)
+            return (int)@mysql_num_rows($this->_rCurrentRes);
+        else
+            return 0;
+    }
+
+    /**
+     * execute any query return number of rows affected/false
+     */
+    function getAffectedRows()
+    {
+        return mysql_affected_rows($this->_rLink);
+    }
+
+    /**
+     * execute any query return number of rows affected/false
+     */
+    function query($query)
+    {
+        $res = $this->res($query);
+        if ($res)
+            return mysql_affected_rows($this->_rLink);
+        return false;
+    }
+
+    /**
+     * execute any query
+     */
+    function res($query, $bErrorChecking = true)
+    {
+        if(!$query)
+            return false;
+
+        if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->beginQuery($query);
+
+        $res = @mysql_query($query, $this->_rLink);
+
+        if (false === $res)
+            $this->_sErrorMessage = @mysql_error($this->_rLink); // we need to remeber last error message since mysql_ping will reset it on the next line !
+        else
+            $this->_sErrorMessage = '';
+
+        if (!$res && !@mysql_ping($this->_rLink)) { // if mysql connection is lost - reconnect and try again
+            @mysql_close($this->_rLink);
+            $sErrorMessage = $this->connect();
+            if ($sErrorMessage)
+                $this->error($sErrorMessage, true);
+            $res = mysql_query($query, $this->_rLink);
+        }
+
+        if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->endQuery($res);
+
+        if (!$res && $bErrorChecking)
+            $this->error('Database query error', false, $query);
+        return $res;
+    }
+
+    /**
+     * execute sql query and return table of records as result
+     */
+    function getAll($query, $arr_type = MYSQL_ASSOC)
+    {
+        if(!$query)
+            return array();
+
+        if($arr_type != MYSQL_ASSOC && $arr_type != MYSQL_NUM && $arr_type != MYSQL_BOTH)
+            $arr_type = MYSQL_ASSOC;
+
+        $res = $this->res ($query);
+        $arr_res = array();
+        if($res) {
+            while($row = mysql_fetch_array($res, $arr_type))
+                $arr_res[] = $row;
+            mysql_free_result($res);
+        }
+        return $arr_res;
+    }
+
+    /**
+     * execute sql query and return table of records as result
+     */
+    function fillArray($res, $arr_type = MYSQL_ASSOC)
+    {
+        if(!$res)
+            return array();
+
+        if($arr_type != MYSQL_ASSOC && $arr_type != MYSQL_NUM && $arr_type != MYSQL_BOTH)
+            $arr_type = MYSQL_ASSOC;
+
+        $arr_res = array();
+        while($row = mysql_fetch_array($res, $arr_type))
+            $arr_res[] = $row;
+        mysql_free_result($res);
+
+        return $arr_res;
+    }
+
+    /**
+     * execute sql query and return table of records as result
+     */
+    function getAllWithKey($query, $sFieldKey)
+    {
+        if(!$query)
+            return array();
+
+        $res = $this->res ($query);
+        $arr_res = array();
+        if($res) {
+            while($row = mysql_fetch_array($res, MYSQL_ASSOC)) {
+                $arr_res[$row[$sFieldKey]] = $row;
+            }
+            mysql_free_result($res);
+        }
+        return $arr_res;
+    }
+
+    /**
+     * execute sql query and return table of records as result
+     */
+    function getPairs($query, $sFieldKey, $sFieldValue)
+    {
+        if(!$query)
+            return array();
+
+        $res = $this->res ($query);
+        $arr_res = array();
+        if($res) {
+            while($row = mysql_fetch_array($res, MYSQL_ASSOC)) {
+                $arr_res[$row[$sFieldKey]] = $row[$sFieldValue];
+            }
+            mysql_free_result($res);
+        }
+        return $arr_res;
+    }
+
+    function lastId()
+    {
+        return mysql_insert_id($this->_rLink);
+    }
+
+    function getErrorMessage ()
+    {
+        $s = mysql_error($this->_rLink);
+        if ($s)
+            return $s;
+        else
+            return $this->_sErrorMessage;
+    }
+
+    function error($text, $isForceErrorChecking = false, $sSqlQuery = '')
+    {
+        if ($this->_bErrorChecking || $isForceErrorChecking)
+            $this->genMySQLErr ($text, $sSqlQuery);
+        else
+            $this->log($text . ': ' . $this->getErrorMessage());
+    }
+
+    protected function isParamInCache($sKey)
+    {
+        return is_array($this->_aParams) && isset($this->_aParams[$sKey]);
+    }
+
+    protected function cacheParams($bForceCacheInvalidate = false)
+    {
+        if ($bForceCacheInvalidate)
+            $this->cacheParamsClear();
+
+        $this->_aParams = $this->fromCache($this->_sParamsCacheName, 'getPairs', "SELECT `name`, `value` FROM `sys_options`", "name", "value");
+        if (empty($this->_aParams)) {
+            $this->_aParams = array ();
+            return false;
+        }
+
+        return true;
+    }
+
+    public function cacheParamsClear()
+    {
+        return $this->cleanCache($this->_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;
+    }
+
+    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);
+    }
+
+    function getParam($sKey, $bFromCache = true)
+    {
+        if (!$sKey)
+            return false;
+        if ($bFromCache && $this->isParamInCache($sKey)) {
+            return $this->_aParams[$sKey];
+        } else {
+            $sQuery = $this->prepare("SELECT `value` FROM `sys_options` WHERE `name` = ? LIMIT 1", $sKey);
+            return $this->getOne($sQuery);
+        }
+    }
+
+    function setParam($sKey, $mixedValue)
+    {
+        $sQuery = $this->prepare("UPDATE `sys_options` SET `value` = ? WHERE `name` = ? LIMIT 1", $mixedValue, $sKey);
+        $bResult = (int)$this->query($sQuery) > 0;
+
+        // renew params cache
+        $bResult &= $this->cacheParams(true);
+
+        return $bResult;
+    }
+
+    function listTables()
+    {
+        return mysql_list_tables($GLOBALS['db']['db'], $this->_rLink);
+    }
+
+    function getFields($sTable)
+    {
+        $rFields = mysql_list_fields($this->_sDbname, $sTable, $this->_rLink);
+        $iFields = mysql_num_fields($rFields);
+
+        $aResult = array('original' => array(), 'uppercase' => array());
+        for($i = 0; $i < $iFields; $i++) {
+            $sName = mysql_field_name($rFields, $i);
+            $aResult['original'][] = $sName;
+            $aResult['uppercase'][] = strtoupper($sName);
+        }
+
+        return $aResult;
+    }
+
+    function isFieldExists($sTable, $sFieldName)
+    {
+        $aFields = $this->getFields($sTable);
+        return in_array(strtoupper($sFieldName), $aFields['uppercase']);
+    }
+
+    function getEncoding()
+    {
+        return  mysql_client_encoding($this->_rLink) or $this->error('Database get encoding error');
+    }
+
+    function genMySQLErr($sOutput, $query = '')
+    {
+        $sParamsOutput = false;
+        $sFoundError = '';
+
+        $aBackTrace = debug_backtrace();
+        unset( $aBackTrace[0] );
+
+        if( $query ) {
+            //try help to find error
+
+            $aFoundError = array();
+
+            foreach( $aBackTrace as $aCall ) {
+
+                // truncating global settings since it repeated many times and output it separately
+                if (isset($aCall['object']) && is_a($aCall['object'], 'BxDolDb')) {
+                    if (false === $sParamsOutput)
+                        $sParamsOutput = var_export($aCall['object']->_aParams, true);
+                    $aCall['object']->_aParams = '[truncated]';
+                }
+
+                if (isset($aCall['args']) && is_array($aCall['args'])) {
+                    foreach( $aCall['args'] as $argNum => $argVal ) {
+                        if( is_string($argVal) and strcmp( $argVal, $query ) == 0 ) {
+                            $aFoundError['file']     = isset($aCall['file']) ? $aCall['file'] : (isset($aCall['class']) ? 'class: ' . $aCall['class'] : 'undefined');
+                            $aFoundError['line']     = isset($aCall['line']) ? $aCall['line'] : 'undefined';
+                            $aFoundError['function'] = $aCall['function'];
+                            $aFoundError['arg']      = $argNum;
+                        }
+                    }
+                }
+            }
+
+            if( $aFoundError ) {
+                $sFoundError = <<<EOJ
+Found error in the file '<b>{$aFoundError['file']}</b>' at line <b>{$aFoundError['line']}</b>.<br />
+Called '<b>{$aFoundError['function']}</b>' function with erroneous argument #<b>{$aFoundError['arg']}</b>.<br /><br />
+EOJ;
+            }
+        }
+
+        if (defined('BX_DB_FULL_VISUAL_PROCESSING') && BX_DB_FULL_VISUAL_PROCESSING) {
+
+            ob_start();
+
+            ?>
+                <div style="border:2px solid red;padding:4px;width:600px;margin:0px auto;">
+                    <div style="text-align:center;background-color:red;color:white;font-weight:bold;">Error</div>
+                    <div style="text-align:center;"><?php echo $sOutput; ?></div>
+            <?php
+            if (defined('BX_DB_FULL_DEBUG_MODE') && BX_DB_FULL_DEBUG_MODE) {
+                if (!empty($query))
+                    echo "<div><b>Query:</b><br />{$query}</div>";
+
+                if ($this->_rLink)
+                    echo '<div><b>Mysql error:</b><br />' . $this->getErrorMessage() . '</div>';
+
+                echo '<div style="overflow:scroll;height:300px;border:1px solid gray;">';
+                    echo $sFoundError;
+                    echo "<b>Debug backtrace:</b><br />";
+
+                    $sBackTrace = print_r($aBackTrace, true);
+                    $sBackTrace = str_replace('[password] => ' . BX_DATABASE_PASS, '[password] => *****', $sBackTrace);
+                    $sBackTrace = str_replace('[user] => ' . BX_DATABASE_USER, '[user] => *****', $sBackTrace);
+
+                    echo '<pre>' . htmlspecialchars_adv($sBackTrace) . '</pre>';
+
+                    if ($sParamsOutput) {
+                        echo '<hr />';
+                        echo "<b>Settings:</b><br />";
+                        echo '<pre>' . htmlspecialchars_adv($sParamsOutput) . '</pre>';
+                    }
+
+                    echo "<b>Called script:</b> " . $_SERVER['PHP_SELF'] . "<br />";
+                    echo "<b>Request parameters:</b><br />";
+                    echoDbg( $_REQUEST );
+                echo '</div>';
+            }
+            ?>
+                </div>
+            <?php
+
+            $sOutput = ob_get_clean();
+        } 
+
+        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";
+
+            if( strlen( $query ) )
+                $sMailBody .= "Query:  <pre>" . htmlspecialchars_adv($query) . "</pre> ";
+
+            if ($this->_rLink)
+                $sMailBody .= "Mysql error: " . $this->getErrorMessage() . "<br /><br /> ";
+
+            $sMailBody .= $sFoundError. '<br /> ';
+
+            $sBackTrace = print_r($aBackTrace, true);
+            $sBackTrace = str_replace('[password] => ' . BX_DATABASE_PASS, '[password] => *****', $sBackTrace);
+            $sBackTrace = str_replace('[user] => ' . BX_DATABASE_USER, '[user] => *****', $sBackTrace);
+            $sMailBody .= "Debug backtrace:\n <pre>" . htmlspecialchars_adv($sBackTrace) . "</pre> ";
+
+            if ($sParamsOutput)
+                $sMailBody .= "<hr />Settings:\n <pre>" . htmlspecialchars_adv($sParamsOutput) . "</pre> ";
+
+            $sMailBody .= "<hr />Called script: " . $_SERVER['PHP_SELF'] . "<br /> ";
+            $sMailBody .= "<hr />Request parameters: <pre>" . print_r( $_REQUEST, true ) . " </pre>";
+            $sMailBody .= "--\nAuto-report system\n";
+
+            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);
+    }
+
+    function setErrorChecking ($b)
+    {
+        $this->_bErrorChecking = $b;
+    }
+
+    function getDbCacheObject ()
+    {
+        if ($this->_oDbCacheObject != null) {
+            return $this->_oDbCacheObject;
+        } else {
+            $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;
+        }
+    }
+
+    function genDbCacheKey ($sName)
+    {
+        return 'db_' . $sName . '_' . bx_site_hash() . '.php';
+    }
+
+    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;
+    }
+
+    function cleanCache ($sName)
+    {
+        if (!$this->getParam('sys_db_cache_enable'))
+            return true;
+
+        $oCache = $this->getDbCacheObject ();
+
+        $sKey = $this->genDbCacheKey($sName);
+
+        return $oCache->delData($sKey);
+    }
+
+    function & fromMemory ($sName, $sFunc)
+    {
+        if (array_key_exists($sName, $GLOBALS['gl_db_cache'])) {
+            return $GLOBALS['gl_db_cache'][$sName];
+
+        } else {
+            $aArgs = func_get_args();
+            array_shift ($aArgs); // shift $sName
+            array_shift ($aArgs); // shift $sFunc
+            $GLOBALS['gl_db_cache'][$sName] = call_user_func_array (array ($this, $sFunc), $aArgs); // pass other function parameters as database function parameters
+            return $GLOBALS['gl_db_cache'][$sName];
+
+        }
+    }
+
+    function cleanMemory ($sName)
+    {
+        if (isset($GLOBALS['gl_db_cache'][$sName])) {
+            unset($GLOBALS['gl_db_cache'][$sName]);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 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.
+     */
+    function escape ($s)
+    {
+        return mysql_real_escape_string($s, $this->_rLink);
+    }
+
+    /**
+     * 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
+     */
+    function implode_escape ($mixed)
+    {
+        if (is_array($mixed)) {
+            $s = '';
+            foreach ($mixed as $v)
+                $s .= (is_numeric($v) ? $v : "'" . mysql_real_escape_string($v, $this->_rLink) . "'") . ',';
+            if ($s)
+                return substr($s, 0, -1);
+            else
+                return 'NULL';
+        }
+        return is_numeric($mixed) ? $mixed : ($mixed ? "'" . mysql_real_escape_string($mixed, $this->_rLink) . "'" : 'NULL');
+    }
+
+    /**
+     * @deprecated
+     */
+    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 string with SQL query ready for execution
+     */
+    function prepare ($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 = "'" . mysql_real_escape_string($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
+     */
+    function arrayToSQL ($a, $sDiv = ',')
+    {
+        $s = '';
+        foreach ($a as $k => $v)
+            $s .= "`{$k}` = '" . $this->escape($v) . "'" . $sDiv;
+        return trim($s, $sDiv);
+    }
+
+    function log ($s)
+    {
+        return file_put_contents(BX_DIRECTORY_PATH_LOGS . 'db.err.log', date('Y-m-d H:i:s') . "\t" . $s . "\n", FILE_APPEND);
+    }
+
+    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'));
+
+        $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);
+            $rResult = $this->res(trim($sQuery), false);
+            if(!$rResult) {
+                $aResult[] = array('query' => $sQuery, 'error' => $this->getErrorMessage());
+                if ($isBreakOnError)
+                    break;
+            }
+
+            $sQuery = "";
+        }
+        fclose($rHandler);
+
+        return empty($aResult) ? true : $aResult;
+    }
+}
+
+function getParam($sParamName, $bUseCache = true)
+{
+    return BxDolDb::getInstance()->getParam($sParamName, $bUseCache);
+}
+
+function setParam($sParamName, $sParamVal)
+{
+    return BxDolDb::getInstance()->setParam($sParamName, $sParamVal);
+}
+
+/** @} */

+ 154 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolEditor.php

@@ -0,0 +1,154 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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 BxDol implements iBxDolFactoryObject
+{
+    protected $_sObject;
+    protected $_aObject;
+
+    /**
+     * Constructor
+     * @param $aObject array of editor options
+     */
+    public 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)
+    {
+        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);
+
+        return ($GLOBALS['bxDolClasses']['BxDolEditor!'.$sObject] = $o);
+    }
+
+    /**
+     * 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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolEditorQuery.php

@@ -0,0 +1,37 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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;
+    }
+
+}
+
+/** @} */

+ 187 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolEmailTemplates.php

@@ -0,0 +1,187 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * @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 accross all templates, by default header/footer files are:
+ * - templates/base/_email_header.html
+ * - templates/base/_email_footer.html
+ *
+ * 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 BxDol implements iBxDolSingleton
+{
+    protected $_oEmailTemplatesQuery;
+
+    protected $iDefaultLangId;
+    protected $iFallbackLangId;
+    protected $aDefaultKeys;
+
+    /**
+     * Class 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();
+
+        $oLang = BxDolLanguages::getInstance();
+        $this->iDefaultLangId = $oLang->getCurrentLangId();
+        $this->iFallbackLangId = $oLang->getLangId('en');
+
+        $sAboutUsLink = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=about');
+
+        $this->aDefaultKeys = array(
+            'site_url' => BX_DOL_URL_ROOT,
+            'site_name' => getParam('site_title'),
+            'about_us' => '<a href="' . $sAboutUsLink . '">' . _t('_sys_et_txt_about_us') . '</a>',
+        );
+
+        $this->_oEmailTemplatesQuery = BxDolEmailTemplatesQuery::getInstance();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolEmailTemplates();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    /**
+     * Function will return array of needed template ;
+     *
+     * @param  string  $sTemplateName - name of necessary template.
+     * @param  integer $iAccountId    - account ID of registered member.
+     * @return array   with template 'Subject' and 'Body'.
+     */
+    function getTemplate($sTemplateName, $iAccountId = 0 )
+    {
+        $iUseLang = $this->iDefaultLangId;
+        if ($iAccountId) {
+            $oAccount = BxDolAccount::getInstance($iAccountId);
+            if ($oAccount) {
+                $aAccountInfo = $oAccount->getInfo();
+                $iUseLang = $aAccountInfo['lang_id'] ? $aAccountInfo['lang_id'] : $this->iDefaultLangId;
+            }
+        }
+
+        return $this->_oEmailTemplatesQuery->getTemplate ($sTemplateName, $iUseLang, $this->iDefaultLangId, $this->iFallbackLangId);
+    }
+
+    /**
+     * Function will return array of needed template with neccessary markers replaced ;
+     *
+     * @param  string  $sTemplateName - name of necessary template.
+     * @param  string  $aTemplatekeys - key ane value pairs to replace in subject and body.
+     * @param  integer $iAccountId    - account ID of registered member.
+     * @param  integer $iProfileId    - profile ID of registered member.
+     * @return array   with template 'Subject' and 'Body'.
+     */
+    function parseTemplate($sTemplateName, $aTemplateKeys, $iAccountId = 0, $iProfileId = 0)
+    {
+        $aTemplate = $this->getTemplate($sTemplateName, $iAccountId);
+        if (!$aTemplate)
+            return false;
+        return array(
+           'Subject' => $this->parseContent($aTemplate['Subject'], $aTemplateKeys, $iAccountId, $iProfileId),
+           'Body' => $this->parseContent($aTemplate['Body'], $aTemplateKeys, $iAccountId, $iProfileId)
+        );
+    }
+
+    function parseContent($sContent, $aKeys, $iAccountId = 0, $iProfileId = 0)
+    {
+        $aResultKeys = $this->aDefaultKeys;
+
+        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);
+
+        return BxDolTemplate::getInstance()->parseHtmlByContent($sContent, $aResultKeys, array('{', '}'));
+    }
+}
+
+/** @} */

+ 68 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolEmailTemplatesQuery.php

@@ -0,0 +1,68 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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);
+    }
+}
+
+/** @} */

+ 205 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolFile.php

@@ -0,0 +1,205 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+class BxDolFile extends BxDol 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);
+    }
+}
+
+/** @} */

+ 1533 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolForm.php

@@ -0,0 +1,1533 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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 - convert value to timestamp value before saving to database, and convert from timespamp value after restoring from database.
+ *      - DateTime - 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
+ *          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
+ *          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, DateTime, 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_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
+
+    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']]))
+                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']);
+        $sSql = $oChecker->dbInsert($this->aParams['db'], $this->aInputs, $aValsToAdd, $isIgnore);
+        if (!$sSql)
+            return false;
+        $oDb = BxDolDb::getInstance();
+        if ($oDb->res($sSql))
+            return $oDb->lastId();
+        return false;
+    }
+
+    function update ($val, $aValsToAdd = array(), &$aTrackTextFieldsChanges = null)
+    {
+        $oChecker = new BxDolFormChecker($this->_sCheckerHelper);
+        $oChecker->setFormMethod($this->aFormAttrs['method']);
+        $sSql = $oChecker->dbUpdate($val, $this->aParams['db'], $this->aInputs, $aValsToAdd, $aTrackTextFieldsChanges);
+        if (!$sSql)
+            return false;
+        return BxDolDb::getInstance()->res($sSql);
+    }
+
+    function delete ($val)
+    {
+        $oChecker = new BxDolFormChecker($this->_sCheckerHelper);
+        $oChecker->setFormMethod($this->aFormAttrs['method']);
+        $sSql = $oChecker->dbDelete($val, $this->aParams['db'], $this->aInputs);
+        if (!$sSql)
+            return false;
+        return BxDolDb::getInstance()->res($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']);
+        $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());
+    }
+
+    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;
+    }
+
+    // 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['type']) && $aInput['type'] != 'files') || !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);
+                }
+                $aNestedForms[$iFileId] = $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
+     */
+    protected function _isVisible ($aInput)
+    {
+        return BxDolAcl::getInstance()->isMemberLevelInSet($aInput['visible_for_levels']);
+    }
+
+    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 (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']]))
+                    $aInputs[$k]['value'] = bx_process_input($val);
+                continue;
+            }
+
+            $sCheckFunction = array($oChecker, 'check' . bx_gen_method_name($a['checker']['func']));
+
+            if (is_callable($sCheckFunction))
+                $bool = call_user_func_array ($sCheckFunction, !empty($a['checker']['params']) ? array_merge(array($val), $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']]))
+                $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)
+    {
+        $oDb = BxDolDb::getInstance();
+        $aValsToUpdate = array();
+        $s = '';
+
+        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'])) continue;
+            $valClean = $this->get ($a['name'], $a['db']['pass'], !empty($a['db']['params']) ? $a['db']['params'] : array());
+            $aValsToUpdate[$a['name']] = $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
+        foreach ($aValsToUpdate as $k => $val)
+            $s .= $oDb->prepare("`{$k}` = ?,", $val);
+        return $s ? substr ($s, 0, -1) : '';
+    }
+
+    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 "UPDATE `{$aDb['table']}` SET $sFields WHERE " . $oDb->prepare("`{$aDb['key']}` = ?", $val);
+    }
+
+    function dbDelete ($val, &$aDb, &$aInputs)
+    {
+        if (!$aDb['table'] || !$aDb['key'])
+            return '';
+
+        $oDb = BxDolDb::getInstance();
+
+        return "DELETE FROM `{$aDb['table']}` WHERE " . $oDb->prepare("`{$aDb['key']}` = ?", $val);
+    }
+
+    function fillWithValues (&$aInputs, &$aValues)
+    {
+        foreach ($aInputs as $k => $a) {
+            if (!isset($aValues[$k])) 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-z0-9_\-]+(\.[_a-z0-9\-]+)*@([_a-z0-9\-]+\.)+([a-z]{2}|aero|arpa|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel)$/i');
+    }
+    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);
+            }
+            return $a;
+        }
+        return self::_passDate ($s);
+    }
+    static public function _passDate ($s)
+    {
+        $iRet = bx_process_input ($s, BX_DATA_DATE_TS);
+        if (false === $iRet)
+            return 0;
+        return $iRet;
+    }
+    static public function passDateTime ($s)
+    {
+        if (is_array($s)) {
+            $a = array ();
+            foreach ($s as $k => $v) {
+                $a[$k] = self::_passDateTime ($v);
+            }
+            return $a;
+        }
+        return self::_passDateTime ($s);
+    }
+    static public function _passDateTime ($s)
+    {
+        $iRet = bx_process_input ($s, BX_DATA_DATETIME_TS);
+        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 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_TS);
+    }
+    static public function displayDateTime ($i)
+    {
+        return bx_process_output ($i, BX_DATA_DATETIME_TS);
+    }
+    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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolFormNested.php

@@ -0,0 +1,56 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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);
+    }
+}
+
+/** @} */

+ 195 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolFormQuery.php

@@ -0,0 +1,195 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * 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'] = 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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolFtp.php

@@ -0,0 +1,145 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+class BxDolFtp extends BxDolFile
+{
+    protected $_sHost;
+    protected $_sLogin;
+    protected $_sPassword;
+    protected $_rStream;
+
+    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 isTrident()
+    {
+        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;
+    }
+}
+
+/** @} */

+ 549 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolGrid.php

@@ -0,0 +1,549 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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)) {
+ *         $this->_echoResultJson(array());
+ *         exit;
+ *     }
+ *
+ *     $aIdsAffected = array ();
+ *     foreach ($aIds as $mixedId) {
+ *         if (!$this->_approve($mixedId))
+ *             continue;
+ *         $aIdsAffected[] = $mixedId;
+ *         $iAffected++;
+ *     }
+ *
+ *     $this->_echoResultJson(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 _echoResultJson 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 BxDol implements iBxDolFactoryObject, iBxDolReplaceable
+{
+    protected $_aMarkers = array ();
+
+    protected $_sObject;
+    protected $_aOptions;
+    protected $_sDefaultSortingOrder = 'ASC';
+
+    /**
+     * Constructor
+     * @param $aOptions array of grid options
+     */
+    public 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)
+    {
+        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);
+
+        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->prepare("`{$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->prepare("(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->prepare(" 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->prepare("(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->prepare(' LIMIT ?, ?', $iStart, $iPerPage);
+        return $oDb->getAll($sQuery);
+    }
+
+    protected function _getDataSqlOrderClause ($sOrderByFilter, $sOrderField, $sOrderDir)
+    {
+        $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->prepare("ORDER BY (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 = " ORDER BY `$sOrderField` $sDir";
+
+            }
+
+        } elseif ($sOrderByFilter) { // order by filter
+
+            $sOrderClause = " ORDER BY $sOrderByFilter DESC";
+
+        } elseif (!empty($this->_aOptions['field_order'])) { // order by "order" field
+
+            if (false == strpos($this->_aOptions['field_order'], ',')) {
+                $sOrderClause = " ORDER BY `" . $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 = " ORDER BY " . trim($sOrderClause, ', ');
+            }
+
+        }
+
+        return $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']);
+    }
+}
+
+/** @} */

+ 156 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolGridConnections.php

@@ -0,0 +1,156 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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';
+
+    public function __construct ($aOptions, $oTemplate = false)
+    {
+        parent::__construct ($aOptions, $oTemplate);
+        $this->_sDefaultSortingOrder = 'DESC';
+
+        $iProfileId = bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if (!$iProfileId)
+            return;
+
+        $oProfile = BxDolProfile::getInstance($iProfileId);
+        if (!$oProfile)
+            return;
+
+        $oConnection = BxDolConnection::getObjectInstance($this->_sObjectConnections);
+        if (!$oConnection)
+            return;
+
+        if ($oProfile->id() == bx_get_logged_profile_id())
+            $this->_bOwner = true;
+
+        $aSQLParts = $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()
+    {
+        $iId = 0;
+        $aIds = bx_get('ids');
+        if ($aIds && is_array($aIds))
+            $iId = (int)array_pop($aIds);
+
+        if (!$iId) {
+            $this->_echoResultJson(array('msg' => _t('_sys_txt_error_occured')), true);
+            exit;
+        }
+
+        $oConn = BxDolConnection::getObjectInstance($this->_sObjectConnections);
+
+        $a = $oConn->actionAdd($iId);
+        if (isset($a['err']) && $a['err'])
+            $this->_echoResultJson(array('msg' => $a['msg']), true);
+        else
+            $this->_echoResultJson(array('grid' => $this->getCode(false), 'blink' => $iId), true);
+    }
+
+    /**
+     * 'add friend' action handler
+     */
+    public function performActionAddFriend()
+    {
+        return $this->performActionAccept();
+    }
+
+    protected function _delete ($mixedId)
+    {
+        $oConn = BxDolConnection::getObjectInstance($this->_sObjectConnections);
+
+        if ($oConn->isConnected(bx_get_logged_profile_id(), (int)$mixedId, true))
+            $a = $oConn->actionRemove($mixedId);
+        else
+            $a = $oConn->actionReject($mixedId);
+
+        return isset($a['err']) && $a['err'] ? false : 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 = '';
+        $oConn = BxDolConnection::getObjectInstance($this->_sObjectConnections);
+
+        // for friend requests display mutual friends
+        if ($this->_bOwner && !$aRow['mutual']) {
+            $a = $oConn->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 = $oConn->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 '';
+
+        $oConn = BxDolConnection::getObjectInstance($this->_sObjectConnections);
+        if ($oConn->isConnected($aRow['id'], bx_get_logged_profile_id()) || $oConn->isConnected(bx_get_logged_profile_id(), $aRow['id']))
+            return '';
+
+        return parent::_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+}
+
+/** @} */

+ 88 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolGridQuery.php

@@ -0,0 +1,88 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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;
+    }
+
+}
+
+/** @} */

+ 274 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolImageResize.php

@@ -0,0 +1,274 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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 BxDol 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
+
+    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 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();
+                    })
+                    ->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;        
+    }
+}
+
+/** @} */

+ 120 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolInformer.php

@@ -0,0 +1,120 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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 BxDol
+{
+    protected $_aMessages = array();
+
+    /**
+     * Constructor
+     */
+    public 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);
+    }
+
+    /**
+     * 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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolInstallerHasher.php

@@ -0,0 +1,85 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolInstallerUtils.php

@@ -0,0 +1,170 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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();
+
+    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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolKey.php

@@ -0,0 +1,132 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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 BxDol implements iBxDolSingleton
+{
+    protected $_oQuery;
+
+    /**
+     * Constructor
+     */
+    public 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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolKeyQuery.php

@@ -0,0 +1,52 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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);
+    }
+}
+
+/** @} */

+ 348 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolLanguages.php

@@ -0,0 +1,348 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+define('BX_DOL_LANGUAGE_DEFAULT', 'en');
+
+define('BX_DOL_LANGUAGE_CATEGORY_SYSTEM', 1);
+define('BX_DOL_LANGUAGE_CATEGORY_CUSTOM', 2);
+
+class BxDolLanguages extends BxDol implements iBxDolSingleton
+{
+    protected $oDb;
+    protected $sCurrentLanguage;
+
+    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 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);
+        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();
+}
+
+/** @} */

+ 293 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolLanguagesQuery.php

@@ -0,0 +1,293 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+class BxDolLanguagesQuery extends BxDolDb implements iBxDolSingleton
+{
+    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)
+    {
+        $sSql = $this->prepare("SELECT `ID` FROM `sys_localization_languages` WHERE `Name`=? AND `Enabled`='1' LIMIT 1", $sName);
+
+        if($bFromCache)
+            return (int)$this->fromCache('checkLangExists_' . $sName, 'getOne', $sSql);
+        else
+            return (int)$this->getOne($sSql);
+    }
+    function getLanguages($bIdAsKey = false, $bActiveOnly = false)
+    {
+        $sSql = "SELECT * FROM `sys_localization_languages` WHERE 1 " . ($bActiveOnly ? " AND `Enabled`='1'" : "") . " ORDER BY `Title` ASC";
+        return $this->getPairs($sSql, $bIdAsKey ? 'ID' : 'Name', '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';
+                $sWhereClause .= $this->prepare(" AND `tl`.`ID`=?", $aParams['value']);
+                break;
+            case 'by_name':
+                $aMethod['name'] = 'getRow';
+                $sWhereClause .= $this->prepare(" AND `tl`.`Name`=?", $aParams['value']);
+                break;
+            case 'default':
+                $aMethod['name'] = 'getRow';
+                $sWhereClause .= $this->prepare(" AND `tl`.`Name`=?", getParam('lang_default'));
+                break;
+            case 'active':
+                $sWhereClause = " AND `tl`.`Enabled`='1'";
+                break;
+            case 'all':
+                $sOrderClause = " `tl`.`Name` ASC ";
+                break;
+            case 'all_by_id':
+                $sWhereClause .= $this->prepare(" AND `tl`.`ID`=?", $aParams['value']);
+                $sOrderClause = " `tl`.`Name` ASC ";
+                break;
+			case 'all_by_name':
+                $sWhereClause .= $this->prepare(" AND `tl`.`Name`=?", $aParams['value']);
+                $sOrderClause = " `tl`.`Name` ASC ";
+                break;
+            case 'all_key_id':
+                $aMethod['name'] = 'getAllWithKey';
+                $aMethod['params'][1] = 'id';
+
+                if(isset($aParams['language']) && (int)$aParams['language'] != 0)
+                    $sWhereClause .= $this->prepare(" AND `tl`.`ID`=?", $aParams['language']);
+                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)
+                    $sWhereClause .= $this->prepare(" AND `tl`.`ID`=?", $aParams['language']);
+                break;
+
+            case 'by_name':
+                $aMethod['name'] = 'getRow';
+                $sWhereClause = $this->prepare(" AND `tk`.`Key`=? ", $aParams['value']);
+                break;
+
+			case 'by_language_name_key_key':
+            	$aMethod['name'] = 'getAllWithKey';
+            	$aMethod['params'][1] = 'key';
+
+                $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 = $this->prepare(" AND `tl`.`Name`=? ", $aParams['value']);
+                break;
+
+            case 'by_language_id_key_key':
+            	$aMethod['name'] = 'getAllWithKey';
+            	$aMethod['params'][1] = 'key';
+
+                $sSelectClause .= ", `ts`.`String` AS `string` ";
+                $sJoinClause = " LEFT JOIN `sys_localization_strings` AS `ts` ON `tk`.`ID`=`ts`.`IDKey` ";
+                $sWhereClause = $this->prepare(" AND `ts`.`IDLanguage`=? ", (int)$aParams['value']);
+                break;
+
+            case 'search':
+                $aMethod['name'] = 'getColumn';
+
+                $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)
+                    $sWhereClause .= $this->prepare(" AND `tc`.`ID`=?", $aParams['category']);
+
+                if(isset($aParams['language']) && (int)$aParams['language'] != 0)
+                    $sWhereClause .= $this->prepare(" AND `tl`.`ID`=?", $aParams['language']);
+
+                if(isset($aParams['keyword']) && $aParams['keyword'] != '' && $aParams['keyword'] != _t('_adm_pgt_txt_keyword'))
+                    $sWhereClause .= $this->prepare(" AND (`tk`.`Key` LIKE ? OR `ts`.`String` LIKE ?)", '%' . $aParams['keyword'] . '%', '%' . $aParams['keyword'] . '%');
+
+                $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'];
+
+                $sLimitClause .= $this->prepare(" LIMIT ?, ?", $iStart, $iLength);
+                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';
+                $sSelectClause = "`tc`.`ID` AS `id`";
+                $sWhereClause = $this->prepare(" AND `tc`.`Name`=?", $aParams['value']);
+                break;
+            case 'by_name':
+                $aMethod['name'] = 'getRow';
+                $sWhereClause = $this->prepare(" AND `tc`.`Name`=?", $aParams['value']);
+                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';
+                $sJoinClause = "LEFT JOIN `sys_localization_keys` AS `tk` ON `ts`.`IDKey`=`tk`.`ID`";
+                $sWhereClause .= $this->prepare("AND `tk`.`Key`=? AND `ts`.`IDLanguage`=?", $aParams['key'], $aParams['language_id']);
+                break;
+
+            case 'all_by_key_key_language_id':
+                $aMethod['name'] = 'getAllWithKey';
+                $aMethod['params'][1] = 'language_id';
+
+                $sJoinClause = "LEFT JOIN `sys_localization_keys` AS `tk` ON `ts`.`IDKey`=`tk`.`ID`";
+                $sWhereClause .= $this->prepare("AND `tk`.`Key`=?", $aParams['value']);
+                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()");
+    }
+}
+
+/** @} */

+ 233 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolLiveUpdates.php

@@ -0,0 +1,233 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * @page objects
+ * @section live updates Live Updates
+ * @ref BxDolLiveUpdates
+ */
+
+class BxDolLiveUpdates extends BxDol
+{
+	protected $_oQuery;
+    protected $_aSystems;
+    protected $_aSystemsActive;
+
+    protected $_iInterval;
+
+	protected $_iCacheTTL;  
+	protected $_sCacheKey;
+
+    protected $_sJsClass;
+    protected $_sJsObject;
+
+    public 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':
+    				$aRequested = $this->_getRequestedData();
+    				foreach($aRequested as $sName => $aData)
+    					$aCached[$sType][$sName] = $aData['count'];
+    				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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolLiveUpdatesQuery.php

@@ -0,0 +1,36 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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'];
+    }
+}
+
+/** @} */

+ 269 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolMenu.php

@@ -0,0 +1,269 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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 BxDol implements iBxDolFactoryObject, iBxDolReplaceable
+{
+    protected static $SEL_MODULE = '';
+    protected static $SEL_NAME = '';
+
+    protected $_sSelModule;
+    protected $_sSelName;
+
+    protected $_sObject;
+    protected $_aObject;
+    protected $_oQuery;
+    protected $_oPermalinks;
+    protected $_aMarkers = array ();
+
+    /**
+     * Constructor
+     * @param $aObject array of menu options
+     */
+    public function __construct($aObject)
+    {
+        parent::__construct();
+
+        $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;
+    }
+
+    /**
+     * 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;
+    }
+
+}
+
+/** @} */

+ 70 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolMenuQuery.php

@@ -0,0 +1,70 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * 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` != 9999 ORDER BY `order` DESC LIMIT 1", $aMenuItem['set_name']);
+            $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()
+    {
+        $sQuery = $this->prepare("SELECT * FROM `sys_menu_items` WHERE `set_name` = ? ORDER BY `order` ASC", $this->_aObject['set_name']);
+        return $this->getAll($sQuery);
+    }
+
+}
+
+/** @} */

+ 535 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolMetatags.php

@@ -0,0 +1,535 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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 BxDol implements iBxDolFactoryObject
+{
+    protected $_sObject;
+    protected $_aObject;
+    protected $_oQuery;
+    protected $_aMetas = array ();
+
+    /**
+     * Constructor
+     * @param $aObject array of metags object options
+     */
+    public 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 = $aObject['override_class_name'] ? $aObject['override_class_name'] : 'BxDolMetatags';
+        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;
+    }
+
+    /**
+     * 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;
+        }
+
+        $a[0] = array_unique($a[0]);
+
+        return $this->_oQuery->keywordsAdd($iId, array_slice($a[0], 0, BX_METATAGS_KEYWORDS_MAX));
+    }
+
+    /**
+     * 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 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
+     * @return true if location was added, or false otherwise
+     */
+    public function locationsAdd($iId, $sLatitude, $sLongitude, $sCountryCode, $sState, $sCity, $sZip = '') 
+    {
+        return $this->_oQuery->locationsAdd($iId, $sLatitude, $sLongitude, $sCountryCode, $sState, $sCity, $sZip);
+    }
+
+    /**
+     * 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'));
+    }
+
+    /**
+     * Get locations string with links
+     * @param $iId content id
+     * @return string with links to country and city
+     */
+    public function locationsString($iId)
+    {
+        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 (!$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>';
+            
+        return _t('_sys_location_country_city', $sCountryUrl, $sCityUrl);
+    }
+
+    /**
+     * 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
+     */
+    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;
+    }
+
+}
+
+/** @} */

+ 96 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolMetatagsQuery.php

@@ -0,0 +1,96 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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)
+    {
+        $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)
+    {
+        $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` = ?", $mixedContentId, $fLat, $fLng, $sCountryCode, $sState, $sCity, $sZip);
+        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);
+    }
+}
+
+/** @} */

+ 169 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolModule.php

@@ -0,0 +1,169 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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
+     */
+    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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolModuleConfig.php

@@ -0,0 +1,129 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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
+     */
+    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;
+    }
+}
+
+/** @} */

+ 179 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolModuleQuery.php

@@ -0,0 +1,179 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+class BxDolModuleQuery extends BxDolDb implements iBxDolSingleton
+{
+    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)
+    {
+        $sSql = $this->prepare("SELECT * FROM `sys_modules` WHERE `id`=? LIMIT 1", $iId);
+        return $this->fromMemory('sys_modules_' . $iId, 'getRow', $sSql);
+    }
+    function getModuleByName($sName)
+    {
+        $sSql = $this->prepare("SELECT * FROM `sys_modules` WHERE `name`=? LIMIT 1", $sName);
+        return $this->fromMemory('sys_modules_' . $sName, 'getRow', $sSql);
+    }
+    function getModuleByUri($sUri)
+    {
+        $sSql = $this->prepare("SELECT * FROM `sys_modules` WHERE `uri`=? LIMIT 1", $sUri);
+        return $this->fromMemory('sys_modules_' . $sUri, '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())
+    {
+    	$aMethod = array('name' => 'getAll', 'params' => array(0 => 'query'));
+        $sPostfix = $sWhereClause = $sOrderByClause = "";
+
+        switch($aParams['type']) {
+            case 'modules':
+                $sPostfix .= '_modules';
+                $sWhereClause .= $this->prepare(" AND `type`=?", BX_DOL_MODULE_TYPE_MODULE);
+                break;
+            case 'languages':
+                $sPostfix .= '_languages';
+                $sWhereClause .= $this->prepare(" AND `type`=?", BX_DOL_MODULE_TYPE_LANGUAGE);
+                break;
+            case 'templates':
+                $sPostfix .= '_templates';
+                $sWhereClause .= $this->prepare(" AND `type`=?", BX_DOL_MODULE_TYPE_TEMPLATE);
+                break;
+            case 'path_and_uri':
+            	$aMethod['name'] = 'getRow';
+            	$sWhereClause .= $this->prepare(" AND `path`=? AND `uri`=?", $aParams['path'], $aParams['uri']);
+            	break;
+        }
+
+        if(isset($aParams['active'])) {
+            $sPostfix .= "_active";
+            $sWhereClause .= $this->prepare(" AND `enabled`=?", (int)$aParams['active']);
+        }
+
+        $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(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` FROM `sys_modules` WHERE `dependencies` LIKE '%" . $this->escape($sUri) . "%'";
+        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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolModuleTemplate.php

@@ -0,0 +1,102 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+class BxDolModuleTemplate extends BxDolTemplate
+{
+    protected $_oDb;
+    protected $_oConfig;
+    protected $_bObStarted = 0;
+
+    /*
+     * Constructor.
+     */
+    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;
+        }
+    }
+}
+
+/** @} */

+ 253 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolPage.php

@@ -0,0 +1,253 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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 BxDol implements iBxDolFactoryObject, iBxDolReplaceable
+{
+    protected $_sObject;
+    protected $_aObject;
+    protected $_oQuery;
+    protected $_aMarkers = array ();
+
+    /**
+     * Constructor
+     * @param $aObject array of page options
+     */
+    public 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)
+    {
+        $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;
+
+				$sPageObject = BxDolService::call($aModule['name'], 'get_page_object_for_page_trigger', array($sPageTriggerName));
+                if (!$sPageObject)
+                    continue;
+
+                $aPageBlock['object'] = $sPageObject;
+                BxDolPageQuery::addPageBlockToPage($aPageBlock);
+            }
+        }
+
+        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']);
+    }
+
+}
+
+/** @} */

+ 354 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolPaginate.php

@@ -0,0 +1,354 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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)
+            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,
+        );
+
+        $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;"' : '';
+    }
+
+}
+
+/** @} */

+ 259 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolPermalinks.php

@@ -0,0 +1,259 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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;
+
+    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
+     * @returm 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;
+    }
+}
+
+/** @} */

+ 303 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolPrivacy.php

@@ -0,0 +1,303 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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 BxDol implements iBxDolFactoryObject
+{
+    protected $_oDb;
+    protected $_sObject;
+    protected $_aObject;
+
+    /**
+     * Constructor
+     * @param $aObject array of grid options
+     */
+    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 = self::getFieldName($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 $sAction action name.
+     * @return string with field name.
+     */
+    public static function getFieldName($sAction)
+    {
+        return 'allow_' . strtolower(str_replace(' ', '-', $sAction)) . '_to';
+    }
+
+    /**
+     * 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' => self::getFieldName($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)
+    {
+        $mixedPrivacyGroups = BX_DOL_PG_ALL;
+        if(isLogged()) {
+            $iProfileIdLogged = bx_get_logged_profile_id();
+            if($iProfileIdLogged == $iProfileIdOwner)
+                return array();
+
+            if ($iProfileIdOwner && $this->checkConnections($iProfileIdOwner, bx_get_logged_profile_id()))
+                $mixedPrivacyGroups = array(BX_DOL_PG_ALL, BX_DOL_PG_MEMBERS, BX_DOL_PG_FRIENDS);
+            else
+                $mixedPrivacyGroups = array(BX_DOL_PG_ALL, BX_DOL_PG_MEMBERS);
+        }
+        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 = self::getFieldName($this->_aObject['action']);
+        return $this->_oDb->getContentByGroupAsSQLPart($sField, $mixedGroupId);
+    }
+
+    /**
+     * 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 = bx_get_logged_profile_id();
+
+        $aObject = $this->_oDb->getObjectInfo($this->getFieldName($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 checkConnections($iOwnerId, $iViewerId)
+    {
+        return BxDolConnection::getObjectInstance('sys_profiles_friends')->isConnected($iOwnerId, $iViewerId, true);
+    }
+
+    protected function getCheckMethod($s)
+    {
+        if(substr($s, 0, 1) != '@')
+            return false;
+
+        return bx_gen_method_name(str_replace('@', 'check_', $s));
+    }
+
+    /**
+     * 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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolPrivacyQuery.php

@@ -0,0 +1,186 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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->prepare("`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->prepare(" 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);
+    }
+}
+
+/** @} */

+ 419 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolProfile.php

@@ -0,0 +1,419 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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
+
+class BxDolProfile extends BxDol 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 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);
+    }
+
+    /**
+     * Get profile status
+     */
+    public function getStatus($iProfileId = false)
+    {
+        $aInfo = $this->_oQuery->getInfoById((int)$iProfileId ? $iProfileId : $this->_iProfileID);
+        return $aInfo['status'];
+    }
+
+    /**
+     * 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 thumbnail 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 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']) {
+            $oProfileAccount = BxDolProfile::getInstanceAccountProfile($aProfileInfo['account_id']);
+            $oAccount->updateProfileContext($oProfileAccount->id());
+        }
+
+        // create system event before deletion
+        $isStopDeletion = false;
+        bx_alert('profile', 'before_delete', $ID, 0, array('delete_with_content' => $bDeleteWithContent, 'stop_deletion' => &$isStopDeletion));
+        if ($isStopDeletion)
+            return false;
+
+        // delete associated 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));
+        return $iProfileId;
+    }
+
+    /**
+     * Change profile status to 'Active'
+     */
+    public function activate($iAction, $iProfileId = 0)
+    {
+        $sStatus = $this->getStatus($iProfileId);
+        return $this->changeStatus(BX_PROFILE_STATUS_ACTIVE, BX_PROFILE_STATUS_PENDING == $sStatus ? 'approve' : 'activate', $iAction, $iProfileId);
+    }
+
+    /**
+     * Change profile status from 'Pending' to the next level - 'Active'
+     */
+    public function approve($iAction, $iProfileId = 0)
+    {
+        return $this->changeStatus(BX_PROFILE_STATUS_ACTIVE, 'approve', $iAction, $iProfileId);
+    }
+
+    /**
+     * Change profile status to 'Pending'
+     */
+    public function disapprove($iAction, $iProfileId = 0)
+    {
+        return $this->changeStatus(BX_PROFILE_STATUS_PENDING, 'disapprove', $iAction, $iProfileId);
+    }
+
+    /**
+     * Change profile status to 'Suspended'
+     */
+    public function suspend($iAction, $iProfileId = 0)
+    {
+        return $this->changeStatus(BX_PROFILE_STATUS_SUSPENDED, 'suspend', $iAction, $iProfileId);
+    }
+
+    protected function changeStatus($sStatus, $sAlertActionName, $iAction, $iProfileId = 0)
+    {
+        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
+        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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolProfileForms.php

@@ -0,0 +1,45 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * Functions to automate profile creation/editing/deletion forms.
+ */
+class BxDolProfileForms extends BxDol
+{
+    /**
+     * 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);
+    }
+
+}
+
+/** @} */

+ 238 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolProfileQuery.php

@@ -0,0 +1,238 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * All queries related to profiles
+ */
+class BxDolProfileQuery extends BxDolDb implements iBxDolSingleton
+{
+    public 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)
+    {
+        $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);
+    }
+
+    /**
+     * 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 
+     * @code
+     * 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
+    }
+}
+
+/** @} */

+ 126 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolProfileUndefined.php

@@ -0,0 +1,126 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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 BxDol 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 '<div>' . $this->getDisplayName() . '</div>';
+    }
+
+    /**
+     * 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 profile edit url
+     */
+    public function getEditUrl()
+    {
+        return '';
+    }
+
+    /**
+     * Check if profile is active
+     */
+    public function isActive()
+    {
+        return true;
+    }
+}
+
+/** @} */

+ 154 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolRequest.php

@@ -0,0 +1,154 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolRss.php

@@ -0,0 +1,96 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident 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 BxDol implements iBxDolFactoryObject
+{
+	protected $_oDb;
+	protected $_sObject;
+    protected $_aObject;
+
+    public static $bInitialized = false;
+
+    /**
+     * Constructor
+     */
+    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/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolRssBoonEx.php

@@ -0,0 +1,31 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+class BxDolRssBoonEx extends BxTemplRss
+{
+	protected $aFeeds;
+
+    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];
+    }
+}
+
+/** @} */

+ 32 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolRssPageBlock.php

@@ -0,0 +1,32 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+class BxDolRssPageBlock extends BxTemplRss
+{
+    function __construct($aObject)
+    {
+        parent::__construct($aObject);
+    }
+
+    public function getUrl($mixedId) {
+	    $oPageQuery = new BxDolPageQuery(array());
+
+	    $sContent = $oPageQuery->getPageBlockContent($mixedId);
+	    if(!$sContent)
+			return false;
+
+		list($sUrl) = explode('#', $sContent);
+		if(!$sUrl)
+			return false;
+
+		return $sUrl;
+    }
+}
+
+/** @} */

+ 33 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolRssQuery.php

@@ -0,0 +1,33 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+class BxDolRssQuery extends BxDolDb
+{
+
+    public function __construct($aObject = array())
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+    }
+
+    static public function getRssObject($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_rss` WHERE `object` = ?", $sObject);
+
+        $aObject = $oDb->getRow($sQuery);
+        if(!$aObject || !is_array($aObject))
+            return false;
+
+        return $aObject;
+    }
+}
+
+/** @} */

+ 858 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolSearch.php

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

+ 109 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolService.php

@@ -0,0 +1,109 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * Service calls to modules' methods.
+ *
+ * The class has one static method to make service calls to the module's methods
+ *
+ *
+ * Example of usage:
+ * @code
+ * $isSpam = BxDolService::call('bx_antispam', 'is_spam', array($sText));
+ * @endcode
+ *
+ *
+ * Memberships/ACL:
+ * Doesn't depend on user's membership.
+ *
+ *
+ * Alerts:
+ * no alerts available
+ *
+ */
+class BxDolService extends BxDol
+{
+    /**
+     * Perform serice call
+     * @param $mixed module name or module id
+     * @param $sMethod service method name in format 'method_name', corresponding class metod is serviceMethodName
+     * @param $aParams params to pass to service method
+     * @param $sClass class to search for service method, by default it is main module class
+     * @return service call result
+     */
+    public static function call($mixed, $sMethod, $aParams = array(), $sClass = 'Module')
+    {
+        $oDb = BxDolModuleQuery::getInstance();
+
+        $aModule = array();
+        if(is_string($mixed))
+            $aModule = $oDb->getModuleByName($mixed);
+        else
+            $aModule = $oDb->getModuleById($mixed);
+
+        return empty($aModule) ? '' : BxDolRequest::processAsService($aModule, $sMethod, $aParams, $sClass);
+    }
+
+    /**
+     * Perform serice call by accepting serialized array of service call parameters:
+     * @code
+     *     array (
+     *         'module' => 'system', // module name
+     *         'method' => 'test', // service method name
+     *         'params' => array(), // array of parameters to pass to service method
+     *         'class' => 'Module', // class to search service method in
+     *     )
+     * @endcode
+     *
+     * @param $s serialized array of serice call
+     * @param $aMarkers service method name in format 'method_name', corresponding class metod is serviceMethodName
+     * @param $sReplaceIn params to pass to service method
+     * @return service call result
+     */
+    public static function callSerialized($s, $aMarkers = array(), $sReplaceIn = 'params')
+    {
+        $a = @unserialize($s);
+        if (false === $a || !is_array($a))
+            return '';
+
+        if (isset($a[$sReplaceIn]))
+            $a[$sReplaceIn] = bx_replace_markers($a[$sReplaceIn], $aMarkers);
+
+        return self::call($a['module'], $a['method'], isset($a['params']) ? $a['params'] : array(), isset($a['class']) ? $a['class'] : 'Module');
+    }
+
+    /**
+     * Check if string is serialized array
+     */
+    public static function isSerializedService($s)
+    {
+        return preg_match('/^a:[\d+]:\{/', $s);
+    }
+
+    /**
+     * Serialized service call array
+     */
+    public static function getSerializedService($mixedModule, $sMethod, $aParams = array(), $sClass = '')
+    {
+		$aService = array(
+			'module' => $mixedModule,
+			'method' => $sMethod,
+		);
+
+		if(!empty($aParams))
+			$aService['params'] = $aParams;
+
+		if(!empty($sClass))
+			$aService['class'] = $sClass;
+
+		return serialize($aService);
+    }
+}
+
+/** @} */

+ 170 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolSession.php

@@ -0,0 +1,170 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+define('BX_DOL_SESSION_LIFETIME', 3600);
+define('BX_DOL_SESSION_COOKIE', 'memberSession');
+
+class BxDolSession extends BxDol implements iBxDolSingleton
+{
+    protected $oDb;
+    protected $sId;
+    protected $iUserId;
+    protected $aData;
+
+    private function __construct()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+
+        $this->oDb = new BxDolSessionQuery();
+        $this->sId = '';
+        $this->iUserId = 0;
+        $this->aData = array();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__]))
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolSession();
+
+        if(!$GLOBALS['bxDolClasses'][__CLASS__]->getId())
+            $GLOBALS['bxDolClasses'][__CLASS__]->start();
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    function start()
+    {
+        if (defined('BX_DOL_CRON_EXECUTE'))
+            return true;
+
+        if($this->exists($this->sId))
+            return true;
+
+        $this->sId = genRndPwd(32, true);
+
+        $aUrl = parse_url(BX_DOL_URL_ROOT);
+        $sPath = isset($aUrl['path']) && !empty($aUrl['path']) ? $aUrl['path'] : '/';
+        setcookie(BX_DOL_SESSION_COOKIE, $this->sId, 0, $sPath, '', false, true);
+
+        $this->save();
+        return true;
+    }
+
+    function destroy()
+    {
+        $aUrl = parse_url(BX_DOL_URL_ROOT);
+        $sPath = isset($aUrl['path']) && !empty($aUrl['path']) ? $aUrl['path'] : '/';
+        setcookie(BX_DOL_SESSION_COOKIE, '', time() - 86400, $sPath, '', false, true);
+        unset($_COOKIE[BX_DOL_SESSION_COOKIE]);
+
+        $this->oDb->delete($this->sId);
+
+        $this->sId = '';
+        $this->iUserId = 0;
+        $this->aData = array();
+    }
+
+    function exists($sId = '')
+    {
+        if(empty($sId) && isset($_COOKIE[BX_DOL_SESSION_COOKIE]))
+            $sId = bx_process_input($_COOKIE[BX_DOL_SESSION_COOKIE]);
+
+        $mixedSession = array();
+        if(($mixedSession = $this->oDb->exists($sId)) !== false) {
+            $this->sId = $mixedSession['id'];
+            $this->iUserId = (int)$mixedSession['user_id'];
+            $this->aData = unserialize($mixedSession['data']);
+            return true;
+        } else
+            return false;
+    }
+
+    function getId()
+    {
+        return $this->sId;
+    }
+
+    function setUserId($iUserId)
+    {
+        $this->iUserId = $iUserId;
+        $this->save();
+    }
+
+    function setValue($sKey, $mixedValue)
+    {
+        if(empty($this->sId))
+            $this->start();
+
+        $this->aData[$sKey] = $mixedValue;
+        $this->save();
+    }
+
+    function unsetValue($sKey)
+    {
+        if(empty($this->sId))
+            $this->start();
+
+        unset($this->aData[$sKey]);
+
+        if(!empty($this->aData))
+            $this->save();
+        else
+            $this->destroy();
+    }
+
+    function getValue($sKey)
+    {
+        if(empty($this->sId))
+            $this->start();
+
+        return isset($this->aData[$sKey]) ? $this->aData[$sKey] : false;
+    }
+
+    function getUnsetValue($sKey)
+    {
+        $mixedValue = $this->getValue($sKey);
+        if($mixedValue !== false)
+            $this->unsetValue($sKey);
+
+        return $mixedValue;
+    }
+
+    public function maintenance()
+    {
+        return $this->oDb->deleteExpired();
+    }
+
+    protected function save()
+    {
+        if($this->iUserId == 0)
+            $this->iUserId = getLoggedId();
+
+        $this->oDb->save($this->sId, array(
+            'user_id' => $this->iUserId,
+            'data' => serialize($this->aData)
+        ));
+    }
+}
+
+/** @} */

+ 56 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolSessionQuery.php

@@ -0,0 +1,56 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * @see BxDolSession
+ */
+class BxDolSessionQuery extends BxDolDb
+{
+    protected $sTable;
+
+    function __construct()
+    {
+        parent::__construct();
+
+        $this->sTable = 'sys_sessions';
+    }
+    function getTableName()
+    {
+        return $this->sTable;
+    }
+    function exists($sId)
+    {
+        $sSql = $this->prepare("SELECT `id`, `user_id`, `data` FROM `" . $this->sTable . "` WHERE `id`=? LIMIT 1", $sId);
+        $aSession = $this->getRow($sSql);
+        return !empty($aSession) ? $aSession : false;
+    }
+    function save($sId, $aSet)
+    {
+        $sSetClause = $this->prepare("`id`=?", $sId);
+        foreach($aSet as $sKey => $sValue)
+            $sSetClause .= $this->prepare(", `" . $sKey . "`=?", $sValue);
+        $sSetClause .= ", `date`=UNIX_TIMESTAMP()";
+
+        return (int)$this->query("REPLACE INTO `" . $this->sTable . "` SET " . $sSetClause) > 0;
+    }
+    function delete($sId)
+    {
+        $sSql = $this->prepare("DELETE FROM `" . $this->sTable . "` WHERE `id`=? LIMIT 1", $sId);
+        return (int)$this->query($sSql) > 0;
+    }
+    function deleteExpired()
+    {
+        $sSql = $this->prepare("DELETE FROM `" . $this->sTable . "` WHERE `date` < (UNIX_TIMESTAMP() - ?)", BX_DOL_SESSION_LIFETIME);
+        $iRet = (int)$this->query($sSql);
+        $this->query("OPTIMIZE TABLE `" . $this->sTable . "`");
+        return $iRet;
+    }
+}
+
+/** @} */

+ 127 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolSocialSharing.php

@@ -0,0 +1,127 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * @page objects
+ * @section social_sharing Social Sharing Buttons
+ * @ref BxDolSocialSharing
+ */
+
+/**
+ * Social sharing buttons for any content
+ *
+ * It displays sharing buttons from popular social networks, like facebook, twitter, gogole plus, etc.
+ *
+ *
+ * @section example Example of usage:
+ *
+ * @code
+ * echo BxTemplSocialSharing::getInstance()->getCode($iId, $sModuleName, $sUrl, $sTitle);
+ * @endcode
+ *
+ *
+ * @section alerts Alerts:
+ *
+ * Type/unit: system
+ * Action: social_sharing_display
+ * Options:
+ *      buttons - reference to buttons array
+ *      markers - reference to variables for replacement
+ *      override_output - override output string
+ *
+ */
+class BxDolSocialSharing extends BxDol implements iBxDolSingleton
+{
+    protected $_oQuery;
+    protected $_aSocialButtons = array (); // active social buttons array
+
+    /**
+     * Constructor
+     */
+    protected function __construct()
+    {
+        parent::__construct();
+        $this->_oQuery = new BxDolSocialSharingQuery();
+        $this->_aSocialButtons = $this->_oQuery->getActiveButtons();
+    }
+
+    /**
+     * Get object instance
+     * @param $sObject object name
+     * @return object instance
+     */
+    static public function getInstance()
+    {
+        if (isset($GLOBALS['bxDolClasses']['BxDolSocialSharing']))
+            return $GLOBALS['bxDolClasses']['BxDolSocialSharing'];
+
+        $o = new BxTemplSocialSharing();
+
+        return ($GLOBALS['bxDolClasses']['BxDolSocialSharing'] = $o);
+    }
+
+    public function getCode ($sContentId, $sModuleName, $sUrl, $sTitle, $aCustomVars = false)
+    {
+        // overrided in template class
+    }
+
+    /**
+     * Replace provided markers in string.
+     * @param $s - string to replace markers in
+     * @param $a - markers array
+     * @return string with replaces markers
+     */
+    protected function _replaceMarkers ($mixed, $aMarkers)
+    {
+        if (empty($mixed) || empty($aMarkers) || !is_array($aMarkers))
+            return $mixed;
+        return bx_replace_markers($mixed, $aMarkers);
+    }
+
+    /**
+     * Get most facebook locale for provided language code.
+     * @param $sLang lang code
+     * @return locale string or empty string if no lacale is found
+     */
+    protected function _getLocaleFacebook ($sLang)
+    {
+        $aLocales = $this->_getLocalesFacebook();
+        if (!$aLocales || !isset($aLocales[$sLang]))
+            return '';
+        return $aLocales[$sLang];
+    }
+
+    /**
+     * Get facebook locales
+     * @return locales array, lang is array key and locale is array value
+     */
+    protected function _getLocalesFacebook ()
+    {
+        $oCache = $this->_oQuery->getDbCacheObject();
+        $sCacheKey = $this->_oQuery->genDbCacheKey('sys_social_sharing_locales_fb');
+        $aData = $oCache->getData($sCacheKey);
+        if (null === $aData) {
+            $sXML = bx_file_get_contents ('http://www.facebook.com/translations/FacebookLocales.xml');
+            if (!$sXML)
+                return false;
+            $xmlLocates = new SimpleXMLElement($sXML);
+            $aData = array ();
+            foreach ($xmlLocates->locale as $xmlLocale) {
+                $sLocale = (string)($xmlLocale->codes->code->standard->representation);
+                list ($sLang,) = explode('_', $sLocale);
+                if (!isset($aData[$sLang]))
+                    $aData[$sLang] = $sLocale;
+            }
+            $oCache->setData ($sCacheKey, $aData);
+        }
+        return $aData;
+    }
+}
+
+/** @} */

+ 28 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolSocialSharingQuery.php

@@ -0,0 +1,28 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * @see BxDolSocialSharing
+ */
+class BxDolSocialSharingQuery extends BxDolDb
+{
+
+    function __construct()
+    {
+        parent::__construct();
+    }
+
+    function getActiveButtons ()
+    {
+        return $this->fromCache('sys_objects_social_sharing', 'getAll', 'SELECT * FROM `sys_objects_social_sharing` WHERE `active` = 1 ORDER BY `order` ASC');
+    }
+
+}
+
+/** @} */

+ 1219 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolStorage.php

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

+ 147 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolStorageLocal.php

@@ -0,0 +1,147 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+define ('BX_DOL_STORAGE_DIR_RIGHTS', BX_DOL_DIR_RIGHTS);
+define ('BX_DOL_STORAGE_FILE_RIGHTS', BX_DOL_FILE_RIGHTS);
+
+/**
+ * File storage in local folder.
+ * @see BxDolStorage
+ */
+class BxDolStorageLocal extends BxDolStorage
+{
+    /**
+     * constructor
+     */
+    public function __construct($aObject)
+    {
+        parent::__construct($aObject);
+    }
+
+    /**
+     * Get file url.
+     * @param $iFileId file
+     * @return file url or false if file was not found
+     */
+    public function getFileUrlById($iFileId)
+    {
+        $aFile = $this->_oDb->getFileById($iFileId);
+        if (!$aFile)
+            return false;
+
+        $sUrl =  $this->getObjectBaseUrl($aFile['private']) . $aFile['remote_id'] . '.' . $aFile['ext'];
+        if ($aFile['private']) {
+            $sToken = $this->_oDb->genToken($iFileId);
+            $sUrl = bx_append_url_params($sUrl, array ('t' => $sToken));
+        }
+        return $sUrl;
+    }
+
+    /**
+     * Start file fownloading by remote id. If file is private then token is checked.
+     */
+    public function download ($sRemoteId, $sToken = false)
+    {
+        $this->setErrorCode(BX_DOL_STORAGE_ERR_OK);
+
+        $aFile = $this->_oDb->getFileByRemoteId($sRemoteId);
+        if (!$aFile) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_FILE_NOT_FOUND);
+            return false;
+        }
+
+        if ($aFile['private'] && !$this->_oDb->isTokenValid($aFile['id'], $sToken)) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_PERMISSION_DENIED);
+            return false;
+        }
+        $sFileLocation = $this->getObjectBaseDir($aFile['private']) . $aFile['path'];
+        if (!file_exists($sFileLocation)) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_FILE_NOT_FOUND);
+            return false;
+        }
+
+        if (!bx_smart_readfile($sFileLocation, $aFile['file_name'], $aFile['mime_type'], $aFile['private'] && $this->_iCacheControl > $this->_aObject['token_life'] ? $this->_aObject['token_life'] : $this->_iCacheControl, $aFile['private'] ? 'private' : 'public')) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_ENGINE_GET);
+            return false;
+        }
+
+        return true;
+    }
+
+    // ----------------
+
+    protected function addFileToEngine($sTmpFile, $sLocalId, $sName, $isPrivate, $iProfileId)
+    {
+        $sPath = $this->genPath($sLocalId, $this->_aObject['levels']);
+        $sNewFileDir = $this->getObjectBaseDir($isPrivate) . $sPath;
+        $sNewFilePath = $sNewFileDir . $sLocalId;
+
+        $this->mkdir($sNewFileDir);
+        if (!file_exists($sNewFileDir) || !is_writable($sNewFileDir)) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_FILESYSTEM_PERM);
+            return false;
+        }
+
+        if (!copy($sTmpFile, $sNewFilePath)) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_ENGINE_ADD);
+            return false;
+        }
+
+        if (!chmod ($sNewFilePath, BX_DOL_STORAGE_FILE_RIGHTS)) {
+            unlink($sNewFilePath);
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_ENGINE_ADD);
+            return false;
+        }
+
+        return true;
+    }
+
+    protected function deleteFileFromEngine($sFilePath, $isPrivate)
+    {
+        $sFileLocation = $this->getObjectBaseDir($isPrivate) . $sFilePath;
+
+        if (!file_exists($sFileLocation))
+            return true;
+
+        if (!unlink($sFileLocation)) {
+            $this->setErrorCode(BX_DOL_STORAGE_ERR_UNLINK);
+            return false;
+        }
+
+        return true;
+    }
+
+    protected function getObjectBaseDir ($isPrivate = false)
+    {
+        return BX_DIRECTORY_STORAGE . $this->_aObject['object'] . '/';
+    }
+
+    protected function getObjectBaseUrl ($isPrivate = false)
+    {
+        return BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('storage.php?o=' . $this->_aObject['object'] . '&f=');
+    }
+
+    protected function mkdir($sDirName, $sRights = false)
+    {
+        if (false == $sRights)
+            $sRights = BX_DOL_STORAGE_DIR_RIGHTS;
+
+        $aDirs = explode('/', $sDirName);
+        $sDir='';
+        foreach ($aDirs as $sPart) {
+            $sDir .= $sPart.'/';
+            if (!is_dir($sDir) && strlen($sDir) > 0 && !file_exists($sDir)) {
+                mkdir($sDir);
+                chmod($sDir, $sRights);
+            }
+        }
+    }
+}
+
+/** @} */

+ 297 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolStorageQuery.php

@@ -0,0 +1,297 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * Database queries for File storage class.
+ * @see BxDolStorage
+ */
+class BxDolStorageQuery extends BxDolDb
+{
+    protected $_aObject;
+    protected $_sTableFiles;
+
+    public function __construct($aObject)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+        $this->_sTableFiles = '`' . $aObject['table_files'] . '`';
+    }
+
+    static public function getStorageObject ($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_storage` WHERE `object` = ?", $sObject);
+        return $oDb->getRow($sQuery);
+    }
+
+    static public function getStorageObjects ()
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = "SELECT * FROM `sys_objects_storage`";
+        return $oDb->getAll($sQuery);
+    }
+
+    public function getMimeTypeByExt($sExt)
+    {
+        $sQuery = $this->prepare("SELECT `mime_type` FROM `sys_storage_mime_types` WHERE `ext` = ?", $sExt);
+        return $this->getOne($sQuery);
+    }
+
+    public function getIconByExt($sExt)
+    {
+        $sQuery = $this->prepare("SELECT `icon` FROM `sys_storage_mime_types` WHERE `ext` = ?", $sExt);
+        return $this->getOne($sQuery);
+    }
+
+    public function clearAllMimeTypes()
+    {
+        return $this->query("TRUNCATE TABLE `sys_storage_mime_types`");
+    }
+
+    public function addMimeType($sMimeType, $sExt, $sIcon = '')
+    {
+        $sQuery = $this->prepare("INSERT IGNORE INTO `sys_storage_mime_types` SET `ext` = ?, `mime_type` = ?, `icon` = ?", $sExt, $sMimeType, $sIcon);
+        return $this->query($sQuery);
+    }
+
+    public function getStorageObjectQuota()
+    {
+        return $this->_aObject;
+    }
+
+    public function updateStorageObjectQuota($iSize, $iNumber = 1)
+    {
+        $iTime = time();
+        $sQuery = $this->prepare("
+            UPDATE `sys_objects_storage`
+            SET `current_size` = `current_size` + ?, `current_number` = `current_number` + (?), `ts` = ?
+            WHERE `object` = ?",
+            $iSize, $iNumber, $iTime, $this->_aObject['object']
+        );
+        if ($this->query($sQuery)) {
+            $this->_aObject = $this->getStorageObject($this->_aObject['object']);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public function getUserQuota($iProfileId)
+    {
+        $sQuery = $this->prepare("SELECT `current_size`, `current_number`, 0 as `quota_size`, 0 as `quota_number`, 0 as `max_file_size` FROM `sys_storage_user_quotas` WHERE `profile_id` = ?", $iProfileId);
+        $a = $this->getRow($sQuery);
+        if (!is_array($a) || !$a)
+            $a = array ('current_size' => 0, 'current_number' => 0, 'quota_size' => 0, 'quota_number' => 0, 'max_file_size' => 0);
+
+        // get quota_number and quota_size from user's acl/membership
+        $aMembershipInfo = BxDolAcl::getInstance()->getMemberMembershipInfo($iProfileId);
+        if ($aMembershipInfo) {
+            if (isset($aMembershipInfo['quota_size']))
+                $a['quota_size'] = $aMembershipInfo['quota_size'];
+            if (isset($aMembershipInfo['quota_number']))
+                $a['quota_number'] = $aMembershipInfo['quota_number'];
+            if (isset($aMembershipInfo['quota_max_file_size']))
+                $a['max_file_size'] = $aMembershipInfo['quota_max_file_size'];
+        }
+
+        return $a;
+    }
+
+    public function updateUserQuota($iProfileId, $iSize, $iNumber = 1)
+    {
+        $iTime = time();
+        $sQuery = $this->prepare("
+            INSERT INTO `sys_storage_user_quotas`
+            SET `profile_id` = ?, `current_size` = `current_size` + ?, `current_number` = `current_number` + ?, `ts` = ?
+            ON DUPLICATE KEY UPDATE `current_size` = `current_size` + ?, `current_number` = `current_number` + ?, `ts` = ?",
+            $iProfileId, $iSize, $iNumber, $iTime, $iSize, $iNumber, $iTime
+        );
+        if ($this->query($sQuery))
+            return true;
+        else
+            return false;
+    }
+
+    public function addFile($iProfileId, $sLocalId, $sPath, $aFileName, $sMimeType, $sExt, $iSize, $iTime, $isPrivate)
+    {
+        $sQuery = $this->prepare("INSERT INTO " . $this->_sTableFiles . " SET
+            `profile_id` = ?, `remote_id` = ?, `path` = ?, `file_name` = ?, `mime_type` = ?, `ext` = ?, `size` = ?, `added` = ?, `modified` = ?, `private` = ?",
+            $iProfileId, $sLocalId, $sPath, $aFileName, $sMimeType, $sExt, $iSize, $iTime, $iTime, $isPrivate ? 1 : 0
+        );
+        return $this->query($sQuery);
+    }
+
+    public function modifyFilePrivate($iFileId, $isPrivate)
+    {
+        return $this->modifyCustomField($iFileId, 'private', $isPrivate ? 1 : 0);
+    }
+
+    public function modifyCustomField($iFileId, $sField, $sValue, $isUpdateModifiedField = true)
+    {
+        $sAdd = '';
+        if ($isUpdateModifiedField) {
+            $iTime = time();
+            $sAdd = $this->prepare(", `modified` = ?", $iTime);
+        }
+        $sQuery = $this->prepare("UPDATE " . $this->_sTableFiles . " SET `{$sField}` = ? {$sAdd} WHERE `id` = ?", $sValue, $iFileId);
+        return $this->query($sQuery);
+    }
+
+    public function deleteFile($iFileId)
+    {
+        // delete queued record for the file
+        $sQuery = $this->prepare("DELETE FROM `sys_storage_deletions` WHERE `object` = ? AND `file_id` = ?", $this->_aObject['object'], $iFileId);
+        $this->query($sQuery);
+
+        // delete file record
+        $sQuery = $this->prepare("DELETE FROM " . $this->_sTableFiles . " WHERE `id` = ?", $iFileId);
+        if (!$this->query($sQuery))
+            return false;
+
+        // delete any file traces in ghosts table
+        $sQuery = $this->prepare("DELETE FROM `sys_storage_ghosts` WHERE `object` = ? AND `id` = ?", $this->_aObject['object'], $iFileId);
+        $this->query($sQuery);
+
+        return true;
+    }
+
+    public function getFileByFileName($sValue)
+    {
+        return $this->_getFileBy('`file_name`', $sValue);
+    }
+
+    public function getFileById($sValue)
+    {
+        return $this->_getFileBy('`id`', $sValue);
+    }
+
+    public function getFileByRemoteId($sValue)
+    {
+        return $this->_getFileBy('`remote_id`', $sValue);
+    }
+
+    protected function _getFileBy($sField, $sValue)
+    {
+        $sQuery = $this->prepare("SELECT * FROM " . $this->_sTableFiles . " WHERE " . $sField . " = ?", $sValue);
+        return $this->getRow($sQuery);
+    }
+
+    public function isTokenValid($iFileId, $sToken)
+    {
+        $iTime = time();
+        $sQuery = $this->prepare("SELECT `created` FROM `sys_storage_tokens` WHERE `id` = ? AND `object` = ? AND `hash` = ? AND `created` > ?", $iFileId, $this->_aObject['object'], $sToken, $iTime - $this->_aObject['token_life']);
+        return $this->getOne($sQuery) ? true : false;
+    }
+
+    public function genToken($iFileId)
+    {
+        $iTime = time();
+        $sToken = md5($iTime . mt_rand() . BX_DOL_SECRET);
+        $sQuery = $this->prepare("INSERT INTO `sys_storage_tokens` SET `id` = ?, `object` = ?, `hash` = ?, `created` = ?", $iFileId, $this->_aObject['object'], $sToken, $iTime);
+        if ($this->query($sQuery))
+            return $sToken;
+        else
+            return false;
+    }
+
+    public function insertGhosts($mixedFileIds, $iProfileId, $iContentId = 0)
+    {
+        $iTime = time();
+        if (!is_array($mixedFileIds))
+            $mixedFileIds = array($mixedFileIds);
+
+        $iCount = 0;
+        foreach ($mixedFileIds as $iFileId) {
+            $sQuery = $this->prepare("INSERT INTO `sys_storage_ghosts`
+                SET `id` = ?, `object` = ?, `profile_id` = ?, `content_id` = ?, `created` = ?
+                ON DUPLICATE KEY UPDATE `profile_id` = ?, `content_id` = ?, `created` = ?", $iFileId, $this->_aObject['object'], $iProfileId, $iContentId, $iTime, $iProfileId, $iContentId, $iTime);
+            $iCount += $this->query($sQuery);
+        }
+        return $iCount;
+    }
+
+    public function updateGhostsContentId($mixedFileIds, $iProfileId, $iContentId)
+    {
+        $sQuery = $this->prepare("UPDATE `sys_storage_ghosts` SET `content_id` = ? WHERE `profile_id` = ? AND `object` = ?", $iContentId, $iProfileId, $this->_aObject['object']);
+        $sQuery .= " AND `id` IN (" . $this->implode_escape($mixedFileIds) . ")";
+        return $this->res($sQuery);
+    }
+
+    public function deleteGhosts($mixedFileIds, $iProfileId, $iContentId = false)
+    {
+        $sQuery = $this->prepare("DELETE FROM `sys_storage_ghosts` WHERE `profile_id` = ? AND `object` = ? AND `id` IN (" . $this->implode_escape($mixedFileIds) . ")", $iProfileId, $this->_aObject['object']);
+        if (false !== $iContentId)
+            $sQuery .= $this->prepare(" AND `content_id` = ?", $iContentId);
+        $iCount = $this->query($sQuery);
+        if ($iCount)
+            $this->query("OPTIMIZE TABLE `sys_storage_ghosts`");
+        return $iCount;
+    }
+
+    public function getGhosts($iProfileId, $iContentId = false)
+    {
+        return $this->getFiles($iProfileId, true, $iContentId);
+    }
+
+    public function getFiles($iProfileId, $isGhostsOnly = false, $iContentId = false)
+    {
+        $sJoin = '';
+        if ($isGhostsOnly) {
+            $sJoin .= $this->prepare(" INNER JOIN `sys_storage_ghosts` AS `g` ON (`f`.`id` = `g`.`id` AND `g`.`profile_id` = ? AND `g`.`object` = ? ", $iProfileId, $this->_aObject['object']);
+            if (false !== $iContentId)
+                $sJoin .= $this->prepare(" AND `g`.`content_id` = ?", $iContentId);
+            $sJoin .= ')';
+        }
+        $sQuery = "SELECT `f`.* FROM " . $this->_sTableFiles . " AS `f` " . $sJoin . (false === $iProfileId ? '' : $this->prepare("WHERE `f`.`profile_id` = ?", $iProfileId));
+        return $this->getAll($sQuery);
+    }
+
+    public function getFilesAll($iStart, $iPerPage)
+    {
+        $sQuery = $this->prepare("SELECT * FROM " . $this->_sTableFiles . " LIMIT ?, ?", (int)$iStart, (int)$iPerPage);
+        return $this->getAll($sQuery);
+    }
+
+    public function prune()
+    {
+        $iTime = time();
+        $sQuery = $this->prepare("DELETE FROM `sys_storage_tokens` WHERE `object` = ? AND `created` < ?", $this->_aObject['object'], $iTime - $this->_aObject['token_life']);
+        $iCount = $this->query($sQuery);
+        if ($iCount)
+            $this->query("OPTIMIZE TABLE `sys_storage_tokens`");
+        return $iCount;
+    }
+
+    public function queueFilesForDeletion ($a)
+    {
+        $iTime = time();
+        $iAdded = 0;
+        foreach ($a as $iFileId) {
+            $sQuery = $this->prepare("INSERT IGNORE INTO `sys_storage_deletions` SET `object` = ?, `file_id` = ?, `requested` = ?", $this->_aObject['object'], (int)$iFileId, $iTime);
+            $iAdded += ($this->query($sQuery) ? 1 : 0);
+        }
+        return $iAdded;
+    }
+
+    public static function getQueuedFilesForDeletion ($iLimit = 1000)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT `object`, `file_id` FROM `sys_storage_deletions` ORDER BY `requested` ASC LIMIT ?", $iLimit);
+        return $oDb->getAll($sQuery);
+    }
+
+    public static function isQueuedFilesForDeletion ($sPrefix)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT COUNT(*) FROM `sys_storage_deletions` WHERE `object` LIKE ?", $sPrefix . '%');
+        return $oDb->getOne($sQuery);
+    }
+}
+
+/** @} */

+ 196 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolStorageS3.php

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

+ 2242 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolTemplate.php

@@ -0,0 +1,2242 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+ini_set('pcre.backtrack_limit', 1000000);
+
+define('BX_DOL_TEMPLATE_DEFAULT_CODE', 'lagoon');
+define('BX_DOL_TEMPLATE_FOLDER_ROOT', 'template');
+
+define('BX_DOL_TEMPLATE_INJECTIONS_CACHE', 'sys_injections.inc');
+
+define('BX_DOL_TEMPLATE_CHECK_IN_BOTH', 'both');
+define('BX_DOL_TEMPLATE_CHECK_IN_BASE', 'base');
+define('BX_DOL_TEMPLATE_CHECK_IN_TMPL', 'tmpl');
+
+define('BX_PAGE_DEFAULT', 0); ///< default, regular page
+define('BX_PAGE_CLEAR', 2); ///< clear page, without any headers and footers
+define('BX_PAGE_POPUP', 44); ///< popup page, without any headers and footers
+define('BX_PAGE_TRANSITION', 150); ///< transition page with redirect to display some msg, like 'please wait', without headers footers
+
+/**
+ * Template engine.
+ *
+ * An object of the class allows to:
+ *  1. Manage HTML templates.
+ *  2. Get URL/path for any template image/icon.
+ *  3. Attach CSS/JavaScript files to the output.
+ *  4. Add some content to any template key using Injection engine.
+ *
+ *
+ * Avalable constructions.
+ *  1. <bx_include_auto:template_name.html /> - the content of the file would be inserted. File would be taken from current template if it existes there, and from base directory otherwise.
+ *  2. <bx_include_base:template_name.html /> - the content of the file would be inserted. File would be taken from base directory.
+ *  3. <bx_include_tmpl:template_name.html /> - the content of the file would be inserted. File would be taken from tmpl_xxx directory.
+ *  4. <bx_url_root /> - the value of BX_DOL_URL_ROOT variable will be inserted.
+ *  5. <bx_url_admin /> - the value of BX_DOL_URL_ADMIN variable will be inserted.
+ *  6. <bx_text:_language_key /> - _language_key will be translated using language file(function _t()) and inserted.
+       <bx_text_js:_language_key /> - _language_key will be translated using language file(function _t()) and inserted, use it to insert text into js string.
+       <bx_text_attribute:_language_key /> - _language_key will be translated using language file(function _t()) and inserted, use it to insert text into html attribute.
+ *  7. <bx_image_url:image_file_name /> - image with 'image_file_name' file name will be searched in the images folder of current template.
+ *     If it's not found, then it will be searched in the images folder of base template. On success full URL will be inserted, otherwise an empty string.
+ *  8. <bx_icon_url:icon_file_name /> - the same with <bx_image_url:image_file_name />, but icons will be searched in the images/icons/ folders.
+ *  9. <bx_injection:injection_name /> - will be replaced with injections registered with the page and injection_name in the `sys_injections`/`sys_injections_admin`/ tables.
+ *  10. <bx_if:tag_name>some_HTML</bx_if:tag_name> - will be replaced with provided content if the condition is true, and with empty string otherwise.
+ *  11. <bx_repeat:cycle_name>some_HTML</bx_repeat:cycle_name> - an inner HTML content will be repeated in accordance with received data.
+ *
+ *
+ * Related classes:
+ *  BxDolTemplateAdmin - for processing admin templates.
+ *  Template classes in modules - for processing modiles' templates.
+ *
+ *
+ * Global variables:
+ *  oSysTemplate - is used for template processing in user part.
+ *  oAdmTemplate - is used for template processing in admin part.
+ *
+ *
+ * Add injection:
+ *  1. Register it in the `sys_injections` table or `sys_injections_admin` table for admin panel.
+ *  2. Clear injections cache(sys_injections.inc and sys_injections_admin.inc in cache folder).
+ *
+ *
+ * Predefined template keys to add injections:
+ *  1. injection_head - add injections in the <head> tag.
+ *  2. injection_body - add ingection(attribute) in the <body> tag.
+ *  3. injection_header - add injection inside the <body> tag at the very beginning.
+ *  4. injection_logo_before - add injection at the left of the main logo(inside logo's DIV).
+ *  5. injection_logo_after - add injection at the right of the main logo(inside logo's DIV).
+ *  6. injection_between_logo_top_menu - add injection between logo and top menu.
+ *  7. injection_top_menu_before - add injection at the left of the top menu(inside top menu's DIV).
+ *  8. injection_top_menu_after - add injection at the right of the top menu(inside top menu's DIV).
+ *  13. injection_content_before - add injection just before main content(inside content's DIV).
+ *  14. injection_content_after - add injection just after main content(inside content's DIV).
+ *  15. injection_between_content_footer - add injection between content and footer.
+ *  16. injection_footer_before - add injection at the left of the footer(inside footer's DIV).
+ *  17. injection_footer_after - add injection at the right of the footer(inside footer's DIV).
+ *  18. injection_footer - add injection inside the <body> tag at the very end.
+ *
+ *
+ * Example of usage:
+ * @code
+ *  $oSysTemplate = BxDolTemplate::getInstance();
+ *
+ *  $oSysTemplate->addCss(array('test1.css', 'test2.css'));
+ *  $oSysTemplate->addJs(array('test1.js', 'test2.js'));
+ *  $oSysTemplate->parseHtmlByName('messageBox.html', array(
+ *    'id' => $iId,
+ *     'msgText' => $sText,
+ *     'bx_if:timer' => array(
+ *        'condition' => $iTimer > 0,
+ *        'content' => array(
+ *           'id' => $iId,
+ *           'time' => 1000 * $iTimer,
+ *           'on_close' => $sOnClose,
+ *        )
+ *     ),
+ *     'bx_if:timer' => array(
+ *        array(
+ *           'name' => $sName,
+ *           'title' => $sTitle
+ *        ),
+ *        array(
+ *           'name' => $sName,
+ *           'title' => $sTitle
+ *        )
+ *     )
+ *  ));
+ * @endcode
+ *
+ *
+ * Memberships/ACL:
+ * Doesn't depend on user's membership.
+ *
+ *
+ * Alerts:
+ * no alerts available
+ *
+ */
+class BxDolTemplate extends BxDol implements iBxDolSingleton
+{
+    /**
+     * Main fields
+     */
+    protected $_sPrefix;
+    protected $_sRootPath;
+    protected $_sRootUrl;
+    protected $_sSubPath;
+    protected $_sInjectionsTable;
+    protected $_sInjectionsCache;
+    protected $_sCode;
+    protected $_sCodeKey;
+    protected $_sKeyWrapperHtml;
+    protected $_sFolderHtml;
+    protected $_sFolderCss;
+    protected $_sFolderImages;
+    protected $_sFolderIcons;
+    protected $_aTemplates;
+
+    protected $_aLocations;
+    protected $_aLocationsJs;
+
+    /**
+     * Cache related fields
+     */
+    protected $_bCacheEnable;
+    protected $_sCacheFolderUrl;
+    protected $_sCachePublicFolderUrl;
+    protected $_sCachePublicFolderPath;
+    protected $_sCacheFilePrefix;
+
+    protected $_bImagesInline;
+    protected $_iImagesMaxSize;
+
+    protected $_bCssCache;
+    protected $_bCssArchive;
+    protected $_sCssCachePrefix;
+
+    protected $_bJsCache;
+    protected $_bJsArchive;
+    protected $_sJsCachePrefix;
+
+    /**
+     * Less related fields
+     */
+    protected $_bLessEnable;
+    protected $_sLessCachePrefix;
+
+    /**
+     * Minify related fields
+     */
+    protected $_bMinifyEnable;
+
+    protected $aPage;
+    protected $aPageContent;
+
+    protected $_oConfigTemplate;
+
+    /**
+     * Constructor
+     */
+    protected function __construct($sRootPath = BX_DIRECTORY_PATH_ROOT, $sRootUrl = BX_DOL_URL_ROOT)
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error ('Multiple instances are not allowed for the class: ' . get_class($this), E_USER_ERROR);
+
+        parent::__construct();
+
+        $this->_sPrefix = 'BxDolTemplate';
+
+        $this->_sRootPath = $sRootPath;
+        $this->_sRootUrl = $sRootUrl;
+        $this->_sInjectionsTable = 'sys_injections';
+        $this->_sInjectionsCache = BX_DOL_TEMPLATE_INJECTIONS_CACHE;
+
+        $this->_sCodeKey = 'skin';
+
+        $sCode = getParam('template');
+        if(empty($sCode))
+            $sCode = BX_DOL_TEMPLATE_DEFAULT_CODE;
+        $this->_checkCode($sCode, false);
+
+        //--- Check selected template in COOKIE(the lowest priority) ---//
+        $sCode = !empty($_COOKIE[$this->_sCodeKey]) ? $_COOKIE[$this->_sCodeKey] : '';
+        $this->_checkCode($sCode, false);
+
+        //--- Check selected template in GET(the highest priority) ---//
+        $sCode = !empty($_GET[$this->_sCodeKey]) ? $_GET[$this->_sCodeKey] : '';
+        $this->_checkCode($sCode, true);
+
+        if (!$this->_sSubPath)
+            $this->_sSubPath = 'boonex/' . BX_DOL_TEMPLATE_DEFAULT_CODE . '/';
+
+        if (!file_exists(BX_DIRECTORY_PATH_MODULES . $this->_sSubPath)) // just for 8.0.0-A6 upgrade
+            $this->_sSubPath = 'boonex/uni/';
+
+        if(isset($_GET[$this->_sCodeKey])) {
+            if(BxDolPermalinks::getInstance()->redirectIfNecessary(array($this->_sCodeKey)))
+                exit;
+        }
+
+        $this->_sKeyWrapperHtml = '__';
+        $this->_sFolderHtml = '';
+        $this->_sFolderCss = 'css/';
+        $this->_sFolderImages = 'images/';
+        $this->_sFolderIcons = 'images/icons/';
+        $this->_aTemplates = array();
+
+        $this->addLocation('system', $this->_sRootPath, $this->_sRootUrl);
+
+        $this->addLocationJs('system_inc_js', BX_DIRECTORY_PATH_INC . 'js/' , BX_DOL_URL_ROOT . 'inc/js/');
+        $this->addLocationJs('system_inc_js_classes', BX_DIRECTORY_PATH_INC . 'js/classes/' , BX_DOL_URL_ROOT . 'inc/js/classes/');
+        $this->addLocationJs('system_plugins_public', BX_DIRECTORY_PATH_PLUGINS_PUBLIC, BX_DOL_URL_PLUGINS_PUBLIC);
+
+        $this->_bCacheEnable = !defined('BX_DOL_CRON_EXECUTE') && getParam('sys_template_cache_enable') == 'on';
+        $this->_sCacheFolderUrl = '';
+        $this->_sCachePublicFolderUrl = BX_DOL_URL_CACHE_PUBLIC;
+        $this->_sCachePublicFolderPath = BX_DIRECTORY_PATH_CACHE_PUBLIC;
+        $this->_sCacheFilePrefix = "bx_templ_";
+
+        $this->_bImagesInline = getParam('sys_template_cache_image_enable') == 'on';
+        $this->_iImagesMaxSize = (int)getParam('sys_template_cache_image_max_size') * 1024;
+
+        $bArchive = getParam('sys_template_cache_compress_enable') == 'on';
+        $this->_bCssCache = !defined('BX_DOL_CRON_EXECUTE') && getParam('sys_template_cache_css_enable') == 'on';
+        $this->_bCssArchive = $this->_bCssCache && $bArchive;
+        $this->_sCssCachePrefix = $this->_sCacheFilePrefix . 'css_';
+
+        $this->_bJsCache = !defined('BX_DOL_CRON_EXECUTE') && getParam('sys_template_cache_js_enable') == 'on';
+        $this->_bJsArchive = $this->_bJsCache && $bArchive;
+        $this->_sJsCachePrefix = $this->_sCacheFilePrefix . 'js_';
+
+        $this->_bLessEnable = true;
+        $this->_sLessCachePrefix = $this->_sCacheFilePrefix . 'less_';
+
+        $this->_bMinifyEnable = true;
+
+        $this->aPage = array();
+        $this->aPageContent = array();
+    }
+
+    /**
+     * Prevent cloning the instance
+     */
+    public function __clone()
+    {
+        if (isset($GLOBALS['bxDolClasses'][get_class($this)]))
+            trigger_error('Clone is not allowed for the class: ' . get_class($this), E_USER_ERROR);
+    }
+
+    /**
+     * Get singleton instance of the class
+     */
+    public static function getInstance()
+    {
+        if(!isset($GLOBALS['bxDolClasses'][__CLASS__])) {
+            $GLOBALS['bxDolClasses'][__CLASS__] = new BxDolTemplate();
+            $GLOBALS['bxDolClasses'][__CLASS__]->init();
+        }
+
+        return $GLOBALS['bxDolClasses'][__CLASS__];
+    }
+
+    /**
+     * Load templates.
+     */
+    function loadTemplates()
+    {
+        $aResult = array();
+        foreach($this->_aTemplates as $sName)
+            $aResult[$sName] = $this->getHtml($sName . '.html');
+        $this->_aTemplates = $aResult;
+    }
+    /**
+     * Initialize template engine.
+     * Note. The method is executed with the system, you shouldn't execute it in your subclasses.
+     */
+    function init()
+    {
+        //--- Load page elements related static variables ---//
+        $this->aPage = array(
+            'name_index' => BX_PAGE_DEFAULT,
+            'header' => '',
+            'header_text' => '',
+            'keywords' => array(),
+            'location' => array(),
+            'description'  => '',
+            'robots' => '',
+            'css_name' => array(),
+            'css_compiled' => array(),
+            'css_system' => array(),
+            'js_name' => array(),
+            'js_compiled' => array(),
+            'js_system' => array(),
+            'js_options' => array(),
+            'js_translations' => array(),
+            'js_images' => array(),
+            'injections' => array()
+        );
+
+        //--- Load default CSS ---//
+        $this->addCssSystem(array(
+            'common.css',
+            'default.less',
+            'general.css',
+            'icons.css',
+            'colors.css',
+            'forms.css',
+            'media-desktop.css',
+            'media-tablet.css',
+            'media-phone.css',
+            'media-print.css',
+            BX_DOL_URL_PLUGINS_PUBLIC . 'marka/marka.min.css',
+        ));
+
+        //--- Load default JS ---//
+        $this->addJsSystem(array(
+            'jquery/jquery.min.js',
+            'jquery/jquery-migrate.min.js',
+            'jquery-ui/jquery.ui.position.min.js',
+            'spin.min.js',
+            'jquery.easing.js',
+            'jquery.cookie.min.js',
+            'moment-with-locales.min.js',
+            'functions.js',
+            'jquery.webForms.js',
+            'jquery.dolPopup.js',
+            'marka/marka.min.js',
+        ));
+
+        //--- Load default JS ---//
+        bx_import('BxDolLanguages');
+        $this->addJsTranslation(array(
+            '_are you sure?',
+            '_error occured',
+            '_sys_loading',
+        ));
+
+        //--- Load injection's cache ---//
+        if (getParam('sys_db_cache_enable')) {
+            $oDb = BxDolDb::getInstance();
+            $oCache = $oDb->getDbCacheObject();
+            $sCacheKey = $oDb->genDbCacheKey($this->_sInjectionsCache);
+
+            $aInjections = $oCache->getData($sCacheKey);
+            if ($aInjections === null) {
+                $aInjections = $this->getInjectionsData();
+                $oCache->setData ($sCacheKey, $aInjections);
+            }
+        } else {
+            $aInjections = $this->getInjectionsData();
+        }
+
+        $this->aPage['injections'] = $aInjections;
+
+        bx_import('BxTemplConfig'); // TODO: for some reason autoloader isn't working here....
+        $this->_oConfigTemplate = BxTemplConfig::getInstance();
+    }
+
+    protected function getInjectionsData ()
+    {
+        $oDb = BxDolDb::getInstance();
+
+        $aInjections = $oDb->getAll("SELECT `page_index`, `name`, `key`, `type`, `data`, `replace` FROM `" . $this->_sInjectionsTable . "` WHERE `active`='1'");
+        if (!$aInjections)
+            return array();
+
+        foreach ($aInjections as $aInjection)
+            $aInjections['page_' . $aInjection['page_index']][$aInjection['key']][] = $aInjection;
+
+        return $aInjections;
+    }
+
+    /**
+     * Set page name index
+     * @param int $i name index
+     */
+    function setPageNameIndex($i)
+    {
+        $this->aPage['name_index'] = $i;
+    }
+
+    /**
+     * Get page name index
+     * @return int $i name index
+     */
+    function getPageNameIndex()
+    {
+        return isset($this->aPage['name_index']) ? (int)$this->aPage['name_index'] : 0;
+    }
+
+    /**
+     * Set page header
+     * @param string $s page header
+     */
+    function setPageHeader($s)
+    {
+        $this->aPage['header'] = $s;
+    }
+
+    /**
+     * Get page header
+     * @return string $s page header
+     */
+    function getPageHeader()
+    {
+        return $this->aPage['header'];
+    }
+
+    /**
+     * Set page params. Available page params are: name_index, header
+     * @param array $a page params
+     */
+    function setPageParams($a)
+    {
+        if (!empty($this->aPage))
+            $this->aPage = array_merge($this->aPage, $a);
+        else
+            $this->aPage = $a;
+    }
+
+    /**
+     * Get page params.
+     * @return array $a page params
+     */
+    function getPageParams()
+    {
+        return $this->aPage;
+    }
+
+    /**
+     * Set page description.
+     *
+     * @param string $sDescription necessary page description.
+     */
+    function setPageDescription($sDescription)
+    {
+        $this->aPage['description'] = $sDescription;
+    }
+
+    /**
+     * Set page meta robots.
+     *
+     * @param string $s page meta robots.
+     */
+    function setPageMetaRobots($s)
+    {
+        $this->aPage['robots'] = $s;
+    }
+
+    /**
+     * Set page content for some variable.
+     * @param string $sVar     name of content variable
+     * @param string $sContent content for $sVar variable
+     * @param int    $iIndex   optional page index, default is index which was set before with @see setPageNameIndex function, or 0
+     */
+    function setPageContent($sVar, $sContent, $iIndex = false)
+    {
+        $i = false !== $iIndex ? $iIndex : $this->getPageNameIndex();
+        $this->aPageContent[$i][$sVar] = $sContent;
+    }
+
+    /**
+     * Get page content for some variable.
+     * @param  string $sVar   name of content variable
+     * @param  int    $iIndex optional page index, default is index which was set before with @see setPageNameIndex function, or 0
+     * @return string page content for some variable or for the whole page.
+     */
+    function getPageContent($sVar = false, $iIndex = false)
+    {
+        $i = false !== $iIndex ? $iIndex : $this->getPageNameIndex();
+        return false !== $sVar ? $this->aPageContent[$i][$sVar] : $this->aPageContent[$i];
+    }
+
+    /**
+     * Get currently active template code.
+     *
+     * @return string template's code.
+     */
+    function getCode()
+    {
+        return $this->_sCode;
+    }
+
+    /**
+     * Check whether code is associated with active template.
+     *
+     * @param string  $sCode      template's unique URI.
+     * @param boolean $bSetCookie save code in COOKIE or not.
+     */
+    protected function _checkCode($sCode, $bSetCookie)
+    {
+        if(empty($sCode) || !preg_match('/^[A-Za-z0-9_-]+$/', $sCode))
+            return;
+
+        $aModule = BxDolModuleQuery::getInstance()->getModuleByUri($sCode);
+        if(empty($aModule) || !is_array($aModule) || (int)$aModule['enabled'] != 1 || !file_exists($this->_sRootPath . 'modules/' . $aModule['path'] . 'data/template/'))
+            return;
+
+        $oConfig = new BxDolModuleConfig($aModule);
+
+        $this->_sCode = $oConfig->getUri();
+        $this->_sSubPath = $oConfig->getDirectory();
+
+        if(!$bSetCookie || bx_get('preview'))
+            return;
+
+        $aUrl = parse_url(BX_DOL_URL_ROOT);
+        $sPath = isset($aUrl['path']) && !empty($aUrl['path']) ? $aUrl['path'] : '/';
+
+        setcookie( $this->_sCodeKey, $this->_sCode, time() + 60*60*24*365, $sPath);
+    }
+
+    /**
+     * Get currently active template path.
+     *
+     * @return string template's path.
+     */
+    function getPath()
+    {
+        return $this->_sSubPath;
+    }
+
+    /**
+     * Set page title.
+     * @deprecated use setPageHeader
+     *
+     * @param string $sTitle necessary page title.
+     */
+    function setPageTitle($sTitle)
+    {
+        $this->setPageHeader($sTitle);
+    }
+
+    /**
+     * Set page's main box title.
+     * @deprecated use setPageParams
+     *
+     * @param string $sTitle necessary page's main box title.
+     */
+    function setPageMainBoxTitle($sTitle)
+    {
+        $this->setPageParams(array('header_text' => $sTitle));
+    }
+
+    /**
+     * Add location in array of locations.
+     * Note. Location is the path/url to folder where 'templates' folder is stored.
+     *
+     * @param string $sKey          - location's    unique key.
+     * @param string $sLocationPath - location's path. For modules: '[path_to_script]/modules/[vendor_name]/[module_name]/'
+     * @param string $sLocationUrl  - location's url. For modules: '[url_to_script]/modules/[vendor_name]/[module_name]/'
+     */
+    function addLocation($sKey, $sLocationPath, $sLocationUrl)
+    {
+        $this->_aLocations[$sKey] = array(
+            'path' => $sLocationPath,
+            'url' => $sLocationUrl,
+        );
+
+        return $sKey;
+    }
+    /**
+     * Add dynamic location.
+     *
+     * @param  string   $sLocationPath - location's path. For modules: '[path_to_script]/modules/[vendor_name]/[module_name]/'
+     * @param  string   $sLocationUrl  - location's url. For modules: '[url_to_script]/modules/[vendor_name]/[module_name]/'
+     * @return location key. Is needed to remove the location.
+     */
+    function addDynamicLocation($sLocationPath, $sLocationUrl)
+    {
+        $sLocationKey = time();
+        $this->addLocation($sLocationKey, $sLocationPath, $sLocationUrl);
+
+        return $sLocationKey;
+    }
+    /**
+     * Remove location from array of locations.
+     * Note. Location is the path/url to folder where templates are stored.
+     *
+     * @param string $sKey - location's    unique key.
+     */
+    function removeLocation($sKey)
+    {
+        if(isset($this->_aLocations[$sKey]))
+           unset($this->_aLocations[$sKey]);
+    }
+    /**
+     * Add JS location in array of JS locations.
+     * Note. Location is the path/url to folder where JS files are stored.
+     *
+     * @param string $sKey          - location's    unique key.
+     * @param string $sLocationPath - location's path. For modules: '[path_to_script]/modules/[vendor_name]/[module_name]/js/'
+     * @param string $sLocationUrl  - location's url. For modules: '[url_to_script]/modules/[vendor_name]/[module_name]/js/'
+     */
+    function addLocationJs($sKey, $sLocationPath, $sLocationUrl)
+    {
+        $this->_aLocationsJs[$sKey] = array(
+            'path' => $sLocationPath,
+            'url' => $sLocationUrl
+        );
+    }
+    /**
+     * Add dynamic JS location.
+     *
+     * @param  string   $sLocationPath - location's path. For modules: '[path_to_script]/modules/[vendor_name]/[module_name]/'
+     * @param  string   $sLocationUrl  - location's url. For modules: '[url_to_script]/modules/[vendor_name]/[module_name]/'
+     * @return location key. Is needed to remove the location.
+     */
+    function addDynamicLocationJs($sLocationPath, $sLocationUrl)
+    {
+        $sLocationKey = time();
+        $this->addLocationJs($sLocationKey, $sLocationPath, $sLocationUrl);
+
+        return $sLocationKey;
+    }
+    /**
+     * Remove JS location from array of locations.
+     * Note. Location is the path/url to folder where templates are stored.
+     *
+     * @param string $sKey - JS location's    unique key.
+     */
+    function removeLocationJs($sKey)
+    {
+        if(isset($this->_aLocationsJs[$sKey]))
+           unset($this->_aLocationsJs[$sKey]);
+    }
+    /**
+     * Add Option in JS output.
+     *
+     * @param mixed $mixedName option's name or an array of options' names.
+     */
+    function addJsOption($mixedName)
+    {
+        if(is_string($mixedName))
+            $mixedName = array($mixedName);
+
+        foreach($mixedName as $sName)
+            $this->aPage['js_options'][$sName] = getParam($sName);
+    }
+    /**
+     * Add language translation for key in JS output.
+     *
+     * @param mixed $mixedKey language key or an array of keys.
+     */
+    function addJsTranslation($mixedKey)
+    {
+        if(is_string($mixedKey))
+            $mixedKey = array($mixedKey);
+
+        foreach($mixedKey as $sKey)
+            $this->aPage['js_translations'][$sKey] = _t($sKey, '{0}', '{1}');
+    }
+    /**
+     * Add image in JS output.
+     *
+     * @param array $aImages an array of image descriptors.
+     *                       The descriptor is a key/value pear in the array of descriptors.
+     */
+    function addJsImage($aImages)
+    {
+        if(!is_array($aImages))
+            return;
+
+        foreach($aImages as $sKey => $sFile) {
+            $sUrl = $this->getImageUrl($sFile);
+            if(empty($sUrl))
+                continue;
+
+            $this->aPage['js_images'][$sKey] = $sUrl;
+        }
+    }
+    /**
+     * Add icon in JS output.
+     *
+     * @param array $aIcons an array of icons descriptors.
+     *                      The descriptor is a key/value pear in the array of descriptors.
+     */
+    function addJsIcon($aIcons)
+    {
+        if(!is_array($aIcons))
+            return;
+
+        foreach($aIcons as $sKey => $sFile) {
+            $sUrl = $this->getIconUrl($sFile);
+            if(empty($sUrl))
+                continue;
+
+            $this->aPage['js_images'][$sKey] = $sUrl;
+        }
+    }
+    /**
+     * Set page keywords.
+     *
+     * @param mixed  $mixedKeywords necessary page keywords(string - single keyword, array - an array of keywords).
+     * @param string $sDevider      - string devider.
+     */
+    function addPageKeywords($mixedKeywords, $sDevider = ',')
+    {
+        if(is_string($mixedKeywords))
+            $mixedKeywords = strpos($mixedKeywords, $sDevider) !== false ? explode($sDevider, $mixedKeywords) : array($mixedKeywords);
+
+        foreach($mixedKeywords as $iKey => $sValue)
+            $mixedKeywords[$iKey] = trim($sValue);
+
+        $this->aPage['keywords'] = isset($this->aPage['keywords']) && is_array($this->aPage['keywords']) ? array_merge($this->aPage['keywords'], $mixedKeywords) : $mixedKeywords;
+    }
+    /**
+     * Set page locatoin coordinates.
+     *
+     * @param $fLat latitude
+     * @param $fLng longitude
+     */
+    function addPageMetaLocation($fLat, $fLng, $sCountryCode)
+    {
+        $this->aPage['location'] = array('lat' => $fLat, 'lng' => $fLng, 'country' => $sCountryCode);
+    }
+    /**
+     * Set page meta image.
+     *
+     * @param $sImageUrl meta image url
+     */
+    function addPageMetaImage($sImageUrl)
+    {
+        $this->aPage['image'] = $sImageUrl;
+    }
+    /**
+     * Set page rss link.
+     *
+     * @param $sTitle - rss feed title
+     * @param $sUrl - rss feed URL
+     */
+    function addPageRssLink($sTitle, $sUrl)
+    {
+        if (!isset($this->aPage['rss']))
+            $this->aPage['rss'] = array('title' => $sTitle, 'url' => $sUrl);
+        else
+            $this->aPage['rss'] = false;
+    }
+    /**
+     * Returns page meta info, like meta keyword, meta description, location, etc
+     */
+    function getMetaInfo()
+    {
+        $sRet = '';
+
+        if (!empty($this->aPage['keywords']) && is_array($this->aPage['keywords']))
+            $sRet .= '<meta name="keywords" content="' . bx_html_attribute(implode(',', $this->aPage['keywords'])) . '" />';
+
+        if (!empty($this->aPage['description']) && is_string($this->aPage['description']))
+            $sRet .= '<meta name="description" content="' . bx_html_attribute($this->aPage['description']) . '" />';
+
+        if (!empty($this->aPage['location']) && isset($this->aPage['location']['lat']) && isset($this->aPage['location']['lng']) && isset($this->aPage['location']['country']))
+            $sRet .= '
+                <meta name="ICBM" content="' . $this->aPage['location']['lat'] . ';' . $this->aPage['location']['lng'] . '" />
+                <meta name="geo.position" content="' . $this->aPage['location']['lat'] . ';' . $this->aPage['location']['lng'] . '" />
+                <meta name="geo.region" content="' . bx_html_attribute($this->aPage['location']['country']) . '" />';
+
+        if (!empty($this->aPage['image']))
+            $sRet .= '<meta property="og:image" content="' . $this->aPage['image'] . '" />';
+        else
+            $sRet .= BxTemplFunctions::getInstance()->getMetaIcons();
+
+        if (!empty($this->aPage['rss']) && !empty($this->aPage['rss']['url']))
+            $sRet .= '<link rel="alternate" type="application/rss+xml" title="' . bx_html_attribute($this->aPage['rss']['title'], BX_ESCAPE_STR_QUOTE) . '" href="' . $this->aPage['rss']['url'] . '" />';
+
+        return $sRet;
+    }
+    /**
+     * Get template, which was loaded earlier.
+     * @see method this->loadTemplates and field this->_aTemplates
+     *
+     * @param  string $sName - template name.
+     * @return string template's content.
+     */
+    function getTemplate($sName)
+    {
+        return $this->_aTemplates[$sName];
+    }
+    /**
+     * Get full URL for the icon.
+     *
+     * @param  string $sName    icon's file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string full URL.
+     */
+    function getIconUrl($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        $sContent = "";
+        if(($sContent = $this->_getInlineData('icon', $sName, $sCheckIn)) !== false)
+            return $sContent;
+
+        return $this->_getAbsoluteLocation('url', $this->_sFolderIcons, $sName, $sCheckIn);
+    }
+    /**
+     * Get absolute Path for the icon.
+     *
+     * @param  string $sName    - icon's file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string absolute path.
+     */
+    function getIconPath($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        return $this->_getAbsoluteLocation('path', $this->_sFolderIcons, $sName, $sCheckIn);
+    }
+    /**
+     * Get full URL for the image.
+     *
+     * @param  string $sName    - images's file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string full URL.
+     */
+    function getImageUrl($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        $sContent = "";
+        if(($sContent = $this->_getInlineData('image', $sName, $sCheckIn)) !== false)
+            return $sContent;
+
+        return $this->_getAbsoluteLocation('url', $this->_sFolderImages, $sName, $sCheckIn);
+    }
+    /**
+     * Get absolute Path for the image.
+     *
+     * @param  string $sName    - image's file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string absolute path.
+     */
+    function getImagePath($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        return $this->_getAbsoluteLocation('path', $this->_sFolderImages, $sName, $sCheckIn);
+    }
+    /**
+     * Get full URL of CSS file.
+     *
+     * @param  string $sName    - CSS file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string full URL.
+     */
+    function getCssUrl($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        return $this->_getAbsoluteLocation('url', $this->_sFolderCss, $sName, $sCheckIn);
+    }
+    /**
+     * Get full Path of CSS file.
+     *
+     * @param  string $sName    - CSS file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string full URL.
+     */
+    function getCssPath($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        return $this->_getAbsoluteLocation('path', $this->_sFolderCss, $sName, $sCheckIn);
+    }
+
+    /**
+     * Get menu.
+     * @param $s menu object name
+     * @return html or empty string
+     */
+    function getMenu ($s)
+    {
+        $oMenu = BxDolMenu::getObjectInstance($s);
+        return $oMenu ? $oMenu->getCode () : '';
+    }
+
+    /**
+     * Get content of HTML file.
+     *
+     * @param  string $sName    - HTML file name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string full content of the file and false on failure.
+     */
+    function getHtml($sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        $sAbsolutePath = $this->_getAbsoluteLocation('path', $this->_sFolderHtml, $sName, $sCheckIn);
+        return !empty($sAbsolutePath) ? trim(file_get_contents($sAbsolutePath)) : false;
+    }
+
+    /**
+     * Parse HTML template. Search for the template with accordance to it's file name.
+     *
+     * @see allows to use cache.
+     *
+     * @param  string $sName               - HTML file name.
+     * @param  array  $aVariables          - key/value pairs. key should be the same as template's key, but without prefix and postfix.
+     * @param  mixed  $mixedKeyWrapperHtml - key wrapper(string value if left and right parts are the same, array(left, right) otherwise).
+     * @param  string $sCheckIn            where the content would be searched(base, template, both)
+     * @return string the result of operation.
+     */
+    function parseHtmlByName($sName, $aVariables, $mixedKeyWrapperHtml = null, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->beginTemplate($sName, $sRand = time().rand());
+
+        if (($sContent = $this->getCached($sName, $aVariables, $mixedKeyWrapperHtml, $sCheckIn)) !== false) {
+            if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->endTemplate($sName, $sRand, $sRet, true);
+            return $sContent;
+        }
+
+        $sRet = '';
+        if (($sContent = $this->getHtml($sName, $sCheckIn)) !== false)
+            $sRet = $this->_parseContent($sContent, $aVariables, $mixedKeyWrapperHtml);
+
+        if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->endTemplate($sName, $sRand, $sRet, false);
+
+        return $sRet;
+    }
+    /**
+     * Parse HTML template.
+     *
+     * @see Doesn't allow to use cache.
+     *
+     * @param  string $sContent            - HTML file content.
+     * @param  array  $aVariables          - key/value pairs. key should be the same as template's key, but without prefix and postfix.
+     * @param  mixed  $mixedKeyWrapperHtml - key wrapper(string value if left and right parts are the same, array(left, right) otherwise).
+     * @return string the result of operation.
+     */
+    function parseHtmlByContent($sContent, $aVariables, $mixedKeyWrapperHtml = null)
+    {
+        if(empty($sContent))
+            return "";
+
+        return $this->_parseContent($sContent, $aVariables, $mixedKeyWrapperHtml);
+    }
+    /**
+     * Parse earlier loaded HTML template.
+     *
+     * @see Doesn't allow to use cache.
+     *
+     * @param  string $sName      - template name.
+     * @param  array  $aVariables - key/value pairs. Key should be the same as template's key, excluding prefix and postfix.
+     * @return string the result of operation.
+     * @see $this->_aTemplates
+     */
+    function parseHtmlByTemplateName($sName, $aVariables, $mixedKeyWrapperHtml = null)
+    {
+        if(!isset($this->_aTemplates[$sName]) || empty($this->_aTemplates[$sName]))
+            return "";
+
+        return $this->_parseContent($this->_aTemplates[$sName], $aVariables, $mixedKeyWrapperHtml);
+    }
+    /**
+     * Parse page HTML template. Search for the page's template with accordance to it's file name.
+     *
+     * @see allows to use cache.
+     *
+     * @param  string $sName      - HTML file name.
+     * @param  array  $aVariables - key/value pairs. key should be the same as template's key, but without prefix and postfix.
+     * @return string the result of operation.
+     */
+    function parsePageByName($sName, $aVariables)
+    {
+        if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->beginPage($sName);
+
+        $sContent = $this->parseHtmlByName($sName, $aVariables, $this->_sKeyWrapperHtml, BX_DOL_TEMPLATE_CHECK_IN_BOTH);
+        if(empty($sContent))
+            $sContent = $this->parseHtmlByName('default.html', $aVariables, $this->_sKeyWrapperHtml, BX_DOL_TEMPLATE_CHECK_IN_BOTH);
+
+        //--- Add CSS and JS at the very last ---//
+        if(strpos($sContent , '<bx_include_css />') !== false) {
+            if (!empty($this->aPage['css_name']))
+                $this->addCss($this->aPage['css_name']);
+            $sContent = str_replace('<bx_include_css />', $this->includeFiles('css', true) . $this->includeFiles('css'), $sContent);
+        }
+
+        if(strpos($sContent , '<bx_include_js />') !== false) {
+            if (!empty($this->aPage['js_name']))
+                $this->addJs($this->aPage['js_name']);
+            $sContent = str_replace('<bx_include_js />', $this->includeFiles('js', true) . $this->includeFiles('js'), $sContent);
+        }
+
+        if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->endPage($sContent);
+
+        return $sContent;
+    }
+    /**
+     * Parse system keys.
+     *
+     * @param  string $sKey key
+     * @return string value associated with the key.
+     */
+    function parseSystemKey($sKey, $mixedKeyWrapperHtml = null, $bProcessInjection = true)
+    {
+        $aKeyWrappers = $this->_getKeyWrappers($mixedKeyWrapperHtml);
+
+        $sRet = '';
+        switch( $sKey ) {
+            case 'page_charset':
+                $sRet = 'UTF-8';
+                break;
+            case 'page_robots':
+                if(!empty($this->aPage['robots']) && is_string($this->aPage['robots']))
+                    $sRet = '<meta name="robots" content="' . bx_html_attribute($this->aPage['robots']) . '" />';
+                break;
+            case 'meta_info':
+                $sRet = $this->getMetaInfo();
+                break;
+            case 'page_header':
+                if(isset($this->aPage['header']))
+                    $sRet = bx_process_output(strip_tags($this->aPage['header']));
+                break;
+            case 'page_header_text':
+                if(isset($this->aPage['header_text']))
+                    $sRet = bx_process_output($this->aPage['header_text']);
+                break;
+            case 'popup_loading':
+                $s = $this->parsePageByName('popup_loading.html', array());
+                $sRet = BxTemplFunctions::getInstance()->transBox('bx-popup-loading', $s, true);
+
+                $oSearch = new BxTemplSearch();
+                $oSearch->setLiveSearch(true);
+                $sRet .= $this->parsePageByName('search.html', array(
+                    'search_form' => $oSearch->getForm(BX_DB_CONTENT_ONLY),
+                    'results' => $oSearch->getResultsContainer(),
+                ));
+
+                $sRet .= $this->getMenu ('sys_site');                
+                $sRet .= isLogged() ? $this->getMenu ('sys_add_content') : '';
+                $sRet .= isLogged() ? $this->getMenu ('sys_account_popup') : '';
+                break;
+            case 'lang':
+                $sRet = bx_lang_name();
+                break;
+            case 'main_logo':
+                $sRet = BxTemplFunctions::getInstance()->getMainLogo();
+                break;
+            case 'informer':
+                $oInformer = BxDolInformer::getInstance($this);
+                $sRet = $oInformer ? $oInformer->display() : '';
+                break;
+            case 'dol_images':
+                $sRet = $this->_processJsImages();
+                break;
+            case 'dol_lang':
+                $sRet = $this->_processJsTranslations();
+                break;
+            case 'dol_options':
+                $sRet = $this->_processJsOptions();
+                break;
+            case 'bottom_text':
+                $sRet = _t( '_bottom_text', date('Y') );
+                break;
+            case 'copyright':
+                $sRet = _t( '_copyright',   date('Y') ) . getVersionComment();
+                break;
+            case 'extra_js':
+                $sRet = empty($this->aPage['extra_js']) ? '' : $this->aPage['extra_js'];
+                break;
+            case 'is_profile_page':
+                $sRet = (defined('BX_PROFILE_PAGE')) ? 'true' : 'false';
+                break;
+
+            default:
+                $sRet = ($sTemplAdd = BxTemplFunctions::getInstance()->TemplPageAddComponent($sKey)) !== false ? $sTemplAdd : $aKeyWrappers['left'] . $sKey . $aKeyWrappers['right'];
+        }
+
+        if($bProcessInjection)
+            $sRet = $this->processInjection($this->getPageNameIndex(), $sKey, $sRet);
+
+        return $sRet;
+    }
+    function getCacheFilePrefix($sType)
+    {
+    	$sResult = '';
+    	switch($sType) {
+    		case 'template':
+				$sResult = $this->_sCacheFilePrefix;
+				break;
+
+    		case 'css':
+    			$sResult = $this->_sCssCachePrefix;
+    			break;
+
+    		case 'js':
+    			$sResult = $this->_sJsCachePrefix;
+    			break;
+    	}
+
+    	return $sResult;
+    }
+    /**
+     * Get cache object for templates
+     * @return cache class instance
+     */
+    function getTemplatesCacheObject ()
+    {
+        $sCacheEngine = getParam('sys_template_cache_engine');
+        $oCacheEngine = bx_instance('BxDolCache' . $sCacheEngine);
+        if(!$oCacheEngine->isAvailable())
+            $oCacheEngine = bx_instance('BxDolCacheFileHtml');
+        return $oCacheEngine;
+    }
+    /**
+     * Get template from cache if it's enabled.
+     *
+     * @param  string  $sName               template name
+     * @param  string  $aVariables          key/value pairs. key should be the same as template's key, but without prefix and postfix.
+     * @param  mixed   $mixedKeyWrapperHtml - key wrapper(string value if left and right parts are the same, array(0 => left, 1 => right) otherwise).
+     * @param  string  $sCheckIn            where the content would be searched(base, template, both)
+     * @param  boolean $bEvaluate           need to evaluate the template or not.
+     * @return string  result of operation or false on failure.
+     */
+    function getCached($sName, &$aVariables, $mixedKeyWrapperHtml = null, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH, $bEvaluate = true)
+    {
+        // initialization
+
+        if(!$this->_bCacheEnable)
+            return false;
+
+        $sAbsolutePath = $this->_getAbsoluteLocation('path', $this->_sFolderHtml, $sName, $sCheckIn);
+        if(empty($sAbsolutePath))
+            return false;
+
+        $oCacheEngine = $this->getTemplatesCacheObject ();
+        $isFileBasedEngine = $bEvaluate && method_exists($oCacheEngine, 'getDataFilePath');
+
+        // try to get cached content
+
+        $sCacheVariableName = "a";
+        $sCacheKey = $this->_getCacheFileName('html', $sAbsolutePath) . '.php';
+        if ($isFileBasedEngine)
+            $sCacheContent = $oCacheEngine->getDataFilePath($sCacheKey);
+        else
+            $sCacheContent = $oCacheEngine->getData($sCacheKey);
+
+
+        // recreate cache if it is empty
+
+        if ($sCacheContent === null && ($sContent = file_get_contents($sAbsolutePath)) !== false && ($sContent = $this->_compileContent($sContent, "\$" . $sCacheVariableName, 1, $aVariables, $mixedKeyWrapperHtml)) !== false) {
+            if (false === $oCacheEngine->setData($sCacheKey, trim($sContent)))
+                return false;
+
+            if ($isFileBasedEngine)
+                $sCacheContent = $oCacheEngine->getDataFilePath($sCacheKey);
+            else
+                $sCacheContent = $sContent;
+        }
+
+        if ($sCacheContent === null)
+            return false;
+
+        // return simple cache content
+
+        if(!$bEvaluate)
+            return $sCacheContent;
+
+        // return evaluated cache content
+
+        ob_start();
+
+        $$sCacheVariableName = &$aVariables;
+
+        if ($isFileBasedEngine)
+            include($sCacheContent);
+        else
+            eval('?'.'>' . $sCacheContent);
+
+        $sContent = ob_get_clean();
+
+        return $sContent;
+    }
+
+    /**
+     * Add JS file(s) to global output.
+     *
+     * @param  mixed          $mixedFiles string value represents a single JS file name. An array - array of JS file names.
+     * @param  boolean        $bDynamic   in the dynamic mode JS file(s) are not included to global output, but are returned from the function directly.
+     * @return boolean/string result of operation.
+     */
+    function addJs($mixedFiles, $bDynamic = false)
+    {
+        return $this->_processFiles('js', 'add', $mixedFiles, $bDynamic);
+    }
+
+    /**
+     * Add System JS file(s) to global output.
+     * System JS files are the files which are attached to all pages. They will be cached separately from the others.
+     *
+     * @param  mixed          $mixedFiles string value represents a single JS file name. An array - array of JS file names.
+     * @param  boolean        $bDynamic   in the dynamic mode JS file(s) are not included to global output, but are returned from the function directly.
+     * @return boolean/string result of operation.
+     */
+    function addJsSystem($mixedFiles)
+    {
+        return $this->_processFiles('js', 'add', $mixedFiles, false, true);
+    }
+
+    /**
+     * Delete JS file(s) from global output.
+     *
+     * @param  mixed   $mixedFiles string value represents a single JS file name. An array - array of JS file names.
+     * @return boolean result of operation.
+     */
+    function deleteJs($mixedFiles)
+    {
+        return $this->_processFiles('js', 'delete', $mixedFiles);
+    }
+
+    /**
+     * Delete System JS file(s) from global output.
+     *
+     * @param  mixed   $mixedFiles string value represents a single JS file name. An array - array of JS file names.
+     * @return boolean result of operation.
+     */
+    function deleteJsSystem($mixedFiles)
+    {
+        return $this->_processFiles('js', 'delete', $mixedFiles, false, true);
+    }
+
+    /**
+     * Compile JS files in one file.
+     *
+     * @param  string $sAbsolutePath CSS file absolute path(full URL for external CSS/JS files).
+     * @param  array  $aIncluded     an array of already included JS files.
+     * @return string result of operation.
+     */
+    function _compileJs($sAbsolutePath, &$aIncluded)
+    {
+        if(isset($aIncluded[$sAbsolutePath]))
+           return '';
+
+        $bExternal = strpos($sAbsolutePath, "http://") !== false || strpos($sAbsolutePath, "https://") !== false;
+        if($bExternal) {
+            $sPath = $sAbsolutePath;
+            $sName = '';
+
+            $sContent = bx_file_get_contents($sAbsolutePath);
+        } else {
+            $aFileInfo = pathinfo($sAbsolutePath);
+            $sPath = $aFileInfo['dirname'] . DIRECTORY_SEPARATOR;
+            $sName = $aFileInfo['basename'];
+
+            $sContent = file_get_contents($sPath . $sName);
+        }
+
+        if(empty($sContent))
+            return '';
+
+        $sUrl = bx_ltrim_str($sPath, realpath(BX_DIRECTORY_PATH_ROOT), BX_DOL_URL_ROOT);
+        $sUrl = str_replace(DIRECTORY_SEPARATOR, '/', $sUrl);
+
+        $sContent = "\r\n/*--- BEGIN: " . $sUrl . $sName . "---*/\r\n" . $sContent . ";\r\n/*--- END: " . $sUrl . $sName . "---*/\r\n";
+        $sContent = str_replace(array("\n\r", "\r\n", "\r"), "\n", $sContent);
+
+        $aIncluded[$sAbsolutePath] = 1;
+
+        return preg_replace(
+            array(
+                "'<bx_url_root />'",
+                "'\r\n'"
+            ),
+            array(
+                BX_DOL_URL_ROOT,
+                "\n"
+            ),
+            $sContent
+        );
+    }
+    /**
+     * Wrap an URL to JS file into JS tag.
+     *
+     * @param  string $sFile - URL to JS file.
+     * @return string the result of operation.
+     */
+    function _wrapInTagJs($sFile)
+    {
+        return "<script language=\"javascript\" type=\"text/javascript\" src=\"" . $sFile . "\"></script>";
+    }
+    /**
+     * Wrap JS code into JS tag.
+     *
+     * @param  string $sCode - JS code.
+     * @return string the result of operation.
+     */
+    function _wrapInTagJsCode($sCode)
+    {
+        return "<script language=\"javascript\" type=\"text/javascript\">\n<!--\n" . $sCode . "\n-->\n</script>";
+    }
+
+    /**
+     * Add CSS file(s) to global output.
+     *
+     * @param  mixed          $mixedFiles string value represents a single CSS file name. An array - array of CSS file names.
+     * @param  boolean        $bDynamic   in the dynamic mode CSS file(s) are not included to global output, but are returned from the function directly.
+     * @return boolean/string result of operation
+     */
+    function addCss($mixedFiles, $bDynamic = false)
+    {
+        return $this->_processFiles('css', 'add', $mixedFiles, $bDynamic);
+    }
+
+    /**
+     * Add System CSS file(s) to global output.
+     * System CSS files are the files which are attached to all pages. They will be cached separately from the others.
+     *
+     * @param  mixed          $mixedFiles string value represents a single CSS file name. An array - array of CSS file names.
+     * @return boolean/string result of operation
+     */
+    function addCssSystem($mixedFiles)
+    {
+        return $this->_processFiles('css', 'add', $mixedFiles, false, true);
+    }
+
+    /**
+     * Delete CSS file(s) from global output.
+     *
+     * @param  mixed   $mixedFiles string value represents a single CSS file name. An array - array of CSS file names.
+     * @return boolean result of operation.
+     */
+    function deleteCss($mixedFiles)
+    {
+        return $this->_processFiles('css', 'delete', $mixedFiles);
+    }
+    /**
+     * Delete System CSS file(s) from global output.
+     *
+     * @param  mixed   $mixedFiles string value represents a single CSS file name. An array - array of CSS file names.
+     * @return boolean result of operation.
+     */
+    function deleteCssSystem($mixedFiles)
+    {
+        return $this->_processFiles('css', 'delete', $mixedFiles, false, true);
+    }
+    /**
+     * Compile CSS files' structure(@see @import css_file_path) in one file.
+     *
+     * @param  string $sAbsolutePath CSS file absolute path(full URL for external CSS/JS files).
+     * @param  array  $aIncluded     an array of already included CSS files.
+     * @return string result of operation.
+     */
+    function _compileCss($sAbsolutePath, &$aIncluded)
+    {
+        if(isset($aIncluded[$sAbsolutePath]))
+           return '';
+
+        $bExternal = strpos($sAbsolutePath, "http://") !== false || strpos($sAbsolutePath, "https://") !== false;
+        if($bExternal) {
+            $sPath = $sAbsolutePath;
+            $sName = '';
+
+            $sContent = bx_file_get_contents($sAbsolutePath);
+        } else {
+            $aFileInfo = pathinfo($sAbsolutePath);
+            $sPath = $aFileInfo['dirname'] . DIRECTORY_SEPARATOR;
+            $sName = $aFileInfo['basename'];
+
+            $sContent = file_get_contents($sPath . $sName);
+        }
+
+        if(empty($sContent))
+            return '';
+
+        $sUrl = bx_ltrim_str($sPath, realpath(BX_DIRECTORY_PATH_ROOT), BX_DOL_URL_ROOT);
+        $sUrl = str_replace(DIRECTORY_SEPARATOR, '/', $sUrl);
+
+        $sContent = "\r\n/*--- BEGIN: " . $sUrl . $sName . "---*/\r\n" . $sContent . "\r\n/*--- END: " . $sUrl . $sName . "---*/\r\n";
+        $aIncluded[$sAbsolutePath] = 1;
+
+        $sContent = str_replace(array("\n\r", "\r\n", "\r"), "\n", $sContent);
+        if($bExternal) {
+            $sContent = preg_replace(
+                array(
+                    "'@import\s+url\s*\(\s*[\'|\"]*\s*([a-zA-Z0-9\.\/_-]+)\s*[\'|\"]*\s*\)\s*;'",
+                    "'url\s*\(\s*[\'|\"]*\s*([a-zA-Z0-9\.\/\?\#_=-]+)\s*[\'|\"]*\s*\)'"
+                ),
+                array(
+                    "",
+                    "'url('" . $sPath . "'\\1)'"
+                ),
+                $sContent
+            );
+        }
+        else {
+        	try {
+        		$oTemplate = &$this;
+
+	            $sContent = preg_replace_callback(
+	                "'@import\s+url\s*\(\s*[\'|\"]*\s*([a-zA-Z0-9\.\/_-]+)\s*[\'|\"]*\s*\)\s*;'", function ($aMatches)  use($oTemplate, $sPath, $aIncluded) {
+	                	return $oTemplate->_compileCss(realpath($sPath . dirname($aMatches[1])) . DIRECTORY_SEPARATOR . basename($aMatches[1]), $aIncluded);
+	                }, $sContent);
+	
+	            $sContent = preg_replace_callback(
+	                "'url\s*\(\s*[\'|\"]*\s*([a-zA-Z0-9\.\/\?\#_=-]+)\s*[\'|\"]*\s*\)'", function ($aMatches)  use($oTemplate, $sPath) {
+						$sFile = basename($aMatches[1]);
+				        $sDirectory = dirname($aMatches[1]);
+				
+				        $sRootPath = realpath(BX_DIRECTORY_PATH_ROOT);
+				        $sAbsolutePath = realpath(addslashes($sPath) . $sDirectory) . DIRECTORY_SEPARATOR . $sFile;
+				
+				        $sRootPath = str_replace(DIRECTORY_SEPARATOR, '/', $sRootPath);
+				        $sAbsolutePath = str_replace(DIRECTORY_SEPARATOR, '/', $sAbsolutePath);
+				
+				        return 'url(' . bx_ltrim_str($sAbsolutePath, $sRootPath, BX_DOL_URL_ROOT) . ')';
+	                }, $sContent);
+			}
+	    	catch(Exception $oException) {
+	        	return '';
+	        }
+        }
+
+        return $sContent;
+    }
+
+    /**
+     * Less CSS
+     *
+     * @param  mixed $mixed CSS string to process with Less compiler or an array with CSS file's Path and URL.
+     * @return mixed string or an array with CSS file's Path and URL.
+     */
+    function _lessCss($mixed)
+    {
+    	require_once(BX_DIRECTORY_PATH_PLUGINS . 'lessphp/Less.php');
+
+        if(is_array($mixed) && isset($mixed['url']) && isset($mixed['path'])) {
+            $sPathFile = realpath($mixed['path']);
+            $aInfoFile = pathinfo($sPathFile);
+            if (!isset($aInfoFile['extension']) || $aInfoFile['extension'] != 'less')
+                return $mixed;
+
+            require_once(BX_DIRECTORY_PATH_PLUGINS . 'lessphp/Cache.php');
+        	$aFiles = array($mixed['path'] => $mixed['url']);
+        	$aOptions = array('cache_dir' => $this->_sCachePublicFolderPath);
+        	$sFile = Less_Cache::Get($aFiles, $aOptions, $this->_oConfigTemplate->aLessConfig);
+
+            return array('url' => $this->_sCachePublicFolderUrl . $sFile, 'path' => $this->_sCachePublicFolderPath . $sFile);
+        }
+
+        $oLess = new Less_Parser();
+        $oLess->ModifyVars($this->_oConfigTemplate->aLessConfig);
+        $oLess->parse($mixed);
+        return $oLess->getCss();
+    }
+
+    /**
+     * Minify CSS
+     *
+     * @param  string $s CSS string to minify
+     * @return string minified CSS string.
+     */
+    function _minifyCss($s)
+    {
+        require_once(BX_DIRECTORY_PATH_PLUGINS . 'minify/lib/Minify/CSS/Compressor.php');
+        return Minify_CSS_Compressor::process($s);
+    }
+
+    /**
+     * Wrap an URL to CSS file into CSS tag.
+     *
+     * @param  string $sFile - URL to CSS file.
+     * @return string the result of operation.
+     */
+    function _wrapInTagCss($sFile)
+    {
+        if (!$sFile)
+            return '';
+        return "<link href=\"" . $sFile . "\" rel=\"stylesheet\" type=\"text/css\" />";
+    }
+    /**
+     * Wrap CSS code into CSS tag.
+     *
+     * @param  string $sCode - CSS code.
+     * @return string the result of operation.
+     */
+    function _wrapInTagCssCode($sCode)
+    {
+        return "<link rel=\"stylesheet\" type=\"text/css\">" . $sCode . "</link>";
+    }
+    /**
+     * Include CSS/JS file(s) attached to the page in its head section.
+     * @see the method is system and would be called automatically.
+     *
+     * @param  string $sType the type of file('js' or 'css')
+     * @return string the result CSS code.
+     */
+    function includeFiles($sType, $bSystem = false)
+    {
+        $sUpcaseType = ucfirst($sType);
+
+        $sArrayKey = $sType . ($bSystem ? '_system' : '_compiled');
+        $aFiles = isset($this->aPage[$sArrayKey]) ? $this->aPage[$sArrayKey] : array();
+        if(empty($aFiles) || !is_array($aFiles))
+            return "";
+
+        if(!$this->{'_b' . $sUpcaseType . 'Cache'})
+            return $this->_includeFiles($sType, $aFiles);
+
+        //--- If cache already exists, return it ---//
+        $sMethodWrap = '_wrapInTag' . $sUpcaseType;
+        $sMethodCompile = '_compile' . $sUpcaseType;
+        $sMethodLess = '_less' . $sUpcaseType;
+        $sMethodMinify = '_minify' . $sUpcaseType;
+
+        ksort($aFiles);
+
+        $sName = "";
+        foreach($aFiles as $aFile)
+            $sName .= $aFile['url'];
+        $sName = $this->_getCacheFileName($sType, $sName);
+
+        $sCacheAbsoluteUrl = $this->_sCachePublicFolderUrl . $sName . '.' . $sType;
+        $sCacheAbsolutePath = $this->_sCachePublicFolderPath . $sName . '.' . $sType;
+        if(file_exists($sCacheAbsolutePath)) {
+            if($this->{'_b' . $sUpcaseType . 'Archive'})
+                $sCacheAbsoluteUrl = $this->_getLoaderUrl($sType, $sName);
+
+           return $this->$sMethodWrap($sCacheAbsoluteUrl);
+        }
+
+        //--- Collect all attached CSS/JS in one file ---//
+        $bLess = $this->_bLessEnable && method_exists($this, $sMethodLess);
+
+        $sResult = "";
+        $aIncluded = array();
+        foreach($aFiles as $aFile) {
+        	if($bLess)
+				$aFile = $this->$sMethodLess($aFile);
+
+            if(($sContent = $this->$sMethodCompile($aFile['path'], $aIncluded)) !== false)
+                $sResult .= $sContent;
+        }
+
+        if ($this->_bMinifyEnable && method_exists($this, $sMethodMinify))
+            $sResult = $this->$sMethodMinify($sResult);
+
+        $mixedWriteResult = false;
+        if(!empty($sResult) && ($rHandler = fopen($sCacheAbsolutePath, 'w')) !== false) {
+            $mixedWriteResult = fwrite($rHandler, $sResult);
+            fclose($rHandler);
+            @chmod ($sCacheAbsolutePath, 0666);
+        }
+
+        if($mixedWriteResult === false)
+            return $this->_includeFiles($sType, $aFiles);
+
+        if($this->{'_b' . $sUpcaseType . 'Archive'})
+            $sCacheAbsoluteUrl = $this->_getLoaderUrl($sType, $sName);
+
+        return $this->$sMethodWrap($sCacheAbsoluteUrl);
+    }
+    /**
+     * Include CSS/JS files without caching.
+     *
+     * @param  string $sType  the file type (css or js)
+     * @param  array  $aFiles CSS/JS files to be added to the page.
+     * @return string result of operation.
+     */
+    function _includeFiles($sType, &$aFiles)
+    {
+        $sUpcaseType = ucfirst($sType);
+
+        $sMethodWrap = '_wrapInTag' . $sUpcaseType;
+        $sMethodLess = '_less' . $sUpcaseType;
+
+        $sResult = "";
+        foreach($aFiles as $aFile) {
+            if($this->_bLessEnable && method_exists($this, $sMethodLess))
+                $aFile = $this->$sMethodLess($aFile);
+
+            $sResult .= $this->$sMethodWrap($aFile['url']);
+        }
+
+        return $sResult;
+    }
+    /**
+     * Insert/Delete CSS file from output stack.
+     *
+     * @param  string  $sType      the file type (css or js)
+     * @param  string  $sAction    add/delete
+     * @param  mixed   $mixedFiles string value represents a single CSS file name. An array - array of CSS file names.
+     * @return boolean result of operation.
+     */
+    function _processFiles($sType, $sAction, $mixedFiles, $bDynamic = false, $bSystem = false)
+    {
+        if(empty($mixedFiles))
+            return $bDynamic ? "" : false;
+
+        if(is_string($mixedFiles))
+            $mixedFiles = array($mixedFiles);
+
+        $sUpcaseType = ucfirst($sType);
+        $sMethodLocate = '_getAbsoluteLocation' . $sUpcaseType;
+        $sMethodWrap = '_wrapInTag' . $sUpcaseType;
+        $sResult = '';
+        foreach($mixedFiles as $sFile) {
+            //--- Process 3d Party CSS/JS file ---//
+            if(strpos($sFile, "http://") !== false || strpos($sFile, "https://") !== false) {
+                $sUrl = $sFile;
+                $sPath = $sFile;
+            }
+            //--- Process Custom CSS/JS file ---//
+            else if(strpos($sFile, "|") !== false) {
+                $sFile = implode('', explode("|", $sFile));
+
+                $sUrl = BX_DOL_URL_ROOT . $sFile;
+                $sPath = realpath(BX_DIRECTORY_PATH_ROOT . $sFile);
+            }
+            //--- Process Common CSS/JS file(check in default locations) ---//
+            else {
+                $sUrl = $this->$sMethodLocate('url', $sFile);
+                $sPath = $this->$sMethodLocate('path', $sFile);
+            }
+
+            if(empty($sPath) || empty($sUrl))
+                continue;
+
+            $sArrayKey = $sType . ($bSystem ? '_system' : '_compiled');
+            switch($sAction) {
+                case 'add':
+                    if($bDynamic)
+                        $sResult .= $this->$sMethodWrap($sUrl);
+                    else {
+                        $bFound = false;
+                        $aSearchIn = $bSystem ? $this->aPage[$sArrayKey] : array_merge($this->aPage[$sType . '_system'], $this->aPage[$sArrayKey]);
+                        foreach($aSearchIn as $iKey => $aValue)
+                            if($aValue['url'] == $sUrl && $aValue['path'] == $sPath) {
+                                $bFound = true;
+                                break;
+                            }
+
+                        if(!$bFound)
+                            $this->aPage[$sArrayKey][] = array('url' => $sUrl, 'path' => $sPath);
+                    }
+                    break;
+                case 'delete':
+                    if(!$bDynamic)
+                        foreach($this->aPage[$sArrayKey]  as $iKey => $aValue)
+                            if($aValue['url'] == $sUrl) {
+                                unset($this->aPage[$sArrayKey][$iKey]);
+                                break;
+                            }
+                    break;
+            }
+        }
+
+        return $bDynamic ? $sResult : true;
+    }
+
+    /**
+     * Parse content.
+     *
+     * @param  string $sContent            - HTML file's content.
+     * @param  array  $aVariables          - key/value pairs. key should be the same as template's key, but without prefix and postfix.
+     * @param  mixed  $mixedKeyWrapperHtml - key wrapper(string value if left and right parts are the same, array(0 => left, 1 => right) otherwise).
+     * @return string the result of operation.
+     */
+    function _parseContent($sContent, $aVariables, $mixedKeyWrapperHtml = null)
+    {
+        $aKeys = array_keys($aVariables);
+        $aValues = array_values($aVariables);
+
+        $aKeyWrappers = $this->_getKeyWrappers($mixedKeyWrapperHtml);
+
+        $iCountKeys = count($aKeys);
+        for ($i = 0; $i < $iCountKeys; $i++) {
+            if (strncmp($aKeys[$i], 'bx_repeat:', 10) === 0) {
+                $sKey = "'<" . $aKeys[$i] . ">(.*)<\/" . $aKeys[$i] . ">'s";
+
+                $aMatches = array();
+                preg_match($sKey, $sContent, $aMatches);
+
+                $sValue = '';
+                if(isset($aMatches[1]) && !empty($aMatches[1])) {
+                    if(is_array($aValues[$i]))
+                        foreach($aValues[$i] as $aValue)
+                            $sValue .= $this->parseHtmlByContent($aMatches[1], $aValue, $mixedKeyWrapperHtml);
+                    else if(is_string($aValues[$i]))
+                        $sValue = $aValues[$i];
+                }
+            } else if (strncmp($aKeys[$i], 'bx_if:', 6) === 0) {
+                $sKey = "'<" . $aKeys[$i] . ">(.*)<\/" . $aKeys[$i] . ">'s";
+
+                $aMatches = array();
+                preg_match($sKey, $sContent, $aMatches);
+
+                $sValue = '';
+                if(isset($aMatches[1]) && !empty($aMatches[1]))
+                    if(is_array($aValues[$i]) && isset($aValues[$i]['content']) && isset($aValues[$i]['condition']) && $aValues[$i]['condition'])
+                        $sValue .= $this->parseHtmlByContent($aMatches[1], $aValues[$i]['content'], $mixedKeyWrapperHtml);
+            } else {
+                $sKey = "'" . $aKeyWrappers['left'] . $aKeys[$i] . $aKeyWrappers['right'] . "'s";
+                $sValue = str_replace('$', '\\$', $aValues[$i]);
+            }
+
+            $aKeys[$i] = $sKey;
+            $aValues[$i] = $sValue;
+        }
+
+        try {
+        	$oTemplate = &$this;
+
+        	$aCallbackPatterns = array(
+        		"'<bx_include_auto:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_BOTH,
+        		"'<bx_include_base:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_BASE,
+        		"'<bx_include_tmpl:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_TMPL
+        	);
+	        foreach($aCallbackPatterns as $sPattern => $sCheckIn)
+		        $sContent = preg_replace_callback($sPattern, function($aMatches) use($oTemplate, $aVariables, $mixedKeyWrapperHtml, $sCheckIn) {
+		        	return $oTemplate->parseHtmlByName($aMatches[1], $aVariables, $mixedKeyWrapperHtml, $sCheckIn);
+		        }, $sContent);
+
+			$sContent = $this->_parseContentKeys($sContent, array(
+				"'<bx_injection:([^\s]+) />'s" => 'get_injection',
+            	"'<bx_menu:([^\s]+) \/>'s" => 'get_menu',
+			));
+        }
+		catch(Exception $oException) {
+        	return '';
+        }
+
+        $aKeys = array_merge($aKeys, array(
+            "'<bx_url_root />'",
+            "'<bx_url_studio />'",
+        ));
+        $aValues = array_merge($aValues, array(
+            BX_DOL_URL_ROOT,
+            BX_DOL_URL_STUDIO,
+        ));
+
+        //--- Parse Predefined Keys ---//
+        $sContent = preg_replace($aKeys, $aValues, $sContent);
+
+        //--- Parse System Keys ---//
+        $sContent = preg_replace_callback("'" . $aKeyWrappers['left'] . "([a-zA-Z0-9_-]+)" . $aKeyWrappers['right'] . "'", function($aMatches) use($oTemplate, $mixedKeyWrapperHtml) {
+        	return $oTemplate->parseSystemKey($aMatches[1], $mixedKeyWrapperHtml);
+        }, $sContent);
+
+        return $sContent;
+    }
+
+    /**
+     * Compile content
+     *
+     * @param  string  $sContent            template.
+     * @param  string  $aVarName            variable name to be saved in the output file.
+     * @param  integer $iVarDepth           depth is used to process nesting, for example, in cycles.
+     * @param  array   $aVarValues          values to be compiled in.
+     * @param  mixed   $mixedKeyWrapperHtml key wrapper(string value if left and right parts are the same, array(0 => left, 1 => right) otherwise).
+     * @return string  the result of operation.
+     */
+    function _compileContent($sContent, $aVarName, $iVarDepth, $aVarValues, $mixedKeyWrapperHtml = null)
+    {
+        $aKeys = array_keys($aVarValues);
+        $aValues = array_values($aVarValues);
+
+        $aKeyWrappers = $this->_getKeyWrappers($mixedKeyWrapperHtml);
+
+        for($i = 0; $i < count($aKeys); $i++) {
+            if(strpos($aKeys[$i], 'bx_repeat:') === 0) {
+                $sKey = "'<" . $aKeys[$i] . ">(.*)<\/" . $aKeys[$i] . ">'s";
+
+                $aMatches = array();
+                preg_match($sKey, $sContent, $aMatches);
+
+                $sValue = '';
+                if(isset($aMatches[1]) && !empty($aMatches[1])) {
+                    if(empty($aValues[$i]) || !is_array($aValues[$i]))
+                        return false;
+
+                    $sIndex = "\$" . str_repeat("i", $iVarDepth);
+                    $sValue .= "<?php if(is_array(" . $aVarName . "['" . $aKeys[$i] . "'])) for(" . $sIndex . "=0; " . $sIndex . "<count(" . $aVarName . "['" . $aKeys[$i] . "']); " . $sIndex . "++){ ?>";
+                    if(($sInnerValue = $this->_compileContent($aMatches[1], $aVarName . "['" . $aKeys[$i] . "'][" . $sIndex . "]", $iVarDepth + 1, current($aValues[$i]), $mixedKeyWrapperHtml)) === false)
+                        return false;
+                    $sValue .= $sInnerValue;
+                    $sValue .= "<?php } else if(is_string(" . $aVarName . "['" . $aKeys[$i] . "'])) echo " . $aVarName . "['" . $aKeys[$i] . "']; ?>";
+                }
+            } else if(strpos($aKeys[$i], 'bx_if:') === 0) {
+                $sKey = "'<" . $aKeys[$i] . ">(.*)<\/" . $aKeys[$i] . ">'s";
+
+                $aMatches = array();
+                preg_match($sKey, $sContent, $aMatches);
+
+                $sValue = '';
+                if(isset($aMatches[1]) && !empty($aMatches[1])) {
+                    if(!is_array($aValues[$i]) || !isset($aValues[$i]['content']) || empty($aValues[$i]['content']) || !is_array($aValues[$i]['content']))
+                        return false;
+
+                    $sValue .= "<?php if(" . $aVarName . "['" . $aKeys[$i] . "']['condition']){ ?>";
+                    if(($sInnerValue = $this->_compileContent($aMatches[1], $aVarName . "['" . $aKeys[$i] . "']['content']", $iVarDepth, $aValues[$i]['content'], $mixedKeyWrapperHtml)) === false)
+                        return false;
+                    $sValue .= $sInnerValue;
+                    $sValue .= "<?php } ?>";
+                }
+            } else {
+                $sKey = "'" . $aKeyWrappers['left'] . $aKeys[$i] . $aKeyWrappers['right'] . "'s";
+                $sValue = "<?php echo " . $aVarName . "['" . $aKeys[$i] . "'];?>";
+            }
+
+            $aKeys[$i] = $sKey;
+            $aValues[$i] = $sValue;
+        }
+
+        try {
+        	$oTemplate = &$this;
+
+        	$aCallbackPatterns = array(
+        		"'<bx_include_auto:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_BOTH,
+        		"'<bx_include_base:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_BASE,
+        		"'<bx_include_tmpl:([^\s]+) \/>'s" => BX_DOL_TEMPLATE_CHECK_IN_TMPL
+        	);
+	        foreach($aCallbackPatterns as $sPattern => $sCheckIn)
+		        $sContent = preg_replace_callback($sPattern, function($aMatches) use($oTemplate, $aVarValues, $mixedKeyWrapperHtml, $sCheckIn) {
+		        	$mixedResult = $oTemplate->getCached($aMatches[1], $aVarValues, $mixedKeyWrapperHtml, $sCheckIn, false);
+		        	if($mixedResult === false)
+		        		throw new Exception('Unable to create cache file.');
+
+		        	return $mixedResult;
+		        }, $sContent);
+
+			$sContent = $this->_parseContentKeys($sContent);
+        }
+        catch(Exception $oException) {
+        	return false;
+        }
+
+        $aKeys = array_merge($aKeys, array(
+            "'<bx_injection:([^\s]+) />'s",
+            "'<bx_menu:([^\s]+) \/>'s",
+            "'<bx_url_root />'",
+            "'<bx_url_studio />'"
+        ));
+        $aValues = array_merge($aValues, array(
+            "<?php echo \$this->processInjection(\$this->aPage['name_index'], '\\1'); ?>",
+            "<?php echo \$this->getMenu('\\1'); ?>",
+            BX_DOL_URL_ROOT,
+            BX_DOL_URL_STUDIO
+        ));
+
+        //--- Parse Predefined Keys ---//
+        $sContent = preg_replace($aKeys, $aValues, $sContent);
+        //--- Parse System Keys ---//
+        $sContent = preg_replace( "'" . $aKeyWrappers['left'] . "([a-zA-Z0-9_-]+)" . $aKeyWrappers['right'] . "'", "<?php echo \$this->parseSystemKey('\\1', \$mixedKeyWrapperHtml);?>", $sContent);
+
+        return $sContent;
+    }
+    protected function _parseContentKeys($sContent, $aCallbackPatterns = array())
+    {
+    	$oTemplate = &$this;
+
+    	$aCallbackPatterns = array_merge($aCallbackPatterns, array(
+			"'<bx_image_url:([^\s]+) \/>'s" => "get_image_url",
+			"'<bx_icon_url:([^\s]+) \/>'s" => "get_icon_url",
+			"'<bx_text:([_\{\}\w\d\s]+[^\s]{1}) \/>'s" => "get_text",
+			"'<bx_text_js:([^\s]+) \/>'s" => "get_text_js",
+			"'<bx_text_attribute:([^\s]+) \/>'s" => "get_text_attribute",
+		));
+
+		foreach($aCallbackPatterns as $sPattern => $sAction)
+			$sContent = preg_replace_callback($sPattern, function($aMatches) use($oTemplate, $sAction) {
+		    	$sResult = '';
+
+	        	switch($sAction) {
+	        		case 'get_image_url':
+	        			$sResult = $oTemplate->getImageUrl($aMatches[1]);
+	        			break;
+	        		case 'get_icon_url':
+	        			$sResult = $oTemplate->getIconUrl($aMatches[1]);
+	        			break;
+					case 'get_text':
+	        			$sResult = _t($aMatches[1]);
+	        			break;
+	        		case 'get_text_js':
+	        			$sResult = bx_js_string(_t($aMatches[1]));
+	        			break;
+	        		case 'get_text_attribute':
+	        			$sResult = bx_html_attribute(_t($aMatches[1]));
+	        			break;
+	        		case 'get_injection':
+	        			$sResult = $oTemplate->processInjection($oTemplate->getPageNameIndex(), $aMatches[1]);
+	        			break;
+	        		case 'get_menu':
+	        			$sResult = $oTemplate->getMenu($aMatches[1]);
+	        			break;
+	        	}
+
+	        	return $sResult;
+	        }, $sContent);
+
+		return $sContent;
+    }
+
+    /**
+     * Get absolute location of some template's part.
+     *
+     * @param  string $sType    - result type. Available values 'url' and 'path'.
+     * @param  string $sFolder  - folders to be searched in. @see $_sFolderHtml, $_sFolderCss, $_sFolderImages and $_sFolderIcons
+     * @param  string $sName    - requested part name.
+     * @param  string $sCheckIn where the content would be searched(base, template, both)
+     * @return string absolute location (path/url) of the part.
+     */
+    function _getAbsoluteLocation($sType, $sFolder, $sName, $sCheckIn = BX_DOL_TEMPLATE_CHECK_IN_BOTH)
+    {
+        $sDirectory = $this->getPath();
+
+        if($sType == 'path') {
+            $sDivider = DIRECTORY_SEPARATOR;
+            $sRoot = BX_DIRECTORY_PATH_ROOT;
+        } else if($sType == 'url') {
+            $sDivider = '/';
+            $sRoot = BX_DOL_URL_ROOT;
+        }
+
+        if(strpos($sName,'|') !== false) {
+            $aParts = explode('|', $sName);
+            $sName = $aParts[1];
+
+            if(strpos($aParts[0],'@') !== false) {
+                $aLocationParts = explode('@', $aParts[0]);
+                $sLocationKey = $this->addLocation($aLocationParts[0], BX_DIRECTORY_PATH_ROOT . $aLocationParts[1], BX_DOL_URL_ROOT . $aLocationParts[1]);
+            }
+        }
+
+        $sResult = '';
+        $aLocations = array_reverse($this->_aLocations, true);
+        foreach($aLocations as $sKey => $aLocation) {
+            if(($sCheckIn == BX_DOL_TEMPLATE_CHECK_IN_BOTH || $sCheckIn == BX_DOL_TEMPLATE_CHECK_IN_TMPL) && extFileExists(BX_DIRECTORY_PATH_MODULES . $this->getPath(). 'data' . DIRECTORY_SEPARATOR . BX_DOL_TEMPLATE_FOLDER_ROOT . DIRECTORY_SEPARATOR . $sKey . DIRECTORY_SEPARATOR . $sFolder . $sName))
+                $sResult = $sRoot . 'modules' . $sDivider . $sDirectory. 'data' . $sDivider . 'template' . $sDivider . $sKey . $sDivider . $sFolder . $sName;
+            else if(($sCheckIn == BX_DOL_TEMPLATE_CHECK_IN_BOTH || $sCheckIn == BX_DOL_TEMPLATE_CHECK_IN_BASE) && extFileExists($aLocation['path'] . BX_DOL_TEMPLATE_FOLDER_ROOT . DIRECTORY_SEPARATOR . $sFolder . $sName))
+                $sResult = $aLocation[$sType] . BX_DOL_TEMPLATE_FOLDER_ROOT . $sDivider . $sFolder . $sName;
+            else
+                continue;
+            break;
+        }
+
+        /**
+         * try to find from received path
+         */
+        if(!$sResult && @is_file(BX_DIRECTORY_PATH_ROOT . $aParts[0] . DIRECTORY_SEPARATOR . $aParts[1])) {
+            $sResult = $sRoot . $aParts[0] . $sDivider . $aParts[1];
+        }
+
+        if(isset($sLocationKey))
+           $this->removeLocation($sLocationKey);
+
+        return $sType == 'path' && !empty($sResult) ? realpath($sResult) : $sResult;
+    }
+    /**
+     * Get absolute location of some template's part.
+     *
+     * @param  string $sType result type. Available values 'url' and 'path'.
+     * @param  string $sName requested part name.
+     * @return string absolute location (path/url) of the part.
+     */
+    function _getAbsoluteLocationJs($sType, $sName)
+    {
+        $sResult = '';
+        $aLocations = array_reverse($this->_aLocationsJs, true);
+        foreach($aLocations as $sKey => $aLocation) {
+            if(extFileExists($aLocation['path'] . $sName))
+                $sResult = $aLocation[$sType] . $sName;
+            else
+                continue;
+            break;
+        }
+        return $sType == 'path' && !empty($sResult) ? realpath($sResult) : $sResult;
+    }
+    function _getAbsoluteLocationCss($sType, $sName)
+    {
+    	$sNameLess = str_replace('.css', '.less', $sName);
+
+    	$sResult = $this->_getAbsoluteLocation($sType, $this->_sFolderCss, $sNameLess);
+    	if(!empty($sResult))
+    		return $sResult;
+
+        return $this->_getAbsoluteLocation($sType, $this->_sFolderCss, $sName);
+    }
+    /**
+     * Get inline data for Images and Icons.
+     *
+     * @param  string  $sType    image/icon
+     * @param  string  $sName    file name
+     * @param  string  $sCheckIn where the content would be searched(base, template, both)
+     * @return unknown
+     */
+    function _getInlineData($sType, $sName, $sCheckIn)
+    {
+        switch($sType) {
+            case 'image':
+                $sFolder = $this->_sFolderImages;
+                break;
+            case 'icon':
+                $sFolder = $this->_sFolderIcons;
+                break;
+        }
+        $sPath = $this->_getAbsoluteLocation('path', $sFolder, $sName, $sCheckIn);
+
+        $iFileSize = 0;
+        if($this->_bImagesInline && ($iFileSize = filesize($sPath)) !== false && $iFileSize < $this->_iImagesMaxSize) {
+            $aFileInfo = pathinfo($sPath);
+            return "data:image/" . strtolower($aFileInfo['extension']) . ";base64," . base64_encode(file_get_contents($sPath));
+        }
+
+        return false;
+    }
+    /**
+     * Get file name where the template would be cached.
+     *
+     * @param  string $sAbsolutePath template's real path.
+     * @return string the result of operation.
+     */
+    function _getCacheFileName($sType, $sAbsolutePath)
+    {
+        $sResult = bx_site_hash($sAbsolutePath);
+        switch($sType) {
+            case 'html':
+                $sResult = $this->_sCacheFilePrefix . bx_lang_name() . '_' . $this->_sCode .  '_' . $sResult;
+                break;
+            case 'css':
+                $sResult = $this->_sCssCachePrefix . $sResult;
+                break;
+            case 'js':
+                $sResult = $this->_sJsCachePrefix . $sResult;
+                break;
+        }
+
+        return $sResult;
+    }
+    /**
+     * Get template key wrappers(left, right)
+     *
+     * @param  mixed $mixedKeyWrapperHtml key wrapper(string value if left and right parts are the same, array(0 => left, 1 => right) otherwise).
+     * @return array result of operation.
+     */
+    function _getKeyWrappers($mixedKeyWrapperHtml)
+    {
+        $aResult = array();
+        if(!empty($mixedKeyWrapperHtml) && is_string($mixedKeyWrapperHtml))
+            $aResult = array('left' => $mixedKeyWrapperHtml, 'right' => $mixedKeyWrapperHtml);
+        else if(!empty($mixedKeyWrapperHtml) && is_array($mixedKeyWrapperHtml))
+            $aResult = array('left' => $mixedKeyWrapperHtml[0], 'right' => $mixedKeyWrapperHtml[1]);
+        else
+            $aResult = array('left' => $this->_sKeyWrapperHtml, 'right' => $this->_sKeyWrapperHtml);
+        return $aResult;
+    }
+
+    /**
+     * Process all added language translations and return them as a string.
+     *
+     * @return string with JS code.
+     */
+    function _processJsTranslations()
+    {
+        $sReturn = '';
+        if(isset($this->aPage['js_translations']) && is_array($this->aPage['js_translations'])) {
+            foreach($this->aPage['js_translations'] as $sKey => $sString)
+                $sReturn .= "'" .  bx_js_string($sKey) . "': '" . bx_js_string($sString) . "',";
+
+            $sReturn = substr($sReturn, 0, -1);
+        }
+
+        return '<script type="text/javascript" language="javascript">var aDolLang = {' . $sReturn . '};</script>';
+    }
+    /**
+     * Process all added options and return them as a string.
+     *
+     * @return string with JS code.
+     */
+    function _processJsOptions()
+    {
+        $sReturn = '';
+        if(isset($this->aPage['js_options']) && is_array($this->aPage['js_options'])) {
+            foreach($this->aPage['js_options'] as $sName => $mixedValue)
+                $sReturn .= "'" .  bx_js_string($sName) . "': '" . bx_js_string($mixedValue) . "',";
+
+            $sReturn = substr($sReturn, 0, -1);
+        }
+
+        return '<script type="text/javascript" language="javascript">var aDolOptions = {' . $sReturn . '};</script>';
+    }
+    /**
+     * Process all added images and return them as a string.
+     *
+     * @return string with JS code.
+     */
+    function _processJsImages()
+    {
+        $sReturn = '';
+        if(isset($this->aPage['js_images']) && is_array($this->aPage['js_images'])) {
+            foreach($this->aPage['js_images'] as $sKey => $sUrl)
+                $sReturn .= "'" .  bx_js_string($sKey) . "': '" . bx_js_string($sUrl) . "',";
+
+            $sReturn = substr($sReturn, 0, -1);
+        }
+
+        return '<script type="text/javascript" language="javascript">var aDolImages = {' . $sReturn . '};</script>';
+    }
+
+    /**
+     * Get Gzip loader URL.
+     *
+     * @param $sType content type CSS/JS
+     * @param $sName file name.
+     * @return string with URL
+     */
+    function _getLoaderUrl($sType, $sName)
+    {
+        return BX_DOL_URL_ROOT . 'gzip_loader.php?file=' . $sName . '.' . $sType;
+    }
+
+    /**
+     *
+     * Functions to display pages with errors, messages and so on.
+     *
+     */
+    function displayAccessDenied ($sMsg = '')
+    {
+        bx_import('BxDolLanguages');
+        header("HTTP/1.0 403 Forbidden");
+        $this->displayMsg($sMsg ? $sMsg : _t('_Access denied'));
+    }
+    function displayNoData ($sMsg = '')
+    {
+        bx_import('BxDolLanguages');
+        header("HTTP/1.0 204 No Content");
+        $this->displayMsg($sMsg ? $sMsg : _t('_Empty'));
+    }
+    function displayErrorOccured ($sMsg = '')
+    {
+        bx_import('BxDolLanguages');
+        header("HTTP/1.0 500 Internal Server Error");
+        $this->displayMsg($sMsg ? $sMsg : _t('_error occured'));
+    }
+    function displayPageNotFound ($sMsg = '')
+    {
+        bx_import('BxDolLanguages');
+        header("HTTP/1.0 404 Not Found");
+        $this->displayMsg($sMsg ? $sMsg : _t('_sys_request_page_not_found_cpt'));
+    }
+    function displayMsg ($s, $bTranslate = false)
+    {
+        $sTitle = $bTranslate ? _t($s) : $s;
+        $oTemplate = BxDolTemplate::getInstance();
+        $oTemplate->setPageNameIndex (BX_PAGE_DEFAULT);
+        $oTemplate->setPageHeader ($sTitle);
+        $oTemplate->setPageContent ('page_main_code', DesignBoxContent($sTitle, MsgBox($sTitle), BX_DB_PADDING_DEF));
+        $oTemplate->getPageCode();
+        exit;
+    }
+
+    /**
+     * * * * Static methods for work with template injections * * *
+     *
+     * Static method is used to add/replace the content of some key in the template.
+     * It's usefull when you don't want to modify existing template but need to add some data to existing template key.
+     *
+     * @param  integer $iPageIndex - page index where injections would processed. Use 0 if you want it to be done on all the pages.
+     * @param  string  $sKey       - template key.
+     * @param  string  $sValue     - the data to be added.
+     * @return string  the result of operation.
+     */
+    function processInjection($iPageIndex, $sKey, $sValue = "")
+    {
+        if($iPageIndex != 0 && isset($this->aPage['injections']['page_0'][$sKey]) && isset($this->aPage['injections']['page_' . $iPageIndex][$sKey]))
+           $aSelection = @array_merge($this->aPage['injections']['page_0'][$sKey], $this->aPage['injections']['page_' . $iPageIndex][$sKey]);
+        else if(isset($this->aPage['injections']['page_0'][$sKey]))
+           $aSelection = $this->aPage['injections']['page_0'][$sKey];
+        else if(isset($this->aPage['injections']['page_' . $iPageIndex][$sKey]))
+            $aSelection = $this->aPage['injections']['page_' . $iPageIndex][$sKey];
+        else
+            $aSelection = array();
+
+        if(is_array($aSelection))
+            foreach($aSelection as $aInjection) {
+
+                if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->beginInjection($sRand = time().rand());
+
+                $sInjData = '';
+                switch($aInjection['type']) {
+                    case 'text':
+                        $sInjData = $aInjection['data'];
+                        break;
+
+                    case 'service':
+                    	if(BxDolService::isSerializedService($aInjection['data']))
+                    		$sInjData = BxDolService::callSerialized($aInjection['data']);                    	
+                        break;
+                }
+
+                if((int)$aInjection['replace'] == 1)
+                    $sValue = $sInjData;
+                else
+                    $sValue .= $sInjData;
+
+                if (isset($GLOBALS['bx_profiler'])) $GLOBALS['bx_profiler']->endInjection($sRand, $aInjection);
+
+            }
+
+        return $sValue != '__' . $sKey . '__' ? str_replace('__' . $sKey . '__', '', $sValue) : $sValue;
+    }
+    /**
+     * Static method to add ingection available on the current page only.
+     *
+     * @param string  $sKey     - template's key.
+     * @param string  $sType    - injection type(text, php).
+     * @param string  $sData    - the data to be added.
+     * @param integer $iReplace - replace already existed data or not.
+     */
+    function addInjection($sKey, $sType, $sData, $iReplace = 0)
+    {
+        $this->aPage['injections']['page_0'][$sKey][] = array(
+            'page_index' => 0,
+            'key' => $sKey,
+            'type' => $sType,
+            'data' => $sData,
+            'replace' => $iReplace
+        );
+    }
+
+    function getPageCode($oTemplate = null)
+    {
+        if (empty($oTemplate))
+           $oTemplate = $this;
+
+        header( 'Content-type: text/html; charset=utf-8' );
+        header( 'X-Frame-Options: sameorigin' );
+        echo $oTemplate->parsePageByName('page_' . $oTemplate->getPageNameIndex() . '.html', $oTemplate->getPageContent());
+    }
+}
+
+/** @} */

+ 825 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolTranscoder.php

@@ -0,0 +1,825 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+define('BX_DOL_QUEUE_PENDING', 'pending'); ///< file is pending for processing
+define('BX_DOL_QUEUE_PROCESSING', 'processing'); ///< file is in the process
+define('BX_DOL_QUEUE_COMPLETE', 'complete'); ///< file is already processed
+define('BX_DOL_QUEUE_FAILED', 'failed'); ///< file processing was failed
+define('BX_DOL_QUEUE_DELETE', 'delete'); ///< file is subject to delete
+
+/**
+ * Base class for @see BxDolTranscoderImage and @see BxDolTranscoderVideo transcoders
+ */
+class BxDolTranscoder extends BxDol implements iBxDolFactoryObject
+{
+    protected $_sLog = ''; ///< file conversion log
+    protected $_aObject; ///< object properties
+    protected $_oStorage; ///< storage object, transcoded files are stored here
+    protected $_oDb; ///< database queries object
+    protected $_sRetinaSuffix = '@2x'; ///< handler suffix for retina image file
+    protected $_sQueueTable = ''; ///< table with queued files for transcoding
+    protected $_sQueueStorage = ''; ///< storage object to storage temporary files
+
+    /**
+     * constructor
+     */
+    protected function __construct($aObject, $oStorage)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+        $this->_oStorage = $oStorage;
+        $this->_oDb = null;
+    }
+
+    /**
+     * Get transcode object instance.
+     * @param $sObject - name of transcode object.
+     * @return false on error or instance of transcoder class.
+     */
+    public static function getObjectInstance($sObject)
+    {
+        if (isset($GLOBALS['bxDolClasses'][__CLASS__ . '!' . $sObject]))
+            return $GLOBALS['bxDolClasses'][__CLASS__ . '!'.$sObject];
+
+        // get transcode object
+        $aObject = BxDolTranscoderQuery::getTranscoderObject($sObject);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        if ($aObject['source_params'])
+            $aObject['source_params'] = unserialize($aObject['source_params']);
+
+        $aObject['filters'] = false; // filters are initialized on demand
+
+        // create storage object to store transcoded data
+        $oStorage = BxDolStorage::getObjectInstance($aObject['storage_object']);
+        if (!$oStorage)
+            return false;
+
+        // create instance
+        $sClass = $aObject['override_class_name'] ? $aObject['override_class_name'] : 'BxDolTranscoderImage';
+        if (!empty($aObject['override_class_file']))
+            require_once(BX_DIRECTORY_PATH_ROOT . $aObject['override_class_file']);
+
+        $o = new $sClass ($aObject, $oStorage);
+
+        return ($GLOBALS['bxDolClasses'][__CLASS__ . '!' . $sObject] = $o);
+    }
+
+    /**
+     * Delete outdated transcoed data from all transcoeeed objects, by last access time.
+     * It called on cron, usually every day.
+     * @return total number of pruned/deleted files
+     */
+    static public function pruning ()
+    {
+        $iCount = 0;
+        $aObjects = self::getTranscoderObjects();
+        foreach ($aObjects as $aObject) {
+            if (!$aObject['atime_tracking'] || !$aObject['atime_pruning'])
+                continue;
+            $oTranscoder = self::getObjectInstance($aObject['object']);
+            if (!$oTranscoder)
+                continue;
+            $iCount += $oTranscoder->prune();
+        }
+        return $iCount;
+    }
+
+    /**
+     * Get transcoder objects array
+     * @return array
+     */
+    static protected function getTranscoderObjects ()
+    {
+        return BxDolTranscoderQuery::getTranscoderObjects ();
+    }
+
+    /**
+     * Process files queued for transcoding
+     * Can be performed on separate server.
+     */
+    static public function processQueue ($bQueuePruning = true)
+    {
+        if ($bQueuePruning)
+            self::pruneQueue();
+
+        $a = BxDolTranscoderQuery::getNextInQueue (gethostname());
+        foreach ($a as $r) {
+            $o = self::getObjectInstance($r['transcoder_object']);
+            $aQueue = $o->getDb()->getFromQueue($r['file_id_source']); 
+            if (!$aQueue || $aQueue['status'] != BX_DOL_QUEUE_PENDING)
+                continue;
+            
+            $o->transcode($aQueue['file_id_source'], $aQueue['profile_id']);
+        }
+    }
+
+    static public function pruneQueue () 
+    {
+        $a = BxDolTranscoderQuery::getForDeletionFromQueue (gethostname());
+        if (!$a)
+            return false;
+
+        foreach ($a as $aQueue) {
+            $o = self::getObjectInstance($aQueue['transcoder_object']);
+            if ($o)
+                $o->deleteFromQueue($aQueue['file_id_source']);
+        }
+    }
+
+    /**
+     * Store completed files from tmp queue storage to the final storage
+     * Must be performed on the actual site.
+     */
+    static public function processCompleted ()
+    {
+        $a = BxDolTranscoderQuery::getCompletedFromQueue ();
+        foreach ($a as $r) {
+            $o = self::getObjectInstance($r['transcoder_object']);
+            $aQueue = $o->getDb()->getFromQueue($r['file_id_source']); 
+            if (!$aQueue || $aQueue['status'] != BX_DOL_QUEUE_COMPLETE)
+                continue;
+            
+            if ($aQueue['file_url_result'] && $aQueue['file_id_result']) {
+  
+                // get transcoded file from provided URL              
+                $sFileData = bx_file_get_contents ($aQueue['file_url_result']);
+                if (false === $sFileData) {
+                    $o->getDb()->updateQueueStatus($aQueue['file_id_source'], BX_DOL_QUEUE_FAILED, "download file failed: {$aQueue['file_url_result']}\n");
+                    continue;   
+                }
+                
+                $sTmpFile = $o->getTmpFilename ('.' . $aQueue['file_ext_result']);
+                if (!file_put_contents($sTmpFile, $sFileData)) {
+                    $o->getDb()->updateQueueStatus($aQueue['file_id_source'], BX_DOL_QUEUE_FAILED, "store downloaded file failed\n");
+                    @unlink($sTmpFile);
+                    continue;
+                }
+                
+                // store downloaded file in final storage
+                if ($o->storeTranscodedFile($aQueue['file_id_source'], $sTmpFile, $aQueue['profile_id']))
+                   $o->getDb()->updateQueueStatus($aQueue['file_id_source'], BX_DOL_QUEUE_DELETE); // mark to delete it (deletion is performed on the server where it was transcoded)
+                else
+                    $o->getDb()->updateQueueStatus($aQueue['file_id_source'], BX_DOL_QUEUE_FAILED, "store file failed:\n" . $o->getLog());
+
+                // delete tmp local file
+                @unlink($sTmpFile);
+            }
+        }
+    }
+
+    /**
+     * Register handlers array
+     * It can be called upon module enable event.
+     * @param $mixed array of transcoders objects, or just one object
+     */
+    static public function registerHandlersArray ($mixed)
+    {
+        self::_callFuncForObjectsArray ($mixed, 'registerHandlers');
+    }
+
+    /**
+     * Unregister handlers array
+     * It can be called upon module disbale event.
+     * @param $mixed array of transcoders objects, or just one object
+     */
+    static public function unregisterHandlersArray ($mixed)
+    {
+        self::_callFuncForObjectsArray ($mixed, 'unregisterHandlers');
+    }
+
+    /**
+     * Cleanup (queue for deletion all resized files)
+     * It can be called upon module disbale event.
+     * @param $mixed array of transcoders objects, or just one object
+     */
+    static public function cleanupObjectsArray ($mixed)
+    {
+        self::_callFuncForObjectsArray ($mixed, 'cleanup');
+    }
+
+    /**
+     * Called automatically, upon local(transcoded) file deletetion.
+     */
+    static public function onAlertResponseFileDeleteLocal ($oAlert, $sObject)
+    {
+        $oTranscoder = self::getObjectInstance($sObject);
+        if (!$oTranscoder)
+            return;
+
+        if ($oAlert->sAction != 'file_deleted')
+            return;
+
+        $oTranscoder->onDeleteFileLocal($oAlert->iObject);
+    }
+
+    /**
+     * Called automatically, upon original file deletetion.
+     */
+    static public function onAlertResponseFileDeleteOrig ($oAlert, $sObject)
+    {
+        $oTranscoder = self::getObjectInstance($sObject);
+        if (!$oTranscoder)
+            return;
+
+        if ($oAlert->sAction != 'file_deleted')
+            return;
+
+        $oTranscoder->onDeleteFileOrig($oAlert->iObject);
+    }
+
+    /**
+     * Called automatically, upon local(transcoded) file deletetion.
+     */
+    public function onDeleteFileLocal($iFileId)
+    {
+        return $this->_oDb->deleteFileTraces($iFileId);
+    }
+
+    /**
+     * Called automatically, upon original file deletetion.
+     */
+    public function onDeleteFileOrig($mixedHandler)
+    {
+        // deleted files queued for transcoding
+        $this->_oDb->deleteFromQueue ($mixedHandler);
+
+        // delete main file
+        $iFileId = $this->_oDb->getFileIdByHandler($mixedHandler);
+        if (!$iFileId)
+            return false;
+
+        if (!$this->_oStorage->deleteFile($iFileId))
+            return false;
+
+        // delete retina file
+        $iFileId = $this->_oDb->getFileIdByHandler($mixedHandler . $this->_sRetinaSuffix);
+        if ($iFileId)
+            $this->_oStorage->deleteFile($iFileId);
+
+        return true;
+    }
+
+    /**
+     * Register necessary alert handlers for automatic deletetion of transcoded data if source file is deleted.
+     * Make sure that you call it once, before first usage, for example upon module installation.
+     */
+    public function registerHandlers ()
+    {
+        if (!$this->_oDb->registerHandlers ())
+            return false;
+        return $this->clearCacheDB();
+    }
+
+    /**
+     * Unregister alert handlers for automatic deletetion of transcoded data if source file is deleted.
+     * Make sure that you call it once, for example upon module uninstallation.
+     */
+    public function unregisterHandlers ()
+    {
+        if (!$this->_oDb->unregisterHandlers ())
+            return false;
+        return $this->clearCacheDB();
+    }
+
+    /**
+     * Delete (queue for deletion) all resized files for the current object
+     */
+    public function cleanup ()
+    {
+        return $this->_oStorage->queueFilesForDeletionFromObject();
+    }
+
+    /**
+     * Get storage object where transcoded data is stored
+     */
+    public function getStorage()
+    {
+        return $this->_oStorage;
+    }
+
+    /**
+     * Get database object instance
+     */
+    public function getDb()
+    {
+        return $this->_oDb;
+    }
+
+    /**
+     * Get transcoded file url.
+     * If transcoded file is ready then direct url to the file is returned.
+     * If there is no transcoded data available, then special url is returned, upon opening this url image is transcoded automatically and redirects to the ready transcoed image.
+     * @params $mixedHandler - file handler
+     * @return file url, or false on error.
+     */
+    public function getFileUrl($mixedHandler)
+    {
+        if ($this->isFileReady($mixedHandler)) {
+
+            $mixedHandler = $this->processHandlerForRetinaDevice($mixedHandler);
+
+            $iFileId = $this->_oDb->getFileIdByHandler($mixedHandler);
+            if (!$iFileId)
+                return false;
+
+            $aFile = $this->_oStorage->getFile($iFileId);
+            if (!$aFile)
+                return false;
+
+            if ($this->_aObject['atime_tracking'])
+                $this->_oDb->updateAccessTime($mixedHandler);
+
+            return $this->_oStorage->getFileUrlById($iFileId);
+        }
+
+        if ($this->_sQueueTable && !$this->addToQueue($mixedHandler)) // add file to queue (when queue is enabled) if it isn't transcoded yet
+            return false;
+
+        return $this->getFileUrlNotReady($mixedHandler);
+    }
+
+    /**
+     * alias for @see getFileUrl
+     */
+    public function getFileUrlById($mixedHandler)
+    {
+        return $this->getFileUrl($mixedHandler);
+    }
+
+    /**
+     * Check if transcoded data is available. No need to call it directly, it is called automatically when it is needed.
+     * @params $mixedHandler - file handler
+     * @params $isCheckOutdated - check if transcoded file is outdated
+     * @return false if there is no ready transcoded file is available or it is outdated, true if file is ready
+     */
+    public function isFileReady ($mixedHandler, $isCheckOutdated = true)
+    {
+        $sMethodFileReady = 'isFileReady_' . $this->_aObject['source_type'];
+        return $this->$sMethodFileReady($mixedHandler, $isCheckOutdated);
+    }
+
+    /**
+     * Transcode file, no need to call it directly, it is called automatically when it is needed.
+     * @params $mixedHandler - file handler
+     * @params $iProfileId - optional profile id, to assign transcoded file ownership to, usually it is NOT assigned to any particular profile, so just leave it default
+     * @return true on success, false on error
+     */
+    public function transcode ($mixedHandler, $iProfileId = 0)
+    {
+        $sExtChange = false;
+
+        $this->_oDb->updateQueueStatus($mixedHandler, BX_DOL_QUEUE_PROCESSING, '', gethostname());
+
+        // create tmp file locally
+        $sSuffix = $this->_sQueueStorage ? 'Queue' : $this->_aObject['source_type'];
+        $sMethodStoreFile = 'storeFileLocally_' . $sSuffix;
+        $sTmpFile = $this->$sMethodStoreFile($mixedHandler);
+        if (!$sTmpFile) {
+            $this->_oDb->updateQueueStatus($mixedHandler, BX_DOL_QUEUE_FAILED, "store local tmp file failed ($sMethodStoreFile)");
+            return false;
+        }
+
+        // appply filters to tmp file
+        $this->initFilters ();
+        foreach ($this->_aObject['filters'] as $aParams) {
+            $this->clearLog();
+            $sMethodFilter = 'applyFilter_' . $aParams['filter'];
+            if (!method_exists($this, $sMethodFilter))
+                continue;
+            if (!$this->$sMethodFilter($sTmpFile, $aParams['filter_params'])) {
+                unlink($sTmpFile);
+                $this->_oDb->updateQueueStatus($mixedHandler, BX_DOL_QUEUE_FAILED, "filter failed: $sMethodFilter\n" . $this->getLog());
+                return false;
+            }
+
+            if (!empty($aParams['filter_params']['force_type'])) {
+                switch ($aParams['filter_params']['force_type']) {
+                    case 'jpeg':
+                    case 'jpg':
+                        $sExtChange = 'jpg';
+                        break;
+                    default:
+                        $sExtChange = $aParams['filter_params']['force_type'];
+                }
+            }
+
+            if ($sExtChange) {
+                $iDotPos = strrpos($sTmpFile, '.');
+                $sExtOld = false === $iDotPos ? '' : substr($sTmpFile, $iDotPos+1);
+                if ($sExtOld != $sExtChange) {
+                    $sTmpFileOld = $sTmpFile;
+                    $sTmpFile = false === $iDotPos ? $sTmpFile . '.' . $sExtChange : substr_replace($sTmpFile, $sExtChange, $iDotPos+1, strlen($sExtOld));
+                    if (!rename($sTmpFileOld, $sTmpFile)) {
+                        @unlink($sTmpFileOld);
+                        $this->_oDb->updateQueueStatus($mixedHandler, BX_DOL_QUEUE_FAILED, "change file extension failed: $sTmpFileOld => $sTmpFile");
+                        return false;
+                    }
+                }
+            }
+        }
+
+        $mixedHandler = $this->processHandlerForRetinaDevice($mixedHandler);
+        
+        $bRet = true;
+        if ($this->_sQueueStorage) {
+
+            $sFileUrlResult = '';
+            if (!($iFileIdResult = $this->storeTranscodedFileInQueueStorage($mixedHandler, $sTmpFile, $sFileUrlResult)))
+                $this->_oDb->updateQueueStatus($mixedHandler, BX_DOL_QUEUE_FAILED, "store queue tmp file failed:\n" . $this->getLog());
+            else
+                $this->_oDb->updateQueueStatus($mixedHandler, BX_DOL_QUEUE_COMPLETE, '', '', $iFileIdResult, $sFileUrlResult, $sExtChange);
+
+        } else {
+
+            $bRet = $this->storeTranscodedFile($mixedHandler, $sTmpFile, $iProfileId);
+            
+            if ($bRet)
+                $this->deleteFromQueue($mixedHandler);
+            else
+                $this->_oDb->updateQueueStatus($mixedHandler, BX_DOL_QUEUE_FAILED, "store file failed:\n" . $this->getLog());
+                
+        }
+
+        @unlink($sTmpFile);
+
+        return $bRet;
+    }
+
+    /**
+     * Delete outdated files by last access time.
+     * @return number of pruned/deleted files
+     */
+    public function prune ()
+    {
+        $aFiles = $this->_oDb->getFilesForPruning();
+        if (!$aFiles)
+            return false;
+        $iCount = 0;
+        foreach ($aFiles as $r)
+            $iCount += $this->_oStorage->deleteFile($r['file_id']) ? 1 : 0;
+        return $iCount;
+    }
+
+    public function getDevicePixelRatio ()
+    {
+        if (isset($_COOKIE['devicePixelRatio']) && (!isset($this->_aObject['source_params']['disable_retina']) || !$this->_aObject['source_params']['disable_retina'])) {
+            $iDevicePixelRatio = intval($_COOKIE['devicePixelRatio']);
+            if ($iDevicePixelRatio >= 2)
+                return 2;
+        }
+        return 1;
+    }
+
+    public function getOrigFileUrl ($mixedHandler)
+    {
+        $sMethodStoreFile = 'getOrigFileUrl_' . $this->_aObject['source_type'];
+        return $this->$sMethodStoreFile($mixedHandler);
+    }
+
+    /**
+     * Get file url when file isn't transcoded yet
+     */
+    public function getFileUrlNotReady($mixedHandler)
+    {
+        return false;
+    }
+
+    public function getTmpFilename ($sOverrideName = false)
+    {
+        if ($sOverrideName)
+            return BX_DIRECTORY_PATH_TMP . rand(10000, 99999) . $sOverrideName;
+        return tempnam(BX_DIRECTORY_PATH_TMP, $this->_aObject['object']);
+    }
+
+    public function clearLog()
+    {
+        $this->_sLog = '';
+    }
+
+    public function addToLog($s)
+    {
+        $this->_sLog .= $s;
+    }
+
+    public function getLog()
+    {
+        return $this->_sLog;
+    }
+
+    public function deleteFromQueue ($mixedHandler)
+    {
+        $sErrMsg = "delete queue tmp file failed:\n";
+        $a = $this->_oDb->getFromQueue($mixedHandler);
+        if (!$a || $a['server'] != gethostname()) // deletion must be performed on the server where it was transcoded
+            return false;
+        
+        if ($a['file_id_result'] && $this->_sQueueStorage && 0 != strncmp($a['log'], $sErrMsg, strlen($sErrMsg))) {
+            $oStorage = BxDolStorage::getObjectInstance($this->_sQueueStorage);
+            if (!$oStorage->deleteFile($a['file_id_result'])) {
+                $this->_oDb->updateQueueStatus($mixedHandler, BX_DOL_QUEUE_FAILED, $sErrMsg . $this->getLog());
+                return false;
+            }
+        }
+
+        return $this->_oDb->deleteFromQueue($mixedHandler);
+    }
+
+    public function storeTranscodedFile ($mixedHandler, $sTmpFile, $iProfileId)
+    {
+        $sMethodIsPrivate = 'isPrivate_' . $this->_aObject['source_type'];
+        $isPrivate = $this->$sMethodIsPrivate($mixedHandler);
+        $iFileId = $this->_oStorage->storeFileFromPath ($sTmpFile, $isPrivate, $iProfileId);
+        if (!$iFileId) {
+            $this->addToLog($this->_oStorage->getErrorString());
+            return false;
+        }
+
+        if (!$this->_oDb->updateHandler($iFileId, $mixedHandler)) {
+            $this->_oStorage->deleteFile($iFileId);
+            return false;
+        }
+
+        $this->_oStorage->afterUploadCleanup($iFileId, $iProfileId);
+
+        return true;
+    }
+
+    protected function storeTranscodedFileInQueueStorage ($mixedHandler, $sTmpFile, &$sFileUrlResult)
+    {
+        if (!$this->_sQueueStorage)
+            return false;
+
+        $oStorage = BxDolStorage::getObjectInstance($this->_sQueueStorage);
+
+        $iFileIdResult = $oStorage->storeFileFromPath ($sTmpFile, 1, 0);
+        if (!$iFileIdResult) {
+            $this->addToLog($oStorage->getErrorString());
+            return false;
+        }
+
+        if (!($sFileUrlResult = $oStorage->getFileUrlById($iFileIdResult))) {
+            $this->_oStorage->deleteFile($iFileIdResult);
+            return false;
+        }
+
+        $oStorage->afterUploadCleanup($iFileIdResult, 0);
+
+        return $iFileIdResult;
+    }
+
+    protected function addToQueue($mixedHandler, $iProfileId = 0)
+    {
+        if ($this->_oDb->getFromQueue($mixedHandler))
+            return true;
+        $sFileUrl = $this->getOrigFileUrl($mixedHandler);
+        return $this->_oDb->addToQueue ($mixedHandler, $sFileUrl, $iProfileId);
+    }
+
+    protected function applyFilter_Resize ($sFile, $aParams)
+    {
+        $o = BxDolImageResize::getInstance();
+        $o->removeCropOptions ();
+
+        $o->setSize (isset($aParams['w']) ? round($aParams['w'] * $this->getDevicePixelRatio()) : null, isset($aParams['h']) ? round($aParams['h'] * $this->getDevicePixelRatio()) : null);
+
+        if (isset($aParams['crop_resize']) && $aParams['crop_resize'])
+            $o->setAutoCrop (true);
+        elseif (isset($aParams['square_resize']) && $aParams['square_resize'])
+            $o->setSquareResize (true);
+        else
+            $o->setSquareResize (false);
+
+        if (IMAGE_ERROR_SUCCESS == $o->resize($sFile))
+            return true;
+
+        return false;
+    }
+
+    protected function isFileReady_Folder ($mixedHandlerOrig, $isCheckOutdated = true)
+    {
+        $mixedHandler = $this->processHandlerForRetinaDevice($mixedHandlerOrig);
+
+        $iFileId = $this->_oDb->getFileIdByHandler($mixedHandler);
+        if (!$iFileId)
+            return false;
+
+        $aFile = $this->_oStorage->getFile($iFileId);
+        if (!$aFile)
+            return false;
+
+        if ($isCheckOutdated) { // warning, $isCheckOutdated is partially supported for Folder source type - file modification is not checked
+            if ($this->_aObject['ts'] > $aFile['modified'] || !$this->getFilePath_Folder($mixedHandlerOrig)) { // if we changed transcoder object params or original file is deleted
+                // delete file, so it will be recreated next time
+                if ($this->_oStorage->deleteFile($aFile['id']))
+                    return false;
+            }
+        }
+
+        return true;
+    }
+
+    protected function isFileReady_Storage ($mixedHandlerOrig, $isCheckOutdated = true)
+    {
+        $mixedHandler = $this->processHandlerForRetinaDevice($mixedHandlerOrig);
+
+        $iFileId = $this->_oDb->getFileIdByHandler($mixedHandler);
+        if (!$iFileId)
+            return false;
+
+        $aFile = $this->_oStorage->getFile($iFileId);
+        if (!$aFile)
+            return false;
+
+        if ($isCheckOutdated) {
+            $oStorageOriginal = BxDolStorage::getObjectInstance($this->_aObject['source_params']['object']);
+            if ($oStorageOriginal) {
+                $aFileOriginal = $oStorageOriginal->getFile($mixedHandlerOrig);
+                if (!$aFileOriginal || $aFileOriginal['modified'] > $aFile['modified'] || $this->_aObject['ts'] >= $aFile['modified']) { // if original file was changed OR we changed transcoder object params
+                    // delete file, so it will be recreated next time
+                    if ($this->_oStorage->deleteFile($aFile['id']))
+                        return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    protected function isPrivate_Folder ($mixedHandler)
+    {
+        if ('no' == $this->_aObject['private'])
+            return false;
+        return true;
+    }
+
+    protected function isPrivate_Storage ($mixedHandler)
+    {
+        switch ($this->_aObject['private']) {
+            case 'no':
+                return false;
+            case 'yes':
+                return true;
+            default:
+            case 'auto':
+                $oStorageOriginal = BxDolStorage::getObjectInstance($this->_aObject['source_params']['object']);
+                if (!$oStorageOriginal)
+                    return true; // in case of error - make sure that file is not public accidentally
+                $aFile = $oStorageOriginal->getFile($mixedHandler);
+                return $oStorageOriginal->isFilePrivate($aFile['id']);
+        }
+    }
+
+    protected function getFilePath_Folder ($mixedHandler)
+    {
+        $sPath = $this->_aObject['source_params']['path'] . $mixedHandler;
+        if (!file_exists($sPath))
+            $sPath = BX_DIRECTORY_PATH_ROOT . $sPath;
+
+        if (!file_exists($sPath))
+            return false;
+
+        return $sPath;
+    }
+
+    protected function storeFileLocally_Folder ($mixedHandler)
+    {
+        $sPath = $this->getFilePath_Folder($mixedHandler);
+        if (!$sPath)
+            return false;
+
+        $sTmpFile = $this->getTmpFilename ($mixedHandler);
+        if (!copy($sPath, $sTmpFile))
+            return false;
+
+        return $sTmpFile;
+    }
+
+    protected function storeFileLocally_Storage ($mixedHandler)
+    {
+        $oStorageOriginal = BxDolStorage::getObjectInstance($this->_aObject['source_params']['object']);
+        if (!$oStorageOriginal)
+            return false;
+
+        $aFile = $oStorageOriginal->getFile($mixedHandler);
+        if (!$aFile)
+            return false;
+
+        $sUrl = $oStorageOriginal->getFileUrlById($mixedHandler);
+        if (!$sUrl)
+            return false;
+
+        $sFileData = bx_file_get_contents ($sUrl);
+        if (false === $sFileData)
+            return false;
+
+        $sTmpFile = $this->getTmpFilename ($aFile['file_name']);
+        if (!file_put_contents($sTmpFile, $sFileData))
+            return false;
+
+        return $sTmpFile;
+    }
+
+    protected function storeFileLocally_Queue ($mixedHandler)
+    {   
+        $aQueue = $this->_oDb->getFromQueue($mixedHandler);
+        if (!$aQueue || !$aQueue['file_url_source'])
+            return false;
+
+        $sFileData = bx_file_get_contents ($aQueue['file_url_source']);
+        if (false === $sFileData)
+            return false;
+
+        $sTmpFile = $this->getTmpFilename ($mixedHandler);
+        if (!file_put_contents($sTmpFile, $sFileData))
+            return false;
+
+        return $sTmpFile;
+    }
+
+    protected function getOrigFileUrl_Folder ($mixedHandler)
+    {
+        $sPath = $this->_aObject['source_params']['path'] . $mixedHandler;
+        if (!file_exists(BX_DIRECTORY_PATH_ROOT . $sPath))
+            return false;
+        
+        return BX_DOL_URL_ROOT . $sPath;
+    }
+
+    protected function getOrigFileUrl_Storage ($mixedHandler)
+    {
+        $oStorageOriginal = BxDolStorage::getObjectInstance($this->_aObject['source_params']['object']);
+        if (!$oStorageOriginal)
+            return false;
+
+        return $oStorageOriginal->getFileUrlById($mixedHandler);
+    }
+
+    protected function initFilters ()
+    {
+        if (false !== $this->_aObject['filters']) // filters are already initilized
+            return;
+
+        // get transcoder filters
+        $aFilters = $this->_oDb->getTranscoderFilters();
+        $this->_aObject['filters'] = array();
+        foreach ($aFilters as $aFilter) {
+            $aFilter['filter_params'] = $aFilter['filter_params'] ? unserialize($aFilter['filter_params']) : array();
+            $this->_aObject['filters'][] = $aFilter;
+        }
+
+    }
+
+    protected function clearCacheDB()
+    {
+        $oCacheDb = $this->_oDb->getDbCacheObject();
+        return $oCacheDb->removeAllByPrefix('db_');
+    }
+
+    public function getDevicePixelRatioHandlerSuffix ()
+    {
+        $iDevicePixelRatio = $this->getDevicePixelRatio ();
+        if ($iDevicePixelRatio >= 2)
+            return $this->_sRetinaSuffix;
+        return '';
+    }
+
+    protected function processHandlerForRetinaDevice ($mixedHandler)
+    {
+        return '' . $mixedHandler . $this->getDevicePixelRatioHandlerSuffix ();
+    }
+
+    static protected function _callFuncForObjectsArray ($mixed, $sFunc)
+    {
+        if (!is_array($mixed))
+            $mixed = array($mixed);
+        foreach ($mixed as $sObject) {
+            $oTranscoder = self::getObjectInstance($sObject);
+            if ($oTranscoder)
+                $oTranscoder->$sFunc();
+        }
+    }
+
+    /**
+     * Get filter params
+     * @param $sName filter name
+     * @return array of filter params if filter was found, or false if given filter isn't enabled for the transcoder
+     */
+    protected function getFilterParams ($sName)
+    {
+        $this->initFilters ();
+        foreach ($this->_aObject['filters'] as $aParams)
+            if ($sName == $aParams['filter'])
+                return $aParams['filter_params'];
+        return false;
+    }
+}
+
+/** @} */

+ 170 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolTranscoderImage.php

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

+ 24 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolTranscoderImageQuery.php

@@ -0,0 +1,24 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * @see BxDolTranscoderVideo
+ */
+class BxDolTranscoderImageQuery extends BxDolTranscoderQuery
+{
+    public function __construct($aObject)
+    {
+        parent::__construct($aObject, false);
+        $this->_sTableFiles = '`sys_transcoder_images_files`';
+        $this->_sHandlerPrefix = 'sys_image_transcoder_';
+    }
+}
+
+/** @} */
+

+ 76 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolTranscoderProxy.php

@@ -0,0 +1,76 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * @page objects
+ * @section transcoder_proxy Proxy Transcoder 
+ * @ref BxDolTranscoderProxy
+ */
+
+/**
+ * This transcoder can be used as universal transcoder for videos and images.
+ * Depending on original file MIME type it calls the appropriate transcoder.
+ * Proxied transcoders names are specified in 'source_params' array:
+ * - object: original file storage object
+ * - image: image transcoder name
+ * - video_poster: video transcoder name for video poster image 
+ * - video: video transcoders array to force conversion for
+ */
+class BxDolTranscoderProxy extends BxDolTranscoder implements iBxDolFactoryObject
+{
+    protected function __construct($aObject, $oStorage)
+    {
+        parent::__construct($aObject, $oStorage);
+        $this->_oDb = new BxDolTranscoderImageQuery($aObject, false);
+        $this->_sQueueTable = $this->_oDb->getQueueTable();
+    }
+
+    /**
+     * Depending on original file mime type call appropriate transcoder 
+     */
+    public function getFileUrl($mixedHandler)
+    {
+        $oStorageOriginal = BxDolStorage::getObjectInstance($this->_aObject['source_params']['object']);
+        if (!$oStorageOriginal)
+            return false;
+
+        $aFile = $oStorageOriginal->getFile($mixedHandler);
+        if (!$aFile)
+            return false;
+
+        $sTranscoder = '';
+        if (0 === strncmp($aFile['mime_type'], 'image/', 6) && !empty($this->_aObject['source_params']['image'])) {
+            $sTranscoder = $this->_aObject['source_params']['image'];
+        } 
+        elseif (0 === strncmp($aFile['mime_type'], 'video/', 6) && !empty($this->_aObject['source_params']['video_poster'])) {
+            $sTranscoder = $this->_aObject['source_params']['video_poster'];
+
+            // if additional video transcoders provided call it to force video conversion
+            if (empty($this->_aObject['source_params']['video']))
+                continue;
+
+            foreach ($this->_aObject['source_params']['video'] as $sVideoTranscoder) {
+                if (!($oTranscoder = BxDolTranscoderVideo::getObjectInstance($sVideoTranscoder)))
+                    continue;
+                
+                $oTranscoder->getFileUrl($mixedHandler);                
+            }
+        }
+    
+        if (!$sTranscoder)
+            return false;
+
+        if (!($oTranscoder = BxDolTranscoderImage::getObjectInstance($sTranscoder)))
+            return false;
+        
+        return $oTranscoder->getFileUrl($mixedHandler);
+    }
+}
+
+/** @} */

+ 278 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolTranscoderQuery.php

@@ -0,0 +1,278 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+define('BX_DOL_QUEUE_PRUNE_FAILED', 604800); ///< timeout in seconds when 'failed' items are deleted
+define('BX_DOL_QUEUE_PRUNE_PROCESSING', 86400); ///< timeout in seconds when 'processing' items are deleted, in case when something went wrong during processing
+
+/**
+ * @see BxDolTranscoder
+ */
+class BxDolTranscoderQuery extends BxDolDb
+{
+    const TABLE_QUEUE = 'sys_transcoder_queue';
+
+    protected $_aObject;
+    protected $_sTableQueue;
+    protected $_sTableFiles;
+    protected $_sTableFilters;
+    protected $_sHandlerPrefix;
+
+    public function __construct($aObject, $bUseQueue = false)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+        $this->_sTableQueue = $bUseQueue ? self::TABLE_QUEUE : '';
+        $this->_sTableFiles = '`sys_transcoder_images_files`';
+        $this->_sTableFilters = '`sys_transcoder_filters`';
+        $this->_sHandlerPrefix = 'sys_image_transcoder_';
+    }
+
+    static public function getTranscoderObject ($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_transcoder` WHERE `object` = ?", $sObject);
+        return $oDb->getRow($sQuery);
+    }
+
+    static public function getTranscoderObjects ()
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_transcoder`");
+        return $oDb->getAll($sQuery);
+    }
+
+    public function getTranscoderFilters ()
+    {
+        $sQuery = $this->prepare("SELECT * FROM {$this->_sTableFilters} WHERE `transcoder_object` = ? ORDER BY `order` ASC", $this->_aObject['object']);
+        return $this->getAll($sQuery);
+    }
+
+    public function updateHandler ($iFileId, $mixedHandler)
+    {
+        $sUpdateATime = '';
+        if ($this->_aObject['atime_tracking']) {
+            $iTime = time();
+            $sUpdateATime = $this->prepare(", `atime` = ?", $iTime);
+        }
+        $sQuery = $this->prepare("INSERT INTO {$this->_sTableFiles} SET `transcoder_object` = ?, `file_id` = ?, `handler` = ? $sUpdateATime
+            ON DUPLICATE KEY UPDATE `file_id` = ? $sUpdateATime", $this->_aObject['object'], $iFileId, $mixedHandler, $iFileId);
+        return $this->res($sQuery);
+    }
+
+    public function getFileIdByHandler ($mixedHandler)
+    {
+        $sQuery = $this->prepare("SELECT `file_id` FROM {$this->_sTableFiles} WHERE `transcoder_object` = ? AND `handler` = ?", $this->_aObject['object'], $mixedHandler);
+        return $this->getOne($sQuery);
+    }
+
+    public function updateObjectModificationTime($iTime = 0)
+    {
+        $sQuery = $this->prepare("UPDATE `sys_objects_transcoder` SET `ts` = ? WHERE `object` = ?", $iTime ? $iTime : time(), $this->_aObject['object']);
+        return $this->res($sQuery);
+    }
+
+    public function updateAccessTime($mixedHandler)
+    {
+        $iTime = time();
+        $sQuery = $this->prepare("UPDATE {$this->_sTableFiles} SET `atime` = ? WHERE `transcoder_object` = ? AND `handler` = ?", $iTime, $this->_aObject['object'], $mixedHandler);
+        return $this->res($sQuery);
+    }
+
+    public function deleteFileTraces($iFileId)
+    {
+        $sQuery = $this->prepare("DELETE FROM {$this->_sTableFiles} WHERE `transcoder_object` = ? AND `file_id` = ?", $this->_aObject['object'], $iFileId);
+        return $this->res($sQuery);
+    }
+
+    public function getFilesForPruning ()
+    {
+        if (!$this->_aObject['atime_tracking'] || !$this->_aObject['atime_pruning'])
+            continue;
+
+        $sQuery = $this->prepare("SELECT * FROM {$this->_sTableFiles} WHERE `transcoder_object` = ? AND `atime` != 0 AND `atime` < ?", $this->_aObject['object'], time() - $this->_aObject['atime_pruning']);
+        return $this->getAll($sQuery);
+    }
+
+    public function registerHandlers ()
+    {
+        if (!$this->registerHandler ('getAlertHandlerNameLocal', 'alert_response_file_delete_local', $this->_aObject['object'], $this->_aObject['storage_object']))
+            return false;
+
+        // add handler for original storage engine
+        if ('Storage' == $this->_aObject['source_type']) // if original storage is "Storage", not "Folder"
+            if (!$this->registerHandler ('getAlertHandlerNameOrig', 'alert_response_file_delete_orig', $this->_aObject['object'], $this->_aObject['source_params']['object']))
+                return false;
+
+        return true;
+    }
+
+    public function unregisterHandlers ()
+    {
+        if (!$this->unregisterHandler('getAlertHandlerNameLocal', $this->_aObject['storage_object']))
+            return false;
+
+        // remove handler for original storage engine
+        if ('Storage' == $this->_aObject['source_type']) // if original storage is "Storage", not "Folder"
+            if (!$this->unregisterHandler('getAlertHandlerNameOrig', $this->_aObject['source_params']['object']))
+                return false;
+
+        return true;
+    }
+
+    static public function getForDeletionFromQueue ($sServer, $iLimit = 10)
+    {
+        $oDb = BxDolDb::getInstance();
+
+        // delete items which are stuck in processing status
+        $sQuery = $oDb->prepare("DELETE FROM `" . self::TABLE_QUEUE . "` WHERE `server` = ? AND `status` = ? AND `changed` < ? - ?", $sServer, BX_DOL_QUEUE_PROCESSING, time(), BX_DOL_QUEUE_PRUNE_PROCESSING);
+        $oDb->query($sQuery);
+
+        // get files which are subject to delete: with status 'delete' and expired 'failed' items
+        $sQuery = $oDb->prepare("SELECT * FROM `" . self::TABLE_QUEUE . "` WHERE `server` = ? AND ((`status` = ? AND `changed` < ? - ?) OR `status` = ?)", $sServer, BX_DOL_QUEUE_FAILED, time(), BX_DOL_QUEUE_PRUNE_FAILED, BX_DOL_QUEUE_DELETE);
+        return $oDb->getAll($sQuery);
+    }
+
+    static public function getCompletedFromQueue ($iLimit = 10)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `" . self::TABLE_QUEUE . "` WHERE `status` = 'complete' ORDER BY `added` ASC LIMIT ?", $iLimit);
+        return $oDb->getAll($sQuery);
+    }
+
+    static public function getNextInQueue ($sServer, $iLimit = 5)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT COUNT(*) FROM `" . self::TABLE_QUEUE . "` WHERE `server` = ? AND `status` = 'processing'", $sServer);
+        if ($oDb->getOne($sQuery))
+            return array();
+
+        $sQuery = $oDb->prepare("SELECT * FROM `" . self::TABLE_QUEUE . "` WHERE `status` = 'pending' ORDER BY `added` ASC LIMIT ?", $iLimit);
+        return $oDb->getAll($sQuery);
+    }
+
+    public function getQueueTable ()
+    {
+        return $this->_sTableQueue;
+    }
+
+    public function deleteFromQueue ($mixedId)
+    {
+        if (!$this->_sTableQueue)
+            return false;
+
+        $sQuery = $this->prepare("DELETE FROM `" . $this->_sTableQueue . "` WHERE `transcoder_object` = ? AND `file_id_source` = ?", $this->_aObject['object'], $mixedId);
+        return $this->query($sQuery);
+    }
+
+    public function getFromQueue ($mixedId)
+    {
+        if (!$this->_sTableQueue)
+            return false;
+
+        $sQuery = $this->prepare("SELECT * FROM `" . $this->_sTableQueue . "` WHERE `transcoder_object` = ? AND `file_id_source` = ?", $this->_aObject['object'], $mixedId);
+        return $this->getRow($sQuery);
+    }
+
+    public function addToQueue ($mixedId, $sFileUrl, $iProfileId)
+    {
+        if (!$this->_sTableQueue)
+            return false;
+        $sQuery = $this->prepare("INSERT INTO `" . $this->_sTableQueue . "` SET `transcoder_object` = ?, `profile_id` = ?, `file_url_source` = ?, `file_id_source` = ?, `status` = ?, `added` = ?, changed = ?", $this->_aObject['object'], $iProfileId, $sFileUrl, $mixedId, BX_DOL_QUEUE_PENDING, time(), time());
+        return $this->query($sQuery);
+    }
+
+    public function updateQueueStatus ($mixedId, $sStatus, $sLog = '', $sServer = '', $mixedIdResult = '', $sFileUrlResult = '', $sFileExtResult = '')
+    {
+        if (!$this->_sTableQueue)
+            return false;
+
+        $sQueryVals = $this->prepare("`status` = ?, changed = ? WHERE `transcoder_object` = ? AND `file_id_source` = ?", $sStatus, time(), $this->_aObject['object'], $mixedId);
+        $sQueryServer = $sServer ? $this->prepare(" `server` = ?, ", $sServer) : '';
+        $sQueryLog = $sLog ? $this->prepare(" `log` = ?, ", $sLog) : '';
+        $sQueryResultFile = $mixedIdResult && $sFileUrlResult ? $this->prepare(" `file_id_result` = ?, `file_url_result` = ?, `file_ext_result` = ?, ", $mixedIdResult, $sFileUrlResult, $sFileExtResult) : '';
+        $sQueryAll = "UPDATE `" . $this->_sTableQueue . "` SET " . $sQueryServer . $sQueryLog . $sQueryResultFile . $sQueryVals;
+        return $this->query($sQueryAll);
+    }
+
+    protected function registerHandler ($sHandlerNameFunc, $sServiceFunc, $sObject, $sUnit)
+    {
+        $sHandlerName = $this->$sHandlerNameFunc ();
+        $iHandlerId = $this->getAlertHandlerId ($sHandlerName);
+        if ($iHandlerId) // if handler already exists, do nothing
+            return true;
+
+        $sServiceCall = serialize(array(
+            'module' => 'system',
+            'method' => $sServiceFunc,
+            'params' => array($sObject),
+            'class' => 'TemplImageServices'
+        ));
+        $sQuery = $this->prepare("INSERT INTO `sys_alerts_handlers` SET `name` = ?, `service_call` = ?", $sHandlerName, $sServiceCall);
+        if (!$this->query($sQuery))
+            return false;
+        $iHandlerId = $this->lastId();
+        $sQuery = $this->prepare("INSERT INTO `sys_alerts` SET `unit` = ?, `action` = ?, `handler_id` = ?", $sUnit, 'file_deleted', $iHandlerId);
+        if (!$this->query($sQuery)) {
+            $this->unregisterHandlers ();
+            return false;
+        }
+
+        return true;
+    }
+
+    protected function unregisterHandler ($sHandlerNameFunc, $sUnit)
+    {
+        $sHandlerName = $this->$sHandlerNameFunc ();
+        $iHandlerId = $this->getAlertHandlerId ($sHandlerName);
+        if (!$iHandlerId) // if handler is alrady missing, do nothing
+            return true;
+
+        $sQuery = $this->prepare("DELETE FROM `sys_alerts` WHERE `unit` = ? AND `action` = ? AND `handler_id` = ?", $sUnit, 'file_deleted', $iHandlerId);
+        if (!$this->query($sQuery))
+            return false;
+
+        $sQuery = $this->prepare("DELETE FROM `sys_alerts_handlers` WHERE `id` = ?", $iHandlerId);
+        return $this->query($sQuery);
+    }
+
+    protected function getAlertHandlerNameLocal ()
+    {
+        return $this->_sHandlerPrefix . 'local_file_delete_' . $this->_aObject['object'];
+    }
+
+    protected function getAlertHandlerNameOrig ()
+    {
+        return $this->_sHandlerPrefix . 'orig_file_delete_' . $this->_aObject['object'];
+    }
+
+    protected function getAlertHandlerId ($sHandlerName)
+    {
+        $sQuery = $this->prepare("SELECT `id` FROM `sys_alerts_handlers` WHERE `name` = ?", $sHandlerName);
+        return $this->getOne($sQuery);
+    }
+
+    public function getTranscodedFileData($mixedHandler)
+    {
+        $sQuery = $this->prepare("SELECT `data` FROM {$this->_sTableFiles} WHERE `transcoder_object` = ? AND `handler` = ?", $this->_aObject['object'], $mixedHandler);
+        $s = $this->getOne($sQuery);
+        if (!$s)
+            return false;
+        return unserialize($s);
+    }
+
+    public function updateTranscodedFileData($mixedHandler, $mixedData)
+    {
+        $sData = $mixedData ? serialize($mixedData) : '';
+        $sQuery = $this->prepare("UPDATE {$this->_sTableFiles} SET `data` = ? WHERE `transcoder_object` = ? AND `handler` = ?", $sData, $this->_aObject['object'], $mixedHandler);
+
+        return $this->res($sQuery);
+    }
+}
+
+/** @} */

+ 271 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolTranscoderVideo.php

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

+ 24 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolTranscoderVideoQuery.php

@@ -0,0 +1,24 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * @see BxDolTranscoderVideo
+ */
+class BxDolTranscoderVideoQuery extends BxDolTranscoderQuery
+{
+    public function __construct($aObject)
+    {
+        parent::__construct($aObject, true);
+        $this->_sTableFiles = '`sys_transcoder_videos_files`';
+        $this->_sHandlerPrefix = 'sys_video_transcoder_';
+    }
+}
+
+/** @} */
+

+ 224 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolUpgrader.php

@@ -0,0 +1,224 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+class BxDolUpgrader extends BxDol
+{
+    protected $_sUrlVersionCheck = 'http://rss.boonex.com/trident/';
+    protected $_sError = false;
+
+    /**
+     * Prepare for system upgrade.
+     * It prepares upgrade by downloading upgrade pack and performing different checks.
+     * After all check are successfully completed, transient cron job is set to run upgrade upon next cron run.
+     * @return true if upgrade was successfully scheduled to run, or false on error (you can call @see getError to determine particular error message)
+     */
+    public function prepare()
+    {
+        $this->setError(false);
+        $iUmaskSave = umask(0);
+
+        while(true) {
+
+            $aVersionUpdateInfo = $this->getVersionUpdateInfo();
+            if (null === $aVersionUpdateInfo) {
+                $this->setError(_t('_sys_upgrade_get_version_info_failed'));
+                break;
+            }
+
+            if (!$this->isUpgradeAvailable($aVersionUpdateInfo))
+                break;
+
+            if (BX_DOL_VERSION != bx_get_ver()) {
+                $this->setError(_t('_sys_upgrade_db_and_files_versions_different'));
+                break;
+            }
+
+            $fChangedFilesPercent = 1;
+            $aFailedFiles = $this->checkFilesChecksums ($fChangedFilesPercent);
+            $bAutoupdateForceModifiedFiles = ('on' == getParam('sys_autoupdate_force_modified_files'));
+            if (!empty($aFailedFiles) && !$bAutoupdateForceModifiedFiles) {
+                $this->setError(_t('_sys_upgrade_files_checksum_failed', implode(', ', $aFailedFiles)));
+                break;
+            }
+            elseif ($fChangedFilesPercent > BX_FORCE_AUTOUPDATE_MAX_CHANGED_FILES_PERCENT && $bAutoupdateForceModifiedFiles) {
+                $this->setError(_t('_sys_upgrade_files_checksum_failed_too_many', round($fChangedFilesPercent * 100)));
+            }
+
+            if (!($sPatchPath = $this->downloadPatch ($aVersionUpdateInfo))) {
+                $this->setError(_t('_sys_upgrade_patch_download_failed'));
+                break;
+            }
+
+            if (!$this->isPatchChecksumCorrect ($sPatchPath, $aVersionUpdateInfo)) {
+                $this->deleteUpgradePatch($sPatchPath);
+                $this->setError(_t('_sys_upgrade_patch_checksum_failed'));
+                break;
+            }
+
+            if (!($sUnpackedPath = $this->unpackPatch ($sPatchPath, true))) {
+                $this->deleteUpgradePatch($sPatchPath);
+                $this->setError(_t('_sys_upgrade_patch_unpack_failed'));
+                break;
+            }            
+
+            $this->deleteUpgradePatch($sPatchPath);
+
+            if (!$this->isValidPatch ($sUnpackedPath, $aVersionUpdateInfo)) {
+                $this->deleteUpgradeFolder($sPatchPath);
+                $this->setError(_t('_sys_upgrade_patch_invalid'));
+                break;
+            }
+
+            if (!$this->setTransientUpgradeCronJob ($sUnpackedPath)) {
+                $this->deleteUpgradeFolder($sPatchPath);
+                $this->setError(_t('_sys_upgrade_set_cron_failed'));
+                break;
+            }
+
+            break;
+        }
+
+        umask($iUmaskSave);
+
+        return $this->getError() ? false : true;
+    }
+
+    public function getLatestVersionNumber ()
+    {
+        $a = $this->getVersionUpdateInfo ();
+        if (!$a)
+            return false;
+
+        return $a['latest_version'];
+    }
+
+    public function getVersionUpdateInfo ()
+    {
+        $s = bx_file_get_contents($this->_sUrlVersionCheck, array ('v' => bx_get_ver()));
+        if (!$s)
+            return null;
+
+        $a = json_decode($s, true);
+        if (!isset($a['latest_version']))
+            return null;
+
+        return $a;
+    }
+
+    public function isNewVersionAvailable ($a)
+    {
+        if (1 == version_compare(strtolower($a['latest_version']), strtolower(bx_get_ver())))
+            return true;
+        return false;
+    }
+
+    public function isUpgradeAvailable ($a)
+    {
+        if ($this->isNewVersionAvailable($a) && isset($a['patch']))
+            return true;
+        return false;
+    }
+
+    protected function downloadPatch ($a)
+    {
+        if (!isset($a['patch']['url']))
+            return false;
+
+        if (!($f = fopen($a['patch']['url'], "rb")))
+            return false;
+
+        $sTmpFile = BX_DIRECTORY_PATH_TMP . 'patch_' . bx_get_ver() . '_' . $a['patch']['ver'] . '.zip';
+        if (file_exists($sTmpFile) && !unlink($sTmpFile))
+            return false;        
+
+        $sRet = false;
+        if (false !== $sTmpFile && false !== file_put_contents($sTmpFile, $f))
+            $sRet = $sTmpFile;
+
+        fclose($f);
+
+        return $sRet;
+    }
+
+    protected function isPatchChecksumCorrect ($sPatchPath, $aVersionUpdateInfo)
+    {
+        return md5_file($sPatchPath) == $aVersionUpdateInfo['patch']['md5'];
+    }
+
+    protected function unpackPatch ($sPatchPath)
+    {
+        $sTmpFolder = $this->getTmpFolderFromZip($sPatchPath);
+        if (file_exists($sTmpFolder) && !bx_rrmdir($sTmpFolder))
+            return false;
+        
+        $oZip = new ZipArchive();
+        if ($oZip->open($sPatchPath) !== true)
+            return false;
+
+        $sRootFolder = $oZip->numFiles > 0 ? $oZip->getNameIndex(0) : false;
+        if (!$sRootFolder || !mkdir($sTmpFolder) || !$oZip->extractTo($sTmpFolder))
+            $sRootFolder = false;
+
+        $oZip->close();
+
+        return $sRootFolder ? $sTmpFolder . '/' . trim($sRootFolder, '/') . '/' : false;
+    }
+
+    protected function checkFilesChecksums (&$fChangedFilesPercent)
+    {
+        $oHasher = bx_instance('BxDolInstallerHasher');
+        return $oHasher->checkSystemFilesHash($fChangedFilesPercent);
+    }
+
+    protected function isValidPatch ($sUnpackedPath, $aVersionUpdateInfo)
+    {
+        $sPath = $sUnpackedPath . 'files/' . $this->normalizeVersion(bx_get_ver()) . '-' . $this->normalizeVersion($aVersionUpdateInfo['patch']['ver']) . '/';
+        $sCheckFilePath =  $sPath . 'check.php';
+        $sVersionFilePath =  $sPath . 'files/inc/version.inc.php';
+        return file_exists($sCheckFilePath) && file_exists($sVersionFilePath) && file_exists($sUnpackedPath . 'BxDolUpgradeCron.php');
+    }
+
+    public function setTransientUpgradeCronJob ($sUnpackedPath)
+    {
+        $sUnpackedPath = rtrim($sUnpackedPath, '/') . '/';
+        return BxDolCronQuery::getInstance()->addTransientJobClass('sys_perform_upgrade', 'BxDolUpgradeCron', bx_ltrim_str($sUnpackedPath, BX_DIRECTORY_PATH_ROOT) . 'BxDolUpgradeCron.php');
+    }
+
+    public function getError()
+    {
+        return $this->_sError;
+    }
+
+    protected function setError($s)
+    {
+        $this->_sError = $s;
+    }
+    
+    protected function normalizeVersion($s)
+    {
+        return str_replace(array('-', '_', ' '), '.', $s);
+    }
+
+    protected function getTmpFolderFromZip($sPatchPath)
+    {
+        return preg_replace('/\.zip$/', '', $sPatchPath);
+    }
+
+    protected function deleteUpgradeFolder($sPatchPath)
+    {
+        @bx_rrmdir($this->getTmpFolderFromZip($sPatchPath));
+    }
+
+    protected function deleteUpgradePatch($sPatchPath)
+    {
+        @unlink($sPatchPath);
+    }
+}
+
+/** @} */

+ 421 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolUploader.php

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

+ 37 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolUploaderQuery.php

@@ -0,0 +1,37 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * Database queries for uploader objects.
+ * @see BxDolUploader
+ */
+class BxDolUploaderQuery extends BxDolDb
+{
+    protected $_aObject;
+
+    public function __construct($aObject)
+    {
+        parent::__construct();
+        $this->_aObject = $aObject;
+    }
+
+    static public function getUploaderObject ($sObject)
+    {
+        $oDb = BxDolDb::getInstance();
+        $sQuery = $oDb->prepare("SELECT * FROM `sys_objects_uploader` WHERE `object` = ?", $sObject);
+        $aObject = $oDb->getRow($sQuery);
+        if (!$aObject || !is_array($aObject))
+            return false;
+
+        return $aObject;
+    }
+
+}
+
+/** @} */

+ 187 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolView.php

@@ -0,0 +1,187 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+define ('BX_DOL_VIEW_OLD_VIEWS', 3 * 86400); ///< views older than this number of seconds will be deleted automatically
+
+/**
+ * @page objects
+ * @section views Views
+ * @ref BxDolView
+ */
+
+/**
+ * Track any object views automatically
+ *
+ * Add record to sys_object_view table to track object views,
+ * to record view just create this class instance with your object id,
+ * for example:
+ * @code
+ *  BxDolView::getObjectInstance('my_system', 25); // 25 - is object id
+ * @endcode
+ *
+ * Description of sys_object_view table fields:
+ * @code
+ *  `name` - system name, it is better to use unique module prefix here, lowercase and all spaces are underscored
+ *  `table_track` - table to track views
+ *  `period` - period in secs to update next views, default is 86400(1 day)
+ *  `is_on` - is the system activated
+ *  `trigger_table` - table where you need to update views field
+ *  `trigger_field_id` - table field id to unique determine object
+ *  `trigger_field_count` - table field where total view number is stored
+ *  `class_name` - your custom class name, if you overrride default class
+ *  `class_file` - your custom class path
+ * @endcode
+ *
+ * Structure of the track table is the following:
+ * @code
+ *  CREATE TABLE `my_views_track` (
+ *      `object_id` int(11) NOT NULL default '0', -- this field type must be exact as your object id type
+ *      `viewer_id` int(11) NOT NULL default '0', -- viewer profile id
+ *      `viewer_nip` int(11) unsigned NOT NULL default '0', -- viewer ip address to track guest views
+ *      `date` int(11) NOT NULL default '0', -- timestamp of last recorded view
+ *      KEY `id` (`object_id`,`viewer_id`,`viewer_nip`)
+ *  ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+ * @endcode
+ *
+ */
+class BxDolView extends BxDolObject
+{
+    /**
+     * Constructor
+     */
+    function __construct($sSystem, $iId, $iInit = true)
+    {
+        parent::__construct($sSystem, $iId, $iInit);
+        if(empty($this->_sSystem))
+            return;
+
+        $this->_oQuery = new BxDolViewQuery($this);
+    }
+
+   /**
+     * get votes object instanse
+     * @param $sSys view object name
+     * @param $iId associated content id, where vote is available
+     * @param $iInit perform initialization
+     * @return null on error, or ready to use class instance
+     */
+    public static function getObjectInstance($sSys, $iId, $iInit = true)
+    {
+        if(isset($GLOBALS['bxDolClasses']['BxDolView!' . $sSys . $iId]))
+            return $GLOBALS['bxDolClasses']['BxDolView!' . $sSys . $iId];
+
+        $aSystems = self::getSystems();
+        if(!isset($aSystems[$sSys]))
+            return null;
+
+        $sClassName = 'BxTemplView';
+        if(!empty($aSystems[$sSys]['class_name'])) {
+            $sClassName = $aSystems[$sSys]['class_name'];
+            if(!empty($aSystems[$sSys]['class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aSystems[$sSys]['class_file']);
+        }
+
+        $o = new $sClassName($sSys, $iId, $iInit);
+        return ($GLOBALS['bxDolClasses']['BxDolView!' . $sSys . $iId] = $o);
+    }
+
+    public static function &getSystems()
+    {
+        if(!isset($GLOBALS['bx_dol_vote_systems']))
+            $GLOBALS['bx_dol_view_systems'] = BxDolDb::getInstance()->fromCache('sys_objects_view', 'getAllWithKey', '
+                SELECT
+                    `id` as `id`,
+                    `name` AS `name`,
+                    `table_track` AS `table_track`,
+                    `period` AS `period`,
+                    `is_on` AS `is_on`,
+                    `trigger_table` AS `trigger_table`,
+                    `trigger_field_id` AS `trigger_field_id`,
+                    `trigger_field_count` AS `trigger_field_count`,
+                    `class_name` AS `class_name`,
+                    `class_file` AS `class_file`
+                FROM `sys_objects_view`', 'name');
+
+        return $GLOBALS['bx_dol_view_systems'];
+    }
+
+    /**
+     * it is called on cron every day or similar period to clean old votes
+     */
+    public static function maintenance()
+    {
+        $iResult = 0;
+        $oDb = BxDolDb::getInstance();
+
+        $aSystems = self::getSystems();
+        foreach($aSystems as $aSystem) {
+            if(!$aSystem['is_on'])
+                continue;
+
+            $sQuery = $oDb->prepare("DELETE FROM `{$aSystem['table_track']}` WHERE `date` < (UNIX_TIMESTAMP() - ?)", BX_DOL_VIEW_OLD_VIEWS);
+            $iDeleted = (int)$oDb->query($sQuery);
+            if($iDeleted > 0)
+                $oDb->query("OPTIMIZE TABLE `{$aSystem['table_track']}`");
+
+            $iResult += $iDeleted;
+        }
+
+        return $iResult;
+    }
+
+    function doView()
+    {
+        if(!$this->isEnabled())
+            return false;
+
+        $iObjectId = $this->getId();
+        $iAuthorId = $this->_getAuthorId();
+        $sAuthorIp = $this->_getAuthorIp();
+
+        if($this->_oQuery->doView($iObjectId, $iAuthorId, $sAuthorIp)) {
+            $this->_triggerView();
+
+            $oZ = new BxDolAlerts($this->_sSystem, 'view', $iObjectId, $iAuthorId);
+            $oZ->alert();
+
+            return true;
+        }
+
+        return false;
+    }
+
+    function onObjectDelete($iObjectId = 0)
+    {
+        $this->_oQuery->deleteObjectViews($iObjectId ? $iObjectId : $this->getId());
+    }
+
+    protected function _getAuthorId ()
+    {
+        return isMember() ? bx_get_logged_profile_id() : 0;
+    }
+
+    protected function _getAuthorIp ()
+    {
+        return getVisitorIP();
+    }
+
+    protected function _triggerView()
+    {
+        if(!$this->_aSystem['trigger_table'])
+            return false;
+
+        $iId = $this->getId();
+        if(!$iId)
+            return false;
+
+        return $this->_oQuery->updateTriggerTable($iId);
+    }
+}
+
+/** @} */

+ 77 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolViewQuery.php

@@ -0,0 +1,77 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+/**
+ * @see BxDolView
+ */
+class BxDolViewQuery extends BxDolDb
+{
+    protected $_oModule;
+
+    protected $_sTableTrack;
+    protected $_sTriggerTable;
+    protected $_sTriggerFieldId;
+    protected $_sTriggerFieldCount;
+
+    protected $_iPeriod;
+
+    public function __construct(&$oModule)
+    {
+        parent::__construct();
+
+        $this->_oModule = $oModule;
+
+        $aSystem = $this->_oModule->getSystemInfo();
+        $this->_sTableTrack = $aSystem['table_track'];
+        $this->_sTriggerTable = $aSystem['trigger_table'];
+        $this->_sTriggerFieldId = $aSystem['trigger_field_id'];
+        $this->_sTriggerFieldCount = $aSystem['trigger_field_count'];
+
+        $this->_iPeriod = (int)$aSystem['period'];
+    }
+
+    public function doView($iObjectId, $iAuthorId, $sAuthorIp)
+    {
+        $iAuthorNip = ip2long($sAuthorIp);
+
+        if($iAuthorId)
+            $sWhere = $this->prepare(" AND `viewer_id` = ? ", $iAuthorId);
+        else
+            $sWhere = $this->prepare(" AND `viewer_id` = '0' AND `viewer_nip` = ? ", $iAuthorNip);
+
+        $sQuery = $this->prepare("SELECT `date` FROM `{$this->_sTableTrack}` WHERE `object_id` = ? $sWhere", $iObjectId);
+        $iDate = (int)$this->getOne($sQuery);
+        $iDateNow = time();
+
+        if(!$iDate) {
+            $sQuery = $this->prepare("INSERT IGNORE INTO `{$this->_sTableTrack}` SET `object_id` = ?, `viewer_id` = ?, `viewer_nip` = ?, `date` = ?", $iObjectId, $iAuthorId, $iAuthorNip, $iDateNow);
+            return (int)$this->query($sQuery) > 0;
+        }
+
+        if(($iDateNow - $iDate) > $this->_iPeriod) {
+            $sQuery = $this->prepare("UPDATE `{$this->_sTableTrack}` SET `date` = ? WHERE `object_id` = ? AND `viewer_id` = ? AND `viewer_nip` = ?", $iDateNow, $iObjectId, $iAuthorId, $iAuthorNip);
+            return (int)$this->query($sQuery) > 0;
+        }
+    }
+
+    public function deleteObjectViews($iObjectId)
+    {
+        $sQuery = $this->prepare("DELETE FROM {$this->_sTableTrack} WHERE `object_id` = ?", $iObjectId);
+        if ($this->query ($sQuery))
+            $this->query ("OPTIMIZE TABLE {$this->_sTableTrack}");
+    }
+
+    public function updateTriggerTable($iObjectId)
+    {
+        $sQuery = $this->prepare("UPDATE `{$this->_sTriggerTable}` SET `{$this->_sTriggerFieldCount}` = `{$this->_sTriggerFieldCount}` + 1 WHERE `{$this->_sTriggerFieldId}` = ?", $iObjectId);
+        return (int)$this->query($sQuery) > 0;
+    }
+}
+
+/** @} */

+ 386 - 0
upgrade/files/8.0.0.A10-8.0.0.A11/files/inc/classes/BxDolVote.php

@@ -0,0 +1,386 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) BoonEx Pty Limited - http://www.boonex.com/
+ * CC-BY License - http://creativecommons.org/licenses/by/3.0/
+ *
+ * @defgroup    TridentCore Trident Core
+ * @{
+ */
+
+define('BX_DOL_VOTE_OLD_VOTES', 365 * 86400); ///< votes older than this number of seconds will be deleted automatically
+
+define('BX_DOL_VOTE_TYPE_STARS', 'stars');
+define('BX_DOL_VOTE_TYPE_LIKES', 'likes');
+
+define('BX_DOL_VOTE_USAGE_BLOCK', 'block');
+define('BX_DOL_VOTE_USAGE_INLINE', 'inline');
+define('BX_DOL_VOTE_USAGE_DEFAULT', BX_DOL_VOTE_USAGE_BLOCK);
+
+/**
+ * @page objects
+ * @section votes Votes
+ * @ref BxDolVote
+ */
+
+/**
+ * Vote for any content
+ *
+ * Related classes:
+ * - BxDolVoteQuery - vote database queries
+ * - BxBaseVote - vote base representation
+ * - BxTemplVote - custom template representation
+ *
+ * AJAX vote for any content. Stars and Plus based representations are supported.
+ *
+ * To add vote section to your feature you need to add a record to 'sys_objects_vote' table:
+ *
+ * - ID - autoincremented id for internal usage
+ * - Name - your unique module name, with vendor prefix, lowercase and spaces are underscored
+ * - TableMain - table name where summary votigs are stored
+ * - TableTrack - table name where each vote is stored
+ * - PostTimeout - number of seconds to not allow duplicate vote
+ * - MinValue - min vote value, 1 by default
+ * - MaxValue - max vote value, 5 by default
+ * - IsUndo - is Undo enabled for Plus based votes
+ * - IsOn - is this vote object enabled
+ * - TriggerTable - table to be updated upon each vote
+ * - TriggerFieldId - TriggerTable table field with unique record id, primary key
+ * - TriggerFieldRate - TriggerTable table field with average rate
+ * - TriggerFieldRateCount - TriggerTable table field with votes count
+ * - ClassName - your custom class name, if you overrride default class
+ * - ClassFile - your custom class path
+ *
+ * You can refer to BoonEx modules for sample record in this table.
+ *
+ *
+ *
+ * @section example Example of usage:
+ * To get Star based vote you need to have different values for MinValue and MaxValue (for example 1 and 5)
+ * and IsUndo should be equal to 0. To get Plus(Like) based vote you need to have equal values
+ * for MinValue and MaxValue (for example 1) and IsUndo should be equal to 1. After filling the other
+ * paramenters in the table you can show vote in any place, using the following code:
+ * @code
+ * $o = BxDolVote::getObjectInstance('system object name', $iYourEntryId);
+ * if (!$o->isEnabled()) return '';
+ *     echo $o->getElementBlock();
+ * @endcode
+ *
+ *
+ * @section acl Memberships/ACL:
+ * - vote
+ *
+ *
+ *
+ * @section alerts Alerts:
+ * Alerts type/unit - every module has own type/unit, it equals to ObjectName.
+ * The following alerts are rised:
+ *
+ * - rate - comment was posted
+ *      - $iObjectId - entry id
+ *      - $iSenderId - rater user id
+ *      - $aExtra['rate'] - rate
+ *
+ */
+
+class BxDolVote extends BxDolObject
+{
+    public function __construct($sSystem, $iId, $iInit = true)
+    {
+        parent::__construct($sSystem, $iId, $iInit);
+        if(empty($this->_sSystem))
+            return;
+
+        $this->_oQuery = new BxDolVoteQuery($this);
+    }
+
+    /**
+     * get votes object instanse
+     * @param $sSys vote object name
+     * @param $iId associated content id, where vote is available
+     * @param $iInit perform initialization
+     * @return null on error, or ready to use class instance
+     */
+    public static function getObjectInstance($sSys, $iId, $iInit = true)
+    {
+        if(isset($GLOBALS['bxDolClasses']['BxDolVote!' . $sSys . $iId]))
+            return $GLOBALS['bxDolClasses']['BxDolVote!' . $sSys . $iId];
+
+        $aSystems = self::getSystems();
+        if(!isset($aSystems[$sSys]))
+            return null;
+
+        $sClassName = 'BxTemplVote';
+        if(!empty($aSystems[$sSys]['class_name'])) {
+            $sClassName = $aSystems[$sSys]['class_name'];
+            if(!empty($aSystems[$sSys]['class_file']))
+                require_once(BX_DIRECTORY_PATH_ROOT . $aSystems[$sSys]['class_file']);
+        }
+
+        $o = new $sClassName($sSys, $iId, $iInit);
+        return ($GLOBALS['bxDolClasses']['BxDolVote!' . $sSys . $iId] = $o);
+    }
+
+    public static function &getSystems()
+    {
+        if(!isset($GLOBALS['bx_dol_vote_systems']))
+            $GLOBALS['bx_dol_vote_systems'] = BxDolDb::getInstance()->fromCache('sys_objects_vote', 'getAllWithKey', '
+                SELECT
+                    `ID` as `id`,
+                    `Name` AS `name`,
+                    `TableMain` AS `table_main`,
+                    `TableTrack` AS `table_track`,
+                    `PostTimeout` AS `post_timeout`,
+                    `MinValue` AS `min_value`,
+                    `MaxValue` AS `max_value`,
+                    `IsUndo` AS `is_undo`,
+                    `IsOn` AS `is_on`,
+                    `TriggerTable` AS `trigger_table`,
+                    `TriggerFieldId` AS `trigger_field_id`,
+                    `TriggerFieldRate` AS `trigger_field_rate`,
+                    `TriggerFieldRateCount` AS `trigger_field_count`,
+                    `ClassName` AS `class_name`,
+                    `ClassFile` AS `class_file`
+                FROM `sys_objects_vote`', 'name');
+
+        return $GLOBALS['bx_dol_vote_systems'];
+    }
+
+    /**
+     * it is called on cron every day or similar period to clean old votes
+     */
+    public static function maintenance()
+    {
+        $iResult = 0;
+        $oDb = BxDolDb::getInstance();
+
+        $aSystems = self::getSystems();
+        foreach($aSystems as $aSystem) {
+            if(!$aSystem['is_on'])
+                continue;
+
+            $sQuery = $oDb->prepare("DELETE FROM `{$aSystem['table_track']}` WHERE `date` < (UNIX_TIMESTAMP() - ?)", BX_DOL_VOTE_OLD_VOTES);
+            $iDeleted = (int)$oDb->query($sQuery);
+            if($iDeleted > 0)
+                $oDb->query("OPTIMIZE TABLE `{$aSystem['table_track']}`");
+
+            $iResult += $iDeleted;
+        }
+
+        return $iResult;
+    }
+
+    public function isUndo()
+    {
+        return (int)$this->_aSystem['is_undo'] == 1;
+    }
+
+    public function isLikeMode()
+    {
+        $iMinValue = $this->getMinValue();
+        $iMaxValue = $this->getMaxValue();
+
+        return $iMinValue == $iMaxValue;
+    }
+
+    public function getMinValue()
+    {
+        return (int)$this->_aSystem['min_value'];
+    }
+
+    public function getMaxValue()
+    {
+        return (int)$this->_aSystem['max_value'];
+    }
+
+    /**
+     * Interface functions for outer usage
+     */
+    public function getStatCounter()
+    {
+        $aVote = $this->_oQuery->getVote($this->getId());
+        return $aVote['count'];
+    }
+
+    public function getStatRate()
+    {
+        $aVote = $this->_oQuery->getVote($this->getId());
+        return $aVote['rate'];
+    }
+
+    public function getSqlParts($sMainTable, $sMainField)
+    {
+        if(!$this->isEnabled())
+            return array();
+
+        return $this->_oQuery->getSqlParts($sMainTable, $sMainField);
+    }
+
+    /**
+     * Actions functions
+     */
+    public function actionVote()
+    {
+        if(!$this->isEnabled()) {
+            $this->_echoResultJson(array('code' => 1));
+            return;
+        }
+
+        $iObjectId = $this->getId();
+        $iAuthorId = $this->_getAuthorId();
+        $iAuthorIp = $this->_getAuthorIp();
+
+        $bUndo = $this->isUndo();
+        $bLikeMode = $this->isLikeMode();
+
+        $bVoted = $this->_oQuery->isVoted($iObjectId, $iAuthorId);
+        $bPerformUndo = $bVoted && $bUndo ? true : false;
+
+        if(!$bPerformUndo && !$this->isAllowedVote(true)) {
+            $this->_echoResultJson(array('code' => 2, 'msg' => $this->msgErrAllowedVote()));
+            return;
+        }
+
+        if((!$bLikeMode && !$this->_oQuery->isPostTimeoutEnded($iObjectId, $iAuthorIp)) || ($bLikeMode && $bVoted && !$bUndo)) {
+            $this->_echoResultJson(array('code' => 3, 'msg' => _t('_vote_err_duplicate_vote')));
+            return;
+        }
+
+        $iValue = bx_get('value');
+        if($iValue === false) {
+            $this->_echoResultJson(array('code' => 4));
+            return;
+        }
+
+        $iValue = bx_process_input($iValue, BX_DATA_INT);
+
+        $iMinValue = $this->getMinValue();
+        if($iValue < $iMinValue)
+            $iValue = $iMinValue;
+
+        $iMaxValue = $this->getMaxValue();
+        if($iValue > $iMaxValue)
+            $iValue = $iMaxValue;
+
+        if(!$this->_oQuery->putVote($iObjectId, $iAuthorId, $iAuthorIp, $iValue, $bPerformUndo)) {
+            $this->_echoResultJson(array('code' => 5));
+            return;
+        }
+
+        $this->_triggerVote();
+
+        $oZ = new BxDolAlerts($this->_sSystem, 'rate', $this->getId(), $iAuthorId, array('value' => $iValue, 'undo' => $bPerformUndo));
+        $oZ->alert();
+
+        $aVote = $this->_oQuery->getVote($iObjectId);
+        $this->_echoResultJson(array(
+            'code' => 0,
+            'rate' => $aVote['rate'],
+            'count' => $aVote['count'],
+            'countf' => (int)$aVote['count'] > 0 ? $this->_getLabelCounter($aVote['count']) : '',
+        	'label_icon' => $bLikeMode ? $this->_getIconDoLike(!$bVoted) : '',
+        	'label_title' => $bLikeMode ? _t($this->_getTitleDoLike(!$bVoted)) : '',
+        	'disabled' => !$bVoted && !$bUndo,
+        ));
+    }
+
+    public function actionGetVotedBy()
+    {
+        if (!$this->isEnabled())
+           return '';
+
+        return $this->_getVotedBy();
+    }
+
+    /**
+     * Permissions functions
+     */
+    public function 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 isAllowedVote($isPerformAction = false)
+    {
+        if(isAdmin())
+            return true;
+
+        return $this->checkAction('vote', $isPerformAction);
+    }
+
+    public function msgErrAllowedVote()
+    {
+        return $this->checkActionErrorMsg('vote');
+    }
+
+    function onObjectDelete($iObjectId = 0)
+    {
+        $this->_oQuery->deleteObjectVotes($iObjectId ? $iObjectId : $this->getId());
+    }
+
+    /**
+     * Internal functions
+     */
+    protected function _getAuthorId ()
+    {
+        return isMember() ? bx_get_logged_profile_id() : 0;
+    }
+
+    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 _getIconDoLike($bVoted)
+    {
+    	return $bVoted && $this->isUndo() ?  'thumbs-down' : 'thumbs-up';
+    }
+
+    protected function _getTitleDoLike($bVoted)
+    {
+    	return $bVoted && $this->isUndo() ? '_vote_do_unlike' : '_vote_do_like';
+    }
+
+    protected function _triggerVote()
+    {
+        if(!$this->_aSystem['trigger_table'])
+            return false;
+
+        $iId = $this->getId();
+        if(!$iId)
+            return false;
+
+        return $this->_oQuery->updateTriggerTable($iId);
+    }
+}
+
+/** @} */

Some files were not shown because too many files changed in this diff